Merge "Store user's serial number in recoverable key store database."
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index 47b9c27..1b14ce2 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -53,6 +53,7 @@
 import com.android.server.locksettings.recoverablekeystore.certificate.CertXml;
 import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
 import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
+import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
@@ -100,6 +101,7 @@
     private final PlatformKeyManager mPlatformKeyManager;
     private final ApplicationKeyStorage mApplicationKeyStorage;
     private final TestOnlyInsecureCertificateHelper mTestCertHelper;
+    private final CleanupManager mCleanupManager;
 
     /**
      * Returns a new or existing instance.
@@ -122,16 +124,24 @@
                 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
             }
 
+            RecoverySnapshotStorage snapshotStorage =
+                    RecoverySnapshotStorage.newInstance();
+            CleanupManager cleanupManager = CleanupManager.getInstance(
+                    context.getApplicationContext(),
+                    snapshotStorage,
+                    db,
+                    applicationKeyStorage);
             mInstance = new RecoverableKeyStoreManager(
                     context.getApplicationContext(),
                     db,
                     new RecoverySessionStorage(),
                     Executors.newSingleThreadExecutor(),
-                    RecoverySnapshotStorage.newInstance(),
+                    snapshotStorage,
                     new RecoverySnapshotListenersStorage(),
                     platformKeyManager,
                     applicationKeyStorage,
-                    new TestOnlyInsecureCertificateHelper());
+                    new TestOnlyInsecureCertificateHelper(),
+                    cleanupManager);
         }
         return mInstance;
     }
@@ -146,7 +156,8 @@
             RecoverySnapshotListenersStorage listenersStorage,
             PlatformKeyManager platformKeyManager,
             ApplicationKeyStorage applicationKeyStorage,
-            TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
+            TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
+            CleanupManager cleanupManager) {
         mContext = context;
         mDatabase = recoverableKeyStoreDb;
         mRecoverySessionStorage = recoverySessionStorage;
@@ -155,8 +166,10 @@
         mSnapshotStorage = snapshotStorage;
         mPlatformKeyManager = platformKeyManager;
         mApplicationKeyStorage = applicationKeyStorage;
-        mTestCertHelper = TestOnlyInsecureCertificateHelper;
-
+        mTestCertHelper = testOnlyInsecureCertificateHelper;
+        mCleanupManager = cleanupManager;
+        // Clears data for removed users.
+        mCleanupManager.verifyKnownUsers();
         try {
             mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
         } catch (NoSuchAlgorithmException e) {
@@ -955,6 +968,9 @@
         mContext.enforceCallingOrSelfPermission(
                 Manifest.permission.RECOVER_KEYSTORE,
                 "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission.");
+        int userId = UserHandle.getCallingUserId();
+        int uid = Binder.getCallingUid();
+        mCleanupManager.registerRecoveryAgent(userId, uid);
     }
 
     private boolean publicKeysMatch(PublicKey publicKey, byte[] vaultParams) {
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
new file mode 100644
index 0000000..be35b50
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/CleanupManager.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 android.content.Context;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Cleans up data when user is removed.
+ */
+public class CleanupManager {
+    private static final String TAG = "CleanupManager";
+
+    private final Context mContext;
+    private final UserManager mUserManager;
+    private final RecoverableKeyStoreDb mDatabase;
+    private final RecoverySnapshotStorage mSnapshotStorage;
+    private final ApplicationKeyStorage mApplicationKeyStorage;
+
+    // Serial number can not be changed at runtime.
+    private Map<Integer, Long> mSerialNumbers; // Always in sync with the database.
+
+    /**
+     * Creates a new instance of the class.
+     * IMPORTANT: {@code verifyKnownUsers} must be called before the first data access.
+     */
+    public static CleanupManager getInstance(
+            Context context,
+            RecoverySnapshotStorage snapshotStorage,
+            RecoverableKeyStoreDb recoverableKeyStoreDb,
+            ApplicationKeyStorage applicationKeyStorage) {
+        return new CleanupManager(
+                context,
+                snapshotStorage,
+                recoverableKeyStoreDb,
+                UserManager.get(context),
+                applicationKeyStorage);
+    }
+
+    @VisibleForTesting
+    CleanupManager(
+            Context context,
+            RecoverySnapshotStorage snapshotStorage,
+            RecoverableKeyStoreDb recoverableKeyStoreDb,
+            UserManager userManager,
+            ApplicationKeyStorage applicationKeyStorage) {
+        mContext = context;
+        mSnapshotStorage = snapshotStorage;
+        mDatabase = recoverableKeyStoreDb;
+        mUserManager = userManager;
+        mApplicationKeyStorage = applicationKeyStorage;
+    }
+
+    /**
+     * Registers recovery agent in the system, if necessary.
+     */
+    public synchronized void registerRecoveryAgent(int userId, int uid) {
+        if (mSerialNumbers == null) {
+            // Table was uninitialized.
+            verifyKnownUsers();
+        }
+        // uid is ignored since recovery agent is a system app.
+        Long storedSerialNumber =  mSerialNumbers.get(userId);
+        if (storedSerialNumber == null) {
+            storedSerialNumber = -1L;
+        }
+        if (storedSerialNumber != -1) {
+            // User was already registered.
+            return;
+        }
+        // User was added after {@code verifyAllUsers} call.
+        long currentSerialNumber = mUserManager.getSerialNumberForUser(UserHandle.of(userId));
+        if (currentSerialNumber != -1) {
+            storeUserSerialNumber(userId, currentSerialNumber);
+        }
+    }
+
+    /**
+     * Removes data if serial number for a user was changed.
+     */
+    public synchronized void verifyKnownUsers() {
+        mSerialNumbers =  mDatabase.getUserSerialNumbers();
+        List<Integer> deletedUserIds = new ArrayList<Integer>(){};
+        for (Map.Entry<Integer, Long> entry : mSerialNumbers.entrySet()) {
+            Integer userId = entry.getKey();
+            Long storedSerialNumber = entry.getValue();
+            if (storedSerialNumber == null) {
+                storedSerialNumber = -1L;
+            }
+            long currentSerialNumber = mUserManager.getSerialNumberForUser(UserHandle.of(userId));
+            if (currentSerialNumber == -1) {
+                // User was removed.
+                deletedUserIds.add(userId);
+                removeDataForUser(userId);
+            } else if (storedSerialNumber == -1) {
+                // User is detected for the first time
+                storeUserSerialNumber(userId, currentSerialNumber);
+            } else if (storedSerialNumber != currentSerialNumber) {
+                // User has unexpected serial number - delete data related to old serial number.
+                deletedUserIds.add(userId);
+                removeDataForUser(userId);
+                // Register new user.
+                storeUserSerialNumber(userId, currentSerialNumber);
+            }
+        }
+
+        for (Integer deletedUser : deletedUserIds) {
+            mSerialNumbers.remove(deletedUser);
+        }
+    }
+
+    private void storeUserSerialNumber(int userId, long userSerialNumber) {
+        Log.d(TAG, "Storing serial number for user " + userId + ".");
+        mSerialNumbers.put(userId, userSerialNumber);
+        mDatabase.setUserSerialNumber(userId, userSerialNumber);
+    }
+
+    /**
+     * Removes all data for given user, including
+     *
+     * <ul>
+     *     <li> Recovery snapshots for all agents belonging to the {@code userId}.
+     *     <li> Entries with data related to {@code userId} from the database.
+     * </ul>
+     */
+    private void removeDataForUser(int userId) {
+        Log.d(TAG, "Removing data for user " + userId + ".");
+        List<Integer> recoveryAgents = mDatabase.getRecoveryAgents(userId);
+        for (Integer uid : recoveryAgents) {
+            mSnapshotStorage.remove(uid);
+            removeAllKeysForRecoveryAgent(userId, uid);
+        }
+
+        mDatabase.removeUserFromAllTables(userId);
+    }
+
+    /**
+     * Removes keys from Android KeyStore for the recovery agent;
+     * Doesn't remove encrypted key material from the database.
+     */
+    private void removeAllKeysForRecoveryAgent(int userId, int uid) {
+        int generationId = mDatabase.getPlatformKeyGenerationId(userId);
+        Map<String, WrappedKey> allKeys = mDatabase.getAllKeys(userId, uid, generationId);
+        for (String alias : allKeys.keySet()) {
+            try {
+                // Delete KeyStore copy.
+                mApplicationKeyStorage.deleteEntry(userId, uid, alias);
+            } catch (ServiceSpecificException e) {
+                // Ignore errors during key removal.
+                Log.e(TAG, "Error while removing recoverable key " + alias + " : " + 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 dffaffe..3f5ac8e 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
@@ -24,6 +24,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.security.keystore.recovery.RecoveryController;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 
 import com.android.server.locksettings.recoverablekeystore.TestOnlyInsecureCertificateHelper;
@@ -261,7 +262,7 @@
      *
      * @hide
      */
-    public Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid,
+    public @NonNull Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid,
             int platformKeyGenerationId) {
         SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
         String[] projection = {
@@ -337,6 +338,58 @@
     }
 
     /**
+     * Returns serial numbers associated with all known users.
+     * -1 is used for uninitialized serial numbers.
+     *
+     * See {@code UserHandle.getSerialNumberForUser}.
+     * @return Map from userId to serial numbers.
+     */
+    public @NonNull Map<Integer, Long> getUserSerialNumbers() {
+        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+        String[] projection = {
+                UserMetadataEntry.COLUMN_NAME_USER_ID,
+                UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER};
+        String selection = null; // get all rows.
+        String[] selectionArguments = {};
+
+        try (
+            Cursor cursor = db.query(
+                UserMetadataEntry.TABLE_NAME,
+                projection,
+                selection,
+                selectionArguments,
+                /*groupBy=*/ null,
+                /*having=*/ null,
+                /*orderBy=*/ null)
+        ) {
+            Map<Integer, Long> serialNumbers = new ArrayMap<>();
+            while (cursor.moveToNext()) {
+                int userId = cursor.getInt(
+                        cursor.getColumnIndexOrThrow(UserMetadataEntry.COLUMN_NAME_USER_ID));
+                long serialNumber = cursor.getLong(cursor.getColumnIndexOrThrow(
+                        UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER));
+                serialNumbers.put(userId, serialNumber);
+            }
+            return serialNumbers;
+        }
+    }
+
+    /**
+     * Sets the {@code serialNumber} for the user {@code userId}.
+     *
+     * @return The primary key of the inserted row, or -1 if failed.
+     */
+    public long setUserSerialNumber(int userId, long serialNumber) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId);
+        values.put(UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER, serialNumber);
+        long result = db.replace(
+                UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
+        return result;
+    }
+
+    /**
      * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}.
      */
     public void invalidateKeysWithOldGenerationId(int userId, int newGenerationId) {
@@ -424,8 +477,7 @@
      */
     @Nullable
     public Long getRecoveryServiceCertSerial(int userId, int uid, @NonNull String rootAlias) {
-        return getLong(userId, uid, rootAlias,
-                RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_SERIAL);
+        return getLong(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_SERIAL);
     }
 
     /**
@@ -441,7 +493,7 @@
      */
     public long setRecoveryServiceCertSerial(int userId, int uid, @NonNull String rootAlias,
             long serial) {
-        return setLong(userId, uid, rootAlias, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_SERIAL,
+        return setLong(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_SERIAL,
                 serial);
     }
 
@@ -457,8 +509,7 @@
      */
     @Nullable
     public CertPath getRecoveryServiceCertPath(int userId, int uid, @NonNull String rootAlias) {
-        byte[] bytes = getBytes(userId, uid, rootAlias,
-                RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_PATH);
+        byte[] bytes = getBytes(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_PATH);
         if (bytes == null) {
             return null;
         }
@@ -489,7 +540,7 @@
         if (certPath.getCertificates().size() == 0) {
             throw new CertificateEncodingException("No certificate contained in the cert path.");
         }
-        return setBytes(userId, uid, rootAlias, RecoveryServiceMetadataEntry.COLUMN_NAME_CERT_PATH,
+        return setBytes(userId, uid, rootAlias, RootOfTrustEntry.COLUMN_NAME_CERT_PATH,
                 certPath.getEncoded(CERT_PATH_ENCODING));
     }
 
@@ -1189,6 +1240,63 @@
                 RootOfTrustEntry.TABLE_NAME, values, selection, selectionArguments);
     }
 
+    /**
+     * Removes all entries for given {@code userId}.
+     */
+    public void removeUserFromAllTables(int userId) {
+        removeUserFromKeysTable(userId);
+        removeUserFromUserMetadataTable(userId);
+        removeUserFromRecoveryServiceMetadataTable(userId);
+        removeUserFromRootOfTrustTable(userId);
+    }
+
+    /**
+     * Removes all entries for given userId from Keys table.
+     *
+     * @return {@code true} if deleted a row.
+     */
+    private boolean removeUserFromKeysTable(int userId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        String selection = KeysEntry.COLUMN_NAME_USER_ID + " = ?";
+        String[] selectionArgs = {Integer.toString(userId)};
+        return db.delete(KeysEntry.TABLE_NAME, selection, selectionArgs) > 0;
+    }
+
+    /**
+     * Removes all entries for given userId from UserMetadata table.
+     *
+     * @return {@code true} if deleted a row.
+     */
+    private boolean removeUserFromUserMetadataTable(int userId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        String selection = UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
+        String[] selectionArgs = {Integer.toString(userId)};
+        return db.delete(UserMetadataEntry.TABLE_NAME, selection, selectionArgs) > 0;
+    }
+
+    /**
+     * Removes all entries for given userId from RecoveryServiceMetadata table.
+     *
+     * @return {@code true} if deleted a row.
+     */
+    private boolean removeUserFromRecoveryServiceMetadataTable(int userId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
+        String[] selectionArgs = {Integer.toString(userId)};
+        return db.delete(RecoveryServiceMetadataEntry.TABLE_NAME, selection, selectionArgs) > 0;
+    }
+
+    /**
+     * Removes all entries for given userId from RootOfTrust table.
+     *
+     * @return {@code true} if deleted a row.
+     */
+    private boolean removeUserFromRootOfTrustTable(int userId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        String selection = RootOfTrustEntry.COLUMN_NAME_USER_ID + " = ?";
+        String[] selectionArgs = {Integer.toString(userId)};
+        return db.delete(RootOfTrustEntry.TABLE_NAME, selection, selectionArgs) > 0;
+    }
 
     /**
      * Creates an empty row in the recovery service metadata table if such a row doesn't exist for
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 b58ee4b..e79d117 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
@@ -20,6 +20,8 @@
 
 /**
  * Contract for recoverable key database. Describes the tables present.
+ *
+ * Make sure that {@code removeUserFromAllKnownTables} is updated, when new table is added.
  */
 class RecoverableKeyStoreDbContract {
     /**
@@ -91,6 +93,11 @@
          * is used to wrap recoverable keys on disk.
          */
         static final String COLUMN_NAME_PLATFORM_KEY_GENERATION_ID = "platform_key_generation_id";
+
+        /**
+         * Serial number for the user which can not be reused. Default value is {@code -1}.
+         */
+        static final String COLUMN_NAME_USER_SERIAL_NUMBER = "user_serial_number";
     }
 
     /**
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
index b0613da..cd5e8cf 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
@@ -32,7 +32,7 @@
 class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
     private static final String TAG = "RecoverableKeyStoreDbHp";
 
-    static final int DATABASE_VERSION = 5;
+    static final int DATABASE_VERSION = 6; // Added user id serial number.
     private static final String DATABASE_NAME = "recoverablekeystore.db";
 
     private static final String SQL_CREATE_KEYS_ENTRY =
@@ -54,7 +54,8 @@
             "CREATE TABLE " + UserMetadataEntry.TABLE_NAME + "( "
                     + UserMetadataEntry._ID + " INTEGER PRIMARY KEY,"
                     + UserMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER UNIQUE,"
-                    + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER)";
+                    + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER,"
+                    + UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER + " INTEGER DEFAULT -1)";
 
     private static final String SQL_CREATE_RECOVERY_SERVICE_METADATA_ENTRY =
             "CREATE TABLE " + RecoveryServiceMetadataEntry.TABLE_NAME + " ("
@@ -141,6 +142,11 @@
             oldVersion = 5;
         }
 
+        if (oldVersion < 6 && newVersion >= 6) {
+            upgradeDbForVersion6(db);
+            oldVersion = 6;
+        }
+
         if (oldVersion != newVersion) {
             Log.e(TAG, "Failed to update recoverablekeystore database to the most recent version");
         }
@@ -179,6 +185,15 @@
                 KeysEntry.COLUMN_NAME_KEY_METADATA, "BLOB", /*defaultStr=*/ null);
     }
 
+    private void upgradeDbForVersion6(SQLiteDatabase db) {
+        Log.d(TAG, "Updating recoverable keystore database to version 6");
+        // adds a column to store the user serial number
+        addColumnToTable(db, UserMetadataEntry.TABLE_NAME,
+                UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER,
+                "INTEGER DEFAULT -1",
+                 /*defaultStr=*/ null);
+    }
+
     private static void addColumnToTable(
             SQLiteDatabase db, String tableName, String column, String columnType,
             String defaultStr) {
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 c78b96d..5bab65c 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
@@ -59,6 +59,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
+import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
@@ -154,6 +155,7 @@
     @Mock private KeyguardManager mKeyguardManager;
     @Mock private PlatformKeyManager mPlatformKeyManager;
     @Mock private ApplicationKeyStorage mApplicationKeyStorage;
+    @Mock private CleanupManager mCleanupManager;
     @Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
 
     private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
@@ -191,7 +193,8 @@
                 mMockListenersStorage,
                 mPlatformKeyManager,
                 mApplicationKeyStorage,
-                mTestOnlyInsecureCertificateHelper);
+                mTestOnlyInsecureCertificateHelper,
+                mCleanupManager);
     }
 
     @After
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
new file mode 100644
index 0000000..0b15a12
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/CleanupManagerTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CleanupManagerTest {
+    private static final int USER_ID = 10;
+    private static final int USER_ID_2 = 20;
+    private static final int UID = 1234;
+    private static final long USER_SERIAL_NUMBER = 101L;
+    private static final long USER_SERIAL_NUMBER_2 = 202L;
+
+    private Context mContext;
+    private CleanupManager mManager;
+
+    @Mock private RecoverableKeyStoreDb mDatabase;
+    @Mock private RecoverySnapshotStorage mRecoverySnapshotStorage;
+    @Mock private UserManager mUserManager;
+    @Mock private ApplicationKeyStorage mApplicationKeyStorage;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getTargetContext();
+        mManager = new CleanupManager(mContext, mRecoverySnapshotStorage, mDatabase, mUserManager,
+                mApplicationKeyStorage);
+    }
+
+    @Test
+    public void registerRecoveryAgent_unknownUser_storesInDb() throws Exception {
+        when(mDatabase.getUserSerialNumbers()).thenReturn(new HashMap<>());
+        when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID))))
+                .thenReturn(USER_SERIAL_NUMBER);
+        when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID_2))))
+                .thenReturn(USER_SERIAL_NUMBER_2);
+
+        mManager.registerRecoveryAgent(USER_ID, UID);
+        mManager.registerRecoveryAgent(USER_ID_2, UID);
+
+        verify(mDatabase).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER);
+        verify(mDatabase).setUserSerialNumber(USER_ID_2, USER_SERIAL_NUMBER_2);
+
+    }
+
+    @Test
+    public void registerRecoveryAgent_registersSameUser_doesntChangeDb() throws Exception {
+        when(mDatabase.getUserSerialNumbers()).thenReturn(new HashMap<>());
+        when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID))))
+                .thenReturn(USER_SERIAL_NUMBER);
+
+        mManager.registerRecoveryAgent(USER_ID, UID);
+        mManager.registerRecoveryAgent(USER_ID, UID); // ignored.
+
+        verify(mDatabase, times(1)).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER);
+    }
+
+    @Test
+    public void verifyKnownUsers_newSerialNumber_deletesData() throws Exception {
+        Map knownSerialNumbers = new HashMap<>();
+        knownSerialNumbers.put(USER_ID, USER_SERIAL_NUMBER);
+        when(mDatabase.getUserSerialNumbers()).thenReturn(knownSerialNumbers);
+        List<Integer> recoveryAgents = new ArrayList<>();
+        recoveryAgents.add(UID);
+        when(mDatabase.getRecoveryAgents(USER_ID)).thenReturn(recoveryAgents);
+
+        when(mUserManager.getSerialNumberForUser(eq(UserHandle.of(USER_ID))))
+                .thenReturn(USER_SERIAL_NUMBER_2); // new value
+
+
+        mManager.verifyKnownUsers();
+
+        verify(mDatabase).removeUserFromAllTables(USER_ID);
+        verify(mDatabase).setUserSerialNumber(USER_ID, USER_SERIAL_NUMBER_2);
+        verify(mRecoverySnapshotStorage).remove(UID);
+    }
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java
index 35215c3..2658af6 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelperTest.java
@@ -51,6 +51,7 @@
     private static final long TEST_LAST_SYNCED_AT = 1517990732000L;
     private static final int TEST_RECOVERY_STATUS = 3;
     private static final int TEST_PLATFORM_KEY_GENERATION_ID = 11;
+    private static final int TEST_USER_SERIAL_NUMBER = 15;
     private static final int TEST_SNAPSHOT_VERSION = 31;
     private static final int TEST_SHOULD_CREATE_SNAPSHOT = 1;
     private static final byte[] TEST_PUBLIC_KEY = "test-public-key".getBytes(UTF_8);
@@ -234,5 +235,14 @@
 
         assertThat(mDatabase.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values))
                 .isGreaterThan(-1L);
+
+        // User serial number column was added when upgrading from v5 to v6
+        values = new ContentValues();
+        values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, TEST_USER_ID);
+        values.put(UserMetadataEntry.COLUMN_NAME_USER_SERIAL_NUMBER, TEST_USER_SERIAL_NUMBER);
+        assertThat(
+                mDatabase.replace(UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values))
+                .isGreaterThan(-1L);
     }
+
 }
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 7de9ffc..932a769 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
@@ -279,6 +279,55 @@
     }
 
     @Test
+    public void getUserSerialNumbers_returnsSerialNumbers() {
+        int userId = 42;
+        int userId2 = 44;
+        Long serialNumber = 24L;
+        Long serialNumber2 = 25L;
+        mRecoverableKeyStoreDb.setUserSerialNumber(userId, serialNumber);
+        mRecoverableKeyStoreDb.setUserSerialNumber(userId2, serialNumber2);
+
+        assertEquals(2, mRecoverableKeyStoreDb.getUserSerialNumbers().size());
+        assertEquals(serialNumber, mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId));
+        assertEquals(serialNumber2, mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId2));
+    }
+
+    @Test
+    public void getUserSerialNumbers_returnsMinusOneIfNoEntry() {
+        int userId = 42;
+        int generationId = 24;
+        Long serialNumber = -1L;
+        // Don't set serial number
+        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId);
+
+        assertEquals(1, mRecoverableKeyStoreDb.getUserSerialNumbers().size());
+        assertEquals(serialNumber, mRecoverableKeyStoreDb.getUserSerialNumbers().get(userId));
+    }
+
+    @Test
+    public void removeUserFromAllTables_removesData() throws Exception {
+        int userId = 12;
+        int generationId = 24;
+        int[] types = new int[]{1};
+        int uid = 10009;
+        mRecoverableKeyStoreDb.setRecoveryServiceCertSerial(userId, uid,
+                TEST_ROOT_CERT_ALIAS, 1234L);
+        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId);
+        mRecoverableKeyStoreDb.setActiveRootOfTrust(userId, uid, "root");
+        mRecoverableKeyStoreDb.setRecoverySecretTypes(userId, uid, types);
+
+        mRecoverableKeyStoreDb.removeUserFromAllTables(userId);
+
+        // RootOfTrust
+        assertThat(mRecoverableKeyStoreDb.getRecoveryServiceCertSerial(userId, uid,
+                TEST_ROOT_CERT_ALIAS)).isNull();
+        // UserMetadata
+        assertThat(mRecoverableKeyStoreDb.getPlatformKeyGenerationId(userId)).isEqualTo(-1);
+        // RecoveryServiceMetadata
+        assertThat(mRecoverableKeyStoreDb.getRecoverySecretTypes(userId, uid)).isEmpty();
+    }
+
+    @Test
     public void setRecoveryStatus_withSingleKey() {
         int userId = 12;
         int uid = 1009;