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

Commit 91044044 authored by Robert Berry's avatar Robert Berry
Browse files

Trigger recovery agent PendingIntent in KeySyncTask

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: Iafae3ff7da9bdc9e986cd19cde72170f9f438180
parent 23ee7703
Loading
Loading
Loading
Loading
+18 −1
Original line number Diff line number Diff line
@@ -71,11 +71,13 @@ public class KeySyncTask implements Runnable {
    private final PlatformKeyManager.Factory mPlatformKeyManagerFactory;
    private final VaultKeySupplier mVaultKeySupplier;
    private final RecoverySnapshotStorage mRecoverySnapshotStorage;
    private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;

    public static KeySyncTask newInstance(
            Context context,
            RecoverableKeyStoreDb recoverableKeyStoreDb,
            RecoverySnapshotStorage snapshotStorage,
            RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
            int userId,
            int credentialType,
            String credential
@@ -83,6 +85,7 @@ public class KeySyncTask implements Runnable {
        return new KeySyncTask(
                recoverableKeyStoreDb,
                snapshotStorage,
                recoverySnapshotListenersStorage,
                userId,
                credentialType,
                credential,
@@ -107,11 +110,13 @@ public class KeySyncTask implements Runnable {
    KeySyncTask(
            RecoverableKeyStoreDb recoverableKeyStoreDb,
            RecoverySnapshotStorage snapshotStorage,
            RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
            int userId,
            int credentialType,
            String credential,
            PlatformKeyManager.Factory platformKeyManagerFactory,
            VaultKeySupplier vaultKeySupplier) {
        mSnapshotListenersStorage = recoverySnapshotListenersStorage;
        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
        mUserId = userId;
        mCredentialType = credentialType;
@@ -136,6 +141,18 @@ public class KeySyncTask implements Runnable {
            return;
        }

        int recoveryAgentUid = mRecoverableKeyStoreDb.getRecoveryAgentUid(mUserId);

        if (recoveryAgentUid == -1) {
            Log.w(TAG, "No recovery agent initialized for user " + mUserId);
            return;
        }

        if (!mSnapshotListenersStorage.hasListener(recoveryAgentUid)) {
            Log.w(TAG, "No pending intent registered for recovery agent " + recoveryAgentUid);
            return;
        }

        byte[] salt = generateSalt();
        byte[] localLskfHash = hashCredentials(salt, mCredential);

@@ -192,7 +209,6 @@ public class KeySyncTask implements Runnable {
            return;
        }

        // TODO: why is the secret sent here? I thought it wasn't sent in the raw at all.
        KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata(
                /*userSecretType=*/ TYPE_LOCKSCREEN,
                /*lockScreenUiFormat=*/ mCredentialType,
@@ -207,6 +223,7 @@ public class KeySyncTask implements Runnable {
                /*recoveryMetadata=*/ metadataList,
                /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys),
                /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey));
        mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
    }

    private PublicKey getVaultPublicKey() {
+13 −8
Original line number Diff line number Diff line
@@ -45,7 +45,6 @@ import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -74,7 +73,7 @@ public class RecoverableKeyStoreManager {
    private final RecoverableKeyStoreDb mDatabase;
    private final RecoverySessionStorage mRecoverySessionStorage;
    private final ExecutorService mExecutorService;
    private final ListenersStorage mListenersStorage;
    private final RecoverySnapshotListenersStorage mListenersStorage;
    private final RecoverableKeyGenerator mRecoverableKeyGenerator;
    private final RecoverySnapshotStorage mSnapshotStorage;

@@ -91,8 +90,8 @@ public class RecoverableKeyStoreManager {
                    db,
                    new RecoverySessionStorage(),
                    Executors.newSingleThreadExecutor(),
                    ListenersStorage.getInstance(),
                    new RecoverySnapshotStorage());
                    new RecoverySnapshotStorage(),
                    new RecoverySnapshotListenersStorage());
        }
        return mInstance;
    }
@@ -103,8 +102,8 @@ public class RecoverableKeyStoreManager {
            RecoverableKeyStoreDb recoverableKeyStoreDb,
            RecoverySessionStorage recoverySessionStorage,
            ExecutorService executorService,
            ListenersStorage listenersStorage,
            RecoverySnapshotStorage snapshotStorage) {
            RecoverySnapshotStorage snapshotStorage,
            RecoverySnapshotListenersStorage listenersStorage) {
        mContext = context;
        mDatabase = recoverableKeyStoreDb;
        mRecoverySessionStorage = recoverySessionStorage;
@@ -462,7 +461,7 @@ public class RecoverableKeyStoreManager {
    /**
     * This function can only be used inside LockSettingsService.
     *
     * @param storedHashType from {@Code CredentialHash}
     * @param storedHashType from {@code CredentialHash}
     * @param credential - unencrypted String. Password length should be at most 16 symbols {@code
     *     mPasswordMaxLength}
     * @param userId for user who just unlocked the device.
@@ -473,7 +472,13 @@ public class RecoverableKeyStoreManager {
        // So as not to block the critical path unlocking the phone, defer to another thread.
        try {
            mExecutorService.execute(KeySyncTask.newInstance(
                    mContext, mDatabase, mSnapshotStorage, userId, storedHashType, credential));
                    mContext,
                    mDatabase,
                    mSnapshotStorage,
                    mListenersStorage,
                    userId,
                    storedHashType,
                    credential));
        } catch (NoSuchAlgorithmException e) {
            Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
        } catch (KeyStoreException e) {
+76 −0
Original line number Diff line number Diff line
@@ -18,50 +18,58 @@ package com.android.server.locksettings.recoverablekeystore;

import android.annotation.Nullable;
import android.app.PendingIntent;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;

import java.util.Map;
import java.util.HashMap;
import com.android.internal.annotations.GuardedBy;

/**
 * In memory storage for listeners to be notified when new recovery snapshot is available.
 * Note: implementation is not thread safe and it is used to mock final {@link PendingIntent}
 * class.
 * In memory storage for listeners to be notified when new recovery snapshot is available. This
 * class is thread-safe. It is used on two threads - the service thread and the thread that runs the
 * {@link KeySyncTask}.
 *
 * @hide
 */
public class ListenersStorage {
    private Map<Integer, PendingIntent> mAgentIntents = new HashMap<>();
public class RecoverySnapshotListenersStorage {
    private static final String TAG = "RecoverySnapshotLstnrs";

    private static final ListenersStorage mInstance = new ListenersStorage();
    public static ListenersStorage getInstance() {
        return mInstance;
    }
    @GuardedBy("this")
    private SparseArray<PendingIntent> mAgentIntents = new SparseArray<>();

    /**
     * Sets new listener for the recovery agent, identified by {@code uid}
     * Sets new listener for the recovery agent, identified by {@code uid}.
     *
     * @param recoveryAgentUid uid
     * @param intent PendingIntent which will be triggered than new snapshot is available.
     * @param recoveryAgentUid uid of the recovery agent.
     * @param intent PendingIntent which will be triggered when new snapshot is available.
     */
    public void setSnapshotListener(int recoveryAgentUid, @Nullable PendingIntent intent) {
    public synchronized void setSnapshotListener(
            int recoveryAgentUid, @Nullable PendingIntent intent) {
        Log.i(TAG, "Registered listener for agent with uid " + recoveryAgentUid);
        mAgentIntents.put(recoveryAgentUid, intent);
    }

    /**
     * Notifies recovery agent, that new snapshot is available.
     * Does nothing if a listener was not registered.
     * Returns {@code true} if a listener has been set for the recovery agent.
     */
    public synchronized boolean hasListener(int recoveryAgentUid) {
        return mAgentIntents.get(recoveryAgentUid) != null;
    }

    /**
     * Notifies recovery agent that new snapshot is available. Does nothing if a listener was not
     * registered.
     *
     * @param recoveryAgentUid uid.
     * @param recoveryAgentUid uid of recovery agent.
     */
    public void recoverySnapshotAvailable(int recoveryAgentUid) {
    public synchronized void recoverySnapshotAvailable(int recoveryAgentUid) {
        PendingIntent intent = mAgentIntents.get(recoveryAgentUid);
        if (intent != null) {
            try {
                intent.send();
            } catch (PendingIntent.CanceledException e) {
                // Ignore - sending intent is not allowed.
                Log.e(TAG,
                        "Failed to trigger PendingIntent for " + recoveryAgentUid,
                        e);
            }
        }
    }
+30 −0
Original line number Diff line number Diff line
@@ -329,6 +329,36 @@ public class RecoverableKeyStoreDb {
                RecoveryServiceMetadataEntry.TABLE_NAME, values, selection, selectionArguments);
    }

    /**
     * Returns the uid of the recovery agent for the given user, or -1 if none is set.
     */
    public int getRecoveryAgentUid(int userId) {
        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();

        String[] projection = { RecoveryServiceMetadataEntry.COLUMN_NAME_UID };
        String selection = RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ?";
        String[] selectionArguments = { Integer.toString(userId) };

        try (
            Cursor cursor = db.query(
                    RecoveryServiceMetadataEntry.TABLE_NAME,
                    projection,
                    selection,
                    selectionArguments,
                    /*groupBy=*/ null,
                    /*having=*/ null,
                    /*orderBy=*/ null)
        ) {
            int count = cursor.getCount();
            if (count == 0) {
                return -1;
            }
            cursor.moveToFirst();
            return cursor.getInt(
                    cursor.getColumnIndexOrThrow(RecoveryServiceMetadataEntry.COLUMN_NAME_UID));
        }
    }

    /**
     * Returns the public key of the recovery service.
     *
+41 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Context;
@@ -70,6 +71,7 @@ public class KeySyncTaskTest {
    private static final String DATABASE_FILE_NAME = "recoverablekeystore.db";
    private static final int TEST_USER_ID = 1000;
    private static final int TEST_APP_UID = 10009;
    private static final int TEST_RECOVERY_AGENT_UID = 90873;
    private static final String TEST_APP_KEY_ALIAS = "rcleaver";
    private static final int TEST_GENERATION_ID = 2;
    private static final int TEST_CREDENTIAL_TYPE = CREDENTIAL_TYPE_PASSWORD;
@@ -78,6 +80,7 @@ public class KeySyncTaskTest {
            "V1 THM_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);

    @Mock private PlatformKeyManager mPlatformKeyManager;
    @Mock private RecoverySnapshotListenersStorage mSnapshotListenersStorage;

    private RecoverySnapshotStorage mRecoverySnapshotStorage;
    private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
@@ -102,6 +105,7 @@ public class KeySyncTaskTest {
        mKeySyncTask = new KeySyncTask(
                mRecoverableKeyStoreDb,
                mRecoverySnapshotStorage,
                mSnapshotListenersStorage,
                TEST_USER_ID,
                TEST_CREDENTIAL_TYPE,
                TEST_CREDENTIAL,
@@ -201,6 +205,39 @@ public class KeySyncTaskTest {
        assertNull(mRecoverySnapshotStorage.get(TEST_USER_ID));
    }

    @Test
    public void run_doesNotSendAnythingIfNoRecoveryAgentSet() throws Exception {
        SecretKey applicationKey = generateKey();
        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
        mRecoverableKeyStoreDb.insertKey(
                TEST_USER_ID,
                TEST_APP_UID,
                TEST_APP_KEY_ALIAS,
                WrappedKey.fromSecretKey(mEncryptKey, applicationKey));
        when(mSnapshotListenersStorage.hasListener(TEST_RECOVERY_AGENT_UID)).thenReturn(true);

        mKeySyncTask.run();

        assertNull(mRecoverySnapshotStorage.get(TEST_USER_ID));
    }

    @Test
    public void run_doesNotSendAnythingIfNoRecoveryAgentPendingIntentRegistered() throws Exception {
        SecretKey applicationKey = generateKey();
        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
        mRecoverableKeyStoreDb.insertKey(
                TEST_USER_ID,
                TEST_APP_UID,
                TEST_APP_KEY_ALIAS,
                WrappedKey.fromSecretKey(mEncryptKey, applicationKey));
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());

        mKeySyncTask.run();

        assertNull(mRecoverySnapshotStorage.get(TEST_USER_ID));
    }

    @Test
    public void run_sendsEncryptedKeysIfAvailableToSync() throws Exception {
        SecretKey applicationKey = generateKey();
@@ -210,6 +247,9 @@ public class KeySyncTaskTest {
                TEST_APP_UID,
                TEST_APP_KEY_ALIAS,
                WrappedKey.fromSecretKey(mEncryptKey, applicationKey));
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
        when(mSnapshotListenersStorage.hasListener(TEST_RECOVERY_AGENT_UID)).thenReturn(true);

        mKeySyncTask.run();

@@ -218,6 +258,7 @@ public class KeySyncTaskTest {
                recoveryData.getRecoveryMetadata().get(0).getKeyDerivationParameters();
        assertEquals(KeyDerivationParameters.ALGORITHM_SHA256,
                keyDerivationParameters.getAlgorithm());
        verify(mSnapshotListenersStorage).recoverySnapshotAvailable(TEST_RECOVERY_AGENT_UID);
        byte[] lockScreenHash = KeySyncTask.hashCredentials(
                keyDerivationParameters.getSalt(),
                TEST_CREDENTIAL);
Loading