Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 89f12d5a authored by Dmitry Dementyev's avatar Dmitry Dementyev
Browse files

Store user's serial number in recoverable key store database.

Wipe data for deleted user on restart from
 * The database
 * Android KeyStore
 * Snapshot cache
Note that userId cannot be reused for different user at runtime and serial number is never reused.

Bug: 126241986
Test: atest FrameworksServicesTests:com.android.server.locksettings.recoverablekeystore
Change-Id: I91cd9e1baccd425a8a234b3da3d583f0920d4804
parent 1512e805
Loading
Loading
Loading
Loading
+21 −5
Original line number Diff line number Diff line
@@ -53,6 +53,7 @@ import com.android.server.locksettings.recoverablekeystore.certificate.CertValid
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 @@ public class RecoverableKeyStoreManager {
    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 @@ public class RecoverableKeyStoreManager {
                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 @@ public class RecoverableKeyStoreManager {
            RecoverySnapshotListenersStorage listenersStorage,
            PlatformKeyManager platformKeyManager,
            ApplicationKeyStorage applicationKeyStorage,
            TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
            TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
            CleanupManager cleanupManager) {
        mContext = context;
        mDatabase = recoverableKeyStoreDb;
        mRecoverySessionStorage = recoverySessionStorage;
@@ -155,8 +166,10 @@ public class RecoverableKeyStoreManager {
        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 @@ public class RecoverableKeyStoreManager {
        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) {
+178 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }
}
+115 −7
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.database.Cursor;
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 @@ public class RecoverableKeyStoreDb {
     *
     * @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 = {
@@ -336,6 +337,58 @@ public class RecoverableKeyStoreDb {
        return result;
    }

    /**
     * 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}.
     */
@@ -424,8 +477,7 @@ public class RecoverableKeyStoreDb {
     */
    @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 class RecoverableKeyStoreDb {
     */
    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 @@ public class RecoverableKeyStoreDb {
     */
    @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 @@ public class RecoverableKeyStoreDb {
        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 @@ public class RecoverableKeyStoreDb {
                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
+7 −0
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@ import android.provider.BaseColumns;

/**
 * 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 @@ class RecoverableKeyStoreDbContract {
         * 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";
    }

    /**
+17 −2
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKe
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 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
            "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 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
            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 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
                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) {
Loading