applicationKeys) throws RemoteException {
- checkRecoverKeyStorePermission();
+ checkRecoverKeyStoreGmsCorePermission();
int userId = UserHandle.getCallingUserId();
int uid = Binder.getCallingUid();
RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
@@ -702,13 +704,13 @@ public void setRecoverySecretTypes(
* Destroys the session with the given {@code sessionId}.
*/
public void closeSession(@NonNull String sessionId) throws RemoteException {
- checkRecoverKeyStorePermission();
+ checkRecoverKeyStoreGmsCorePermission();
Objects.requireNonNull(sessionId, "invalid session");
mRecoverySessionStorage.remove(Binder.getCallingUid(), sessionId);
}
public void removeKey(@NonNull String alias) throws RemoteException {
- checkRecoverKeyStorePermission();
+ checkRecoverKeyStoreGmsCorePermission();
Objects.requireNonNull(alias, "alias is null");
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
@@ -747,7 +749,7 @@ public String generateKey(@NonNull String alias) throws RemoteException {
*/
public String generateKeyWithMetadata(@NonNull String alias, @Nullable byte[] metadata)
throws RemoteException {
- checkRecoverKeyStorePermission();
+ checkRecoverKeyStoreGmsCorePermission();
Objects.requireNonNull(alias, "alias is null");
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
@@ -852,7 +854,7 @@ public String generateKeyWithMetadata(@NonNull String alias, @Nullable byte[] me
* @return grant alias, which caller can use to access the key.
*/
public @Nullable String getKey(@NonNull String alias) throws RemoteException {
- checkRecoverKeyStorePermission();
+ checkRecoverKeyStoreGmsCorePermission();
Objects.requireNonNull(alias, "alias is null");
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
@@ -998,6 +1000,27 @@ public void lockScreenSecretChanged(@NonNull LockscreenCredential credential, in
}
}
+ /**
+ * Removes recoverable keystore state for a recovery agent.
+ *
+ * Run the cleanup on the key sync executor so it cannot race a previously scheduled key
+ * sync task.
+ */
+ public void removeRecoverableKeystoreStateForRecoveryAgent(int userId, int uid) {
+ try {
+ mExecutorService.submit(() -> {
+ mRecoverySessionStorage.remove(uid);
+ mListenersStorage.remove(uid);
+ mCleanupManager.removeDataForRecoveryAgent(userId, uid);
+ }).get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.e(TAG, "Interrupted while removing recoverable keystore state for uid " + uid, e);
+ } catch (ExecutionException e) {
+ Log.e(TAG, "Failed to remove recoverable keystore state for uid " + uid, e.getCause());
+ }
+ }
+
/**
* Starts a session to verify lock screen credentials provided by a remote device.
*/
@@ -1154,9 +1177,25 @@ private int lockPatternUtilsToKeyguardType(int credentialsType) {
}
private void checkRecoverKeyStorePermission() {
- mContext.enforceCallingOrSelfPermission(
- Manifest.permission.RECOVER_KEYSTORE,
- "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission.");
+ checkRecoverKeyStorePermission(/*allowGmsCorePermission=*/ false);
+ }
+
+ private void checkRecoverKeyStoreGmsCorePermission() {
+ checkRecoverKeyStorePermission(/*allowGmsCorePermission=*/ true);
+ }
+
+ private void checkRecoverKeyStorePermission(boolean allowGmsCorePermission) {
+ boolean permissionGranted = mContext.checkCallingOrSelfPermission(
+ Manifest.permission.RECOVER_KEYSTORE) == PackageManager.PERMISSION_GRANTED;
+ if (!permissionGranted && allowGmsCorePermission) {
+ permissionGranted = mContext.checkCallingOrSelfPermission(
+ Manifest.permission.RECOVER_KEYSTORE_GMSCORE)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ if (!permissionGranted) {
+ throw new SecurityException("Caller " + Binder.getCallingUid()
+ + " doesn't have RecoverKeyStore permission.");
+ }
int userId = UserHandle.getCallingUserId();
int uid = Binder.getCallingUid();
mCleanupManager.registerRecoveryAgent(userId, uid);
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java
index bd9f0fdbe63dd..d5bd5f00fb250 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorage.java
@@ -64,6 +64,14 @@ public synchronized boolean hasListener(int recoveryAgentUid) {
return mAgentIntents.get(recoveryAgentUid) != null;
}
+ /**
+ * Removes the listener and pending snapshot state for the recovery agent.
+ */
+ public synchronized void remove(int recoveryAgentUid) {
+ mAgentIntents.remove(recoveryAgentUid);
+ mAgentsWithPendingSnapshots.remove(recoveryAgentUid);
+ }
+
/**
* Notifies recovery agent that new snapshot is available. If a recovery agent has not yet
* registered a {@link PendingIntent}, remembers that a snapshot is pending for it, so that
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java
index ef56a1e268543..2dcfa35dfd37e 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorage.java
@@ -35,6 +35,9 @@
import java.security.KeyStore.SecretKeyEntry;
import java.security.KeyStoreException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
import java.util.Locale;
import javax.crypto.spec.SecretKeySpec;
@@ -105,6 +108,39 @@ public void deleteEntry(int userId, int uid, String alias) {
}
}
+ /**
+ * Deletes all application keys belonging to the given recovery agent.
+ */
+ public void deleteEntriesForRecoveryAgent(int userId, int uid) {
+ String aliasPrefix = getInternalAliasPrefix(userId, uid);
+ List aliasesToDelete = new ArrayList<>();
+ try {
+ Enumeration aliases = mKeyStore.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ if (alias.startsWith(aliasPrefix)) {
+ aliasesToDelete.add(alias);
+ }
+ }
+ } catch (KeyStoreException e) {
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ }
+
+ KeyStoreException deletionException = null;
+ for (String alias : aliasesToDelete) {
+ try {
+ mKeyStore.deleteEntry(alias);
+ } catch (KeyStoreException e) {
+ Log.e(TAG, "Failed to delete recoverable application key " + alias, e);
+ deletionException = e;
+ }
+ }
+ if (deletionException != null) {
+ throw new ServiceSpecificException(
+ ERROR_SERVICE_INTERNAL_ERROR, deletionException.getMessage());
+ }
+ }
+
/**
* Returns the alias in locksettins service's KeyStore namespace used for given application key.
*
@@ -117,7 +153,11 @@ public void deleteEntry(int userId, int uid, String alias) {
* @return The alias.
*/
private String getInternalAlias(int userId, int uid, String alias) {
- return APPLICATION_KEY_ALIAS_PREFIX + userId + "/" + uid + "/" + alias;
+ return getInternalAliasPrefix(userId, uid) + alias;
+ }
+
+ private String getInternalAliasPrefix(int userId, int uid) {
+ return APPLICATION_KEY_ALIAS_PREFIX + userId + "/" + uid + "/";
}
private String makeKeystoreEngineGrantString(int uid, String alias) {
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java
index 7d5fd652ef38f..f1b789eccce8b 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java
@@ -154,6 +154,16 @@ private void removeDataForUser(int userId) {
mDatabase.removeUserFromAllTables(userId);
}
+ /**
+ * Removes all data belonging to the given recovery agent.
+ */
+ public synchronized void removeDataForRecoveryAgent(int userId, int uid) {
+ Log.d(TAG, "Removing data for recovery agent " + uid + " for user " + userId + ".");
+ mSnapshotStorage.remove(uid);
+ removeAllKeysForRecoveryAgentByPrefix(userId, uid);
+ mDatabase.removeRecoveryAgentFromAllTables(userId, uid);
+ }
+
/**
* Removes keys from Android KeyStore for the recovery agent;
* Doesn't remove encrypted key material from the database.
@@ -171,4 +181,13 @@ private void removeAllKeysForRecoveryAgent(int userId, int uid) {
}
}
}
+
+ private void removeAllKeysForRecoveryAgentByPrefix(int userId, int uid) {
+ try {
+ mApplicationKeyStorage.deleteEntriesForRecoveryAgent(userId, uid);
+ } catch (ServiceSpecificException e) {
+ // Ignore errors during key removal.
+ Log.e(TAG, "Error while removing recoverable keys for uid " + uid, e);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
index f2c55544e859c..32828c54ecbab 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -1346,6 +1346,38 @@ public void removeUserFromAllTables(int userId) {
removeUserFromRootOfTrustTable(userId);
}
+ /**
+ * Removes all entries for the given recovery agent without removing user-wide metadata.
+ */
+ public void removeRecoveryAgentFromAllTables(int userId, int uid) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ String[] selectionArgs = {
+ Integer.toString(userId),
+ Integer.toString(uid)
+ };
+
+ db.beginTransaction();
+ try {
+ // Use each table's own contract so ownership-column changes are caught during rebases.
+ deleteRecoveryAgentRows(db, KeysEntry.TABLE_NAME, KeysEntry.COLUMN_NAME_USER_ID,
+ KeysEntry.COLUMN_NAME_UID, selectionArgs);
+ deleteRecoveryAgentRows(db, RecoveryServiceMetadataEntry.TABLE_NAME,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID, selectionArgs);
+ deleteRecoveryAgentRows(db, RootOfTrustEntry.TABLE_NAME,
+ RootOfTrustEntry.COLUMN_NAME_USER_ID, RootOfTrustEntry.COLUMN_NAME_UID,
+ selectionArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private static void deleteRecoveryAgentRows(SQLiteDatabase db, String tableName,
+ String userIdColumn, String uidColumn, String[] selectionArgs) {
+ db.delete(tableName, userIdColumn + " = ? AND " + uidColumn + " = ?", selectionArgs);
+ }
+
/**
* Removes all entries for given userId from Keys table.
*
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
index 599ba89478a25..60e363543bca0 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
@@ -21,7 +21,9 @@
/**
* Contract for recoverable key database. Describes the tables present.
*
- * Make sure that {@code removeUserFromAllKnownTables} is updated, when new table is added.
+ * Make sure that {@link RecoverableKeyStoreDb#removeUserFromAllTables(int)} is updated when a table
+ * is added. If the table contains recovery-agent state, also update
+ * {@link RecoverableKeyStoreDb#removeRecoveryAgentFromAllTables(int, int)}.
*/
class RecoverableKeyStoreDbContract {
/**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
index ef6dfe96f58a6..b2a2ac0e8ebd0 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
@@ -80,7 +80,13 @@ public void remove(int uid, String sessionId) {
if (mSessionsByUid.get(uid) == null) {
return;
}
- mSessionsByUid.get(uid).removeIf(session -> session.mSessionId.equals(sessionId));
+ mSessionsByUid.get(uid).removeIf(session -> {
+ if (!session.mSessionId.equals(sessionId)) {
+ return false;
+ }
+ session.destroy();
+ return true;
+ });
}
/**
diff --git a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java
index 4a8c2b52a80c2..013e02c963363 100644
--- a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java
+++ b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java
@@ -7,8 +7,9 @@
import android.app.ActivityManagerInternal;
import android.content.pm.GosPackageState;
import android.content.pm.GosPackageStateFlag;
-import android.ext.KnownSystemPackages;
import android.ext.DerivedPackageFlag;
+import android.ext.KnownSystemPackages;
+import android.ext.PackageId;
import android.os.Binder;
import android.os.Process;
import android.os.RemoteException;
@@ -19,7 +20,9 @@
import com.android.internal.pm.parsing.pkg.AndroidPackageInternal;
import com.android.internal.pm.pkg.component.ParsedUsesPermission;
import com.android.server.LocalServices;
+import com.android.server.locksettings.LockSettingsInternal;
import com.android.server.pm.PackageManagerLocal.GosPackageStateChangeCallback;
+import com.android.server.pm.ext.PackageExt;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.PackageUserStateInternal;
@@ -352,6 +355,38 @@ public static void onClearApplicationUserData(PackageManagerService pm, String p
}
}
+ static void onApplicationDataCleared(
+ PackageManagerService pm, String packageName, int userId) {
+ PackageStateInternal packageState =
+ pm.snapshotComputer().getPackageStateInternal(packageName);
+ if (shouldClearGmsCoreRecoverableKeystoreState(packageState)) {
+ clearGmsCoreRecoverableKeystoreState(
+ pm, userId, packageState.getAppId());
+ }
+ }
+
+ static boolean shouldClearGmsCoreRecoverableKeystoreState(
+ @Nullable PackageStateInternal packageState) {
+ return packageState != null
+ && !packageState.isSystem()
+ && PackageExt.get(packageState).getPackageId() == PackageId.GMS_CORE;
+ }
+
+ static void clearGmsCoreRecoverableKeystoreState(
+ PackageManagerService pm, int userId, int appId) {
+ LockSettingsInternal lockSettings =
+ LocalServices.getService(LockSettingsInternal.class);
+ if (lockSettings == null) {
+ Slog.w(TAG, "LockSettingsInternal is unavailable");
+ return;
+ }
+
+ for (int realUserId : pm.resolveUserIds(userId)) {
+ lockSettings.removeRecoverableKeystoreStateForRecoveryAgent(
+ realUserId, UserHandle.getUid(realUserId, appId));
+ }
+ }
+
private static void clearContactScopesStorage(PackageManagerService pm, int userId) {
for (PackageStateInternal ps : pm.snapshotComputer().getPackageStates().values()) {
PackageUserStateInternal us = ps.getUserStateOrDefault(userId);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 201d99aa9e9af..955d9d1aad790 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -5176,6 +5176,10 @@ public void run() {
callingUid);
}
}
+ if (succeeded) {
+ GosPackageStatePmHooks.onApplicationDataCleared(
+ PackageManagerService.this, packageName, userId);
+ }
}
if (succeeded) {
// invoke DeviceStorageMonitor's update method to clear any notifications
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 7099c2e0bd468..3fb5e14a68077 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -353,14 +353,21 @@ public void clearPackageStateForUserLIF(PackageSetting ps, @CanBeALL @UserIdInt
if (ps.getAppId() == SYSTEM_UID) {
return;
}
+ final boolean clearGmsCoreRecoverableKeystoreState =
+ GosPackageStatePmHooks.shouldClearGmsCoreRecoverableKeystoreState(ps);
+ final int appId = ps.getAppId();
mPm.mInjector.getBackgroundHandler().post(() -> {
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER,
- "clearKeystoreData:" + ps.getAppId() + " for user: " + userId);
- mAppDataHelper.clearKeystoreData(userId, ps.getAppId());
+ "clearKeystoreData:" + appId + " for user: " + userId);
+ mAppDataHelper.clearKeystoreData(userId, appId);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
+ if (clearGmsCoreRecoverableKeystoreState) {
+ GosPackageStatePmHooks.clearGmsCoreRecoverableKeystoreState(
+ mPm, userId, appId);
+ }
});
}
diff --git a/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java b/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java
index 5abcf28bc28a5..4aa0d5fc4835b 100644
--- a/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java
+++ b/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java
@@ -41,6 +41,9 @@ public int overridePermissionState(String permission, int userId) {
case Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER:
flag = GmsCorePackageFlag.GRANT_PERMS_FOR_ICC_AUTHENTICATION;
break;
+ case Manifest.permission.RECOVER_KEYSTORE_GMSCORE:
+ flag = GmsCorePackageFlag.GRANT_PERMS_FOR_RECOVER_KEYSTORE_GMSCORE;
+ break;
default:
return NO_PERMISSION_OVERRIDE;
}
@@ -99,7 +102,8 @@ public List addUsesPermissions() {
var l = createUsesPerms(
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Manifest.permission.READ_PHONE_NUMBERS,
- Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
+ Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
+ Manifest.permission.RECOVER_KEYSTORE_GMSCORE
);
res.addAll(l);
return res;
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
index 0965916588920..bed579fd65c86 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
@@ -95,6 +95,7 @@
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import javax.crypto.KeyGenerator;
@@ -172,6 +173,7 @@ public class RecoverableKeyStoreManagerTest {
@Mock private ApplicationKeyStorage mApplicationKeyStorage;
@Mock private CleanupManager mCleanupManager;
@Mock private ScheduledExecutorService mExecutorService;
+ @Mock private Future> mFuture;
@Mock private LockSettingsService mLockSettingsService;
@Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
@@ -764,9 +766,8 @@ public void startRecoverySession_checksPermissionFirst() throws Exception {
TEST_VAULT_CHALLENGE,
ImmutableList.of(TEST_PROTECTION_PARAMS));
- verify(mMockContext, times(1))
- .enforceCallingOrSelfPermission(
- eq(Manifest.permission.RECOVER_KEYSTORE), any());
+ verify(mMockContext, times(1)).checkCallingOrSelfPermission(
+ eq(Manifest.permission.RECOVER_KEYSTORE));
}
@Ignore("Causing breakages so ignoring to resolve, b/281583079")
@@ -1133,6 +1134,42 @@ public void setSnapshotCreatedPendingIntent() throws Exception {
verify(mMockListenersStorage).setSnapshotListener(eq(uid), any(PendingIntent.class));
}
+ @Test
+ public void removeRecoverableKeystoreStateForRecoveryAgent_removesOnlyTargetAgentState()
+ throws Exception {
+ int uid = 1234;
+ int otherUid = 5678;
+ mRecoverySessionStorage.add(
+ uid,
+ new RecoverySessionStorage.Entry(
+ TEST_SESSION_ID,
+ TEST_SECRET.clone(),
+ TEST_SALT.clone(),
+ TEST_VAULT_PARAMS.clone()));
+ mRecoverySessionStorage.add(
+ otherUid,
+ new RecoverySessionStorage.Entry(
+ TEST_SESSION_ID,
+ TEST_SECRET.clone(),
+ TEST_SALT.clone(),
+ TEST_VAULT_PARAMS.clone()));
+ when(mExecutorService.submit(any(Runnable.class)))
+ .thenAnswer(
+ invocation -> {
+ invocation.getArgument(0).run();
+ return mFuture;
+ });
+
+ mRecoverableKeyStoreManager.removeRecoverableKeystoreStateForRecoveryAgent(
+ TEST_USER_ID, uid);
+
+ verify(mFuture).get();
+ assertThat(mRecoverySessionStorage.get(uid, TEST_SESSION_ID)).isNull();
+ assertThat(mRecoverySessionStorage.get(otherUid, TEST_SESSION_ID)).isNotNull();
+ verify(mMockListenersStorage).remove(uid);
+ verify(mCleanupManager).removeDataForRecoveryAgent(TEST_USER_ID, uid);
+ }
+
@Test
public void setServerParams_updatesServerParams() throws Exception {
int uid = Binder.getCallingUid();
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java
index 418d474523306..074a311d9a97d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java
@@ -49,6 +49,58 @@ public void hasListener_isTrueForRegisteredUid() {
assertTrue(mStorage.hasListener(recoveryAgentUid));
}
+ @Test
+ public void remove_clearsListener() {
+ int recoveryAgentUid = 1000;
+ PendingIntent intent =
+ PendingIntent.getBroadcast(
+ InstrumentationRegistry.getTargetContext(),
+ /* requestCode= */ 1,
+ new Intent()
+ .setPackage(
+ InstrumentationRegistry.getTargetContext()
+ .getPackageName()),
+ /* flags= */ PendingIntent.FLAG_MUTABLE);
+ mStorage.setSnapshotListener(recoveryAgentUid, intent);
+
+ mStorage.remove(recoveryAgentUid);
+
+ assertFalse(mStorage.hasListener(recoveryAgentUid));
+ }
+
+ @Test
+ public void remove_clearsPendingSnapshot() throws Exception {
+ Context context = InstrumentationRegistry.getTargetContext();
+ int recoveryAgentUid = 1000;
+ mStorage.recoverySnapshotAvailable(recoveryAgentUid);
+ mStorage.remove(recoveryAgentUid);
+ PendingIntent intent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 0,
+ new Intent(TEST_INTENT_ACTION).setPackage(context.getPackageName()),
+ /* flags= */ PendingIntent.FLAG_MUTABLE);
+ CountDownLatch latch = new CountDownLatch(1);
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ latch.countDown();
+ }
+ };
+ context.registerReceiver(
+ broadcastReceiver,
+ new IntentFilter(TEST_INTENT_ACTION),
+ Context.RECEIVER_EXPORTED_UNAUDITED);
+
+ try {
+ mStorage.setSnapshotListener(recoveryAgentUid, intent);
+ assertFalse(latch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ } finally {
+ context.unregisterReceiver(broadcastReceiver);
+ }
+ }
+
@Test
public void setSnapshotListener_invokesIntentImmediatelyIfPreviouslyNotified()
throws Exception {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorageTest.java
new file mode 100644
index 0000000000000..6ff7d13d0c9e0
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/ApplicationKeyStorageTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2026 GrapheneOS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.os.ServiceSpecificException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.locksettings.recoverablekeystore.KeyStoreProxy;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.KeyStoreException;
+import java.util.Arrays;
+import java.util.Collections;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ApplicationKeyStorageTest {
+ private static final int USER_ID = 10;
+ private static final int UID = 1012345;
+ private static final String ALIAS_PREFIX =
+ "com.android.server.locksettings.recoverablekeystore/application/10/1012345/";
+ private static final String MATCHING_ALIAS_1 = ALIAS_PREFIX + "first";
+ private static final String MATCHING_ALIAS_2 = ALIAS_PREFIX + "second";
+
+ @Mock private KeyStoreProxy mKeyStore;
+
+ private ApplicationKeyStorage mStorage;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mStorage = new ApplicationKeyStorage(mKeyStore);
+ }
+
+ @Test
+ public void deleteEntriesForRecoveryAgent_deletesOnlyMatchingAliases() throws Exception {
+ when(mKeyStore.aliases())
+ .thenReturn(
+ Collections.enumeration(
+ Arrays.asList(
+ MATCHING_ALIAS_1,
+ "com.android.server.locksettings.recoverablekeystore/application/11/1012345/first",
+ "com.android.server.locksettings.recoverablekeystore/application/10/1012346/first",
+ "com.android.server.locksettings.recoverablekeystore/application/10/10123450/first",
+ MATCHING_ALIAS_2)));
+
+ mStorage.deleteEntriesForRecoveryAgent(USER_ID, UID);
+
+ verify(mKeyStore).aliases();
+ verify(mKeyStore).deleteEntry(MATCHING_ALIAS_1);
+ verify(mKeyStore).deleteEntry(MATCHING_ALIAS_2);
+ verifyNoMoreInteractions(mKeyStore);
+ }
+
+ @Test
+ public void deleteEntriesForRecoveryAgent_continuesAfterDeletionFailure() throws Exception {
+ when(mKeyStore.aliases())
+ .thenReturn(
+ Collections.enumeration(Arrays.asList(MATCHING_ALIAS_1, MATCHING_ALIAS_2)));
+ doThrow(new KeyStoreException("failure")).when(mKeyStore).deleteEntry(MATCHING_ALIAS_1);
+
+ assertThrows(
+ ServiceSpecificException.class,
+ () -> mStorage.deleteEntriesForRecoveryAgent(USER_ID, UID));
+
+ verify(mKeyStore).deleteEntry(MATCHING_ALIAS_1);
+ verify(mKeyStore).deleteEntry(MATCHING_ALIAS_2);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java
index 1c9c6dc8d0db3..8065a2f6f26d2 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java
@@ -16,7 +16,9 @@
package com.android.server.locksettings.recoverablekeystore.storage;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -27,6 +29,8 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -108,5 +112,33 @@ public void verifyKnownUsers_newSerialNumber_deletesData() throws Exception {
verify(mDatabase).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER_2);
verify(mRecoverySnapshotStorage).remove(UID);
}
-}
+ @Test
+ public void verifyKnownUsers_removedUser_removesAllUserState() throws Exception {
+ Map knownSerialNumbers = new HashMap<>();
+ knownSerialNumbers.put(USER_ID, USER_SERIAL_NUMBER);
+ when(mDatabase.getUserSerialNumbers()).thenReturn(knownSerialNumbers);
+ when(mDatabase.getRecoveryAgents(USER_ID)).thenReturn(List.of(UID));
+ when(mDatabase.getPlatformKeyGenerationId(USER_ID)).thenReturn(1);
+ Map keys = new HashMap<>();
+ keys.put("alias", new WrappedKey(new byte[] {1}, new byte[] {2}, null, 1));
+ when(mDatabase.getAllKeys(USER_ID, UID, 1)).thenReturn(keys);
+ when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID)))).thenReturn(-1L);
+
+ mManager.verifyKnownUsers();
+
+ verify(mRecoverySnapshotStorage).remove(UID);
+ verify(mApplicationKeyStorage).deleteEntry(USER_ID, UID, "alias");
+ verify(mDatabase).removeUserFromAllTables(USER_ID);
+ }
+
+ @Test
+ public void removeDataForRecoveryAgent_removesOnlyAgentState() {
+ mManager.removeDataForRecoveryAgent(USER_ID, UID);
+
+ verify(mRecoverySnapshotStorage).remove(UID);
+ verify(mApplicationKeyStorage).deleteEntriesForRecoveryAgent(USER_ID, UID);
+ verify(mDatabase).removeRecoveryAgentFromAllTables(USER_ID, UID);
+ verify(mDatabase, never()).removeUserFromAllTables(anyInt());
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
index 036742559ef69..b96136e6f0c6d 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
@@ -448,6 +448,52 @@ public void removeUserFromAllTables_removesData() throws Exception {
assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEmpty();
}
+ @Test
+ public void removeRecoveryAgentFromAllTables_removesOnlyAgentData() throws Exception {
+ int userId = 12;
+ int uid = 120001;
+ int otherUid = 120002;
+ int generationId = 24;
+ long userSerialNumber = 42L;
+ String alias = "target";
+ String otherAlias = "other";
+ WrappedKey wrappedKey =
+ new WrappedKey(
+ getUtf8Bytes("nonce"),
+ getUtf8Bytes("key"),
+ /* metadata= */ null,
+ generationId);
+ mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId);
+ mRecoverableKeyStoreDb.setUserSerialNumber(userId, userSerialNumber);
+ mRecoverableKeyStoreDb.insertKey(userId, uid, alias, wrappedKey);
+ mRecoverableKeyStoreDb.insertKey(userId, otherUid, otherAlias, wrappedKey);
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, new int[] {1});
+ mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, otherUid, new int[] {2});
+ mRecoverableKeyStoreDb.setRecoveryServiceCertSerial(userId, uid, TEST_ROOT_CERT_ALIAS, 1L);
+ mRecoverableKeyStoreDb.setRecoveryServiceCertSerial(
+ userId, otherUid, TEST_ROOT_CERT_ALIAS, 2L);
+
+ mRecoverableKeyStoreDb.removeRecoveryAgentFromAllTables(userId, uid);
+
+ assertThat(mRecoverableKeyStoreDb.getKey(uid, alias)).isNull();
+ assertThat(mRecoverableKeyStoreDb.getKey(otherUid, otherAlias)).isNotNull();
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEmpty();
+ assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, otherUid))
+ .isEqualTo(new int[] {2});
+ assertThat(
+ mRecoverableKeyStoreDb.getRecoveryServiceCertSerial(
+ userId, uid, TEST_ROOT_CERT_ALIAS))
+ .isNull();
+ assertThat(
+ mRecoverableKeyStoreDb.getRecoveryServiceCertSerial(
+ userId, otherUid, TEST_ROOT_CERT_ALIAS))
+ .isEqualTo(2L);
+ assertThat(mRecoverableKeyStoreDb.getPlatformKeyGenerationId(userId))
+ .isEqualTo(generationId);
+ assertThat(mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId))
+ .isEqualTo(userSerialNumber);
+ }
+
@Test
public void setRecoveryStatus_withSingleKey() {
int userId = 12;