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

Commit 2fd4b597 authored by Robert Berry's avatar Robert Berry
Browse files

Create snapshot even if no PendingIntent is registered

When the user first unlocks the phone after booting, the system
app has not yet started. As such, it will not have had a chance to
register a PendingIntent. But if it has ever previously initialized,
the framework can still create a snapshot, and should. Otherwise, it
may be up to 72 hours before the user unlocks their phone again using
the primary method, which adds delay to the key sync.

Bug: 73921897
Test: runtest frameworks-services -p \
      com.android.server.locksettings.recoverablekeystore

Change-Id: Idfaf53194e6a2f5d5ce0123d72f46197392d2c99
parent 3912a7f5
Loading
Loading
Loading
Loading
+2 −8
Original line number Original line Diff line number Diff line
@@ -172,7 +172,7 @@ public class KeySyncTask implements Runnable {


    private void syncKeysForAgent(int recoveryAgentUid) {
    private void syncKeysForAgent(int recoveryAgentUid) {
        boolean recreateCurrentVersion = false;
        boolean recreateCurrentVersion = false;
        if (!shoudCreateSnapshot(recoveryAgentUid)) {
        if (!shouldCreateSnapshot(recoveryAgentUid)) {
            recreateCurrentVersion =
            recreateCurrentVersion =
                    (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null)
                    (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null)
                    && (mRecoverySnapshotStorage.get(recoveryAgentUid) == null);
                    && (mRecoverySnapshotStorage.get(recoveryAgentUid) == null);
@@ -184,11 +184,6 @@ public class KeySyncTask implements Runnable {
            }
            }
        }
        }


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

        PublicKey publicKey;
        PublicKey publicKey;
        CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId,
        CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId,
                recoveryAgentUid);
                recoveryAgentUid);
@@ -313,7 +308,6 @@ public class KeySyncTask implements Runnable {
            return;
            return;
        }
        }
        mRecoverySnapshotStorage.put(recoveryAgentUid, keyChainSnapshotBuilder.build());
        mRecoverySnapshotStorage.put(recoveryAgentUid, keyChainSnapshotBuilder.build());

        mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
        mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
    }
    }


@@ -354,7 +348,7 @@ public class KeySyncTask implements Runnable {
     * Returns {@code true} if a sync is pending.
     * Returns {@code true} if a sync is pending.
     * @param recoveryAgentUid uid of the recovery agent.
     * @param recoveryAgentUid uid of the recovery agent.
     */
     */
    private boolean shoudCreateSnapshot(int recoveryAgentUid) {
    private boolean shouldCreateSnapshot(int recoveryAgentUid) {
        int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid);
        int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid);
        if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) {
        if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) {
            // Only lockscreen type is supported.
            // Only lockscreen type is supported.
+37 −10
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.locksettings.recoverablekeystore;


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


@@ -36,6 +37,9 @@ public class RecoverySnapshotListenersStorage {
    @GuardedBy("this")
    @GuardedBy("this")
    private SparseArray<PendingIntent> mAgentIntents = new SparseArray<>();
    private SparseArray<PendingIntent> mAgentIntents = new SparseArray<>();


    @GuardedBy("this")
    private ArraySet<Integer> mAgentsWithPendingSnapshots = new ArraySet<>();

    /**
    /**
     * Sets new listener for the recovery agent, identified by {@code uid}.
     * Sets new listener for the recovery agent, identified by {@code uid}.
     *
     *
@@ -46,6 +50,11 @@ public class RecoverySnapshotListenersStorage {
            int recoveryAgentUid, @Nullable PendingIntent intent) {
            int recoveryAgentUid, @Nullable PendingIntent intent) {
        Log.i(TAG, "Registered listener for agent with uid " + recoveryAgentUid);
        Log.i(TAG, "Registered listener for agent with uid " + recoveryAgentUid);
        mAgentIntents.put(recoveryAgentUid, intent);
        mAgentIntents.put(recoveryAgentUid, intent);

        if (mAgentsWithPendingSnapshots.contains(recoveryAgentUid)) {
            Log.i(TAG, "Snapshot already created for agent. Immediately triggering intent.");
            tryToSendIntent(recoveryAgentUid, intent);
        }
    }
    }


    /**
    /**
@@ -56,21 +65,39 @@ public class RecoverySnapshotListenersStorage {
    }
    }


    /**
    /**
     * Notifies recovery agent that new snapshot is available. Does nothing if a listener was not
     * Notifies recovery agent that new snapshot is available. If a recovery agent has not yet
     * registered.
     * registered a {@link PendingIntent}, remembers that a snapshot is pending for it, so that
     * when it does register, that intent is immediately triggered.
     *
     *
     * @param recoveryAgentUid uid of recovery agent.
     * @param recoveryAgentUid uid of recovery agent.
     */
     */
    public synchronized void recoverySnapshotAvailable(int recoveryAgentUid) {
    public synchronized void recoverySnapshotAvailable(int recoveryAgentUid) {
        PendingIntent intent = mAgentIntents.get(recoveryAgentUid);
        PendingIntent intent = mAgentIntents.get(recoveryAgentUid);
        if (intent != null) {
        if (intent == null) {
            Log.i(TAG, "Snapshot available for agent " + recoveryAgentUid
                    + " but agent has not yet initialized. Will notify agent when it does.");
            mAgentsWithPendingSnapshots.add(recoveryAgentUid);
            return;
        }

        tryToSendIntent(recoveryAgentUid, intent);
    }

    /**
     * Attempts to send {@code intent} for the recovery agent. If this fails, remembers to notify
     * the recovery agent immediately if it registers a new intent.
     */
    private synchronized void tryToSendIntent(int recoveryAgentUid, PendingIntent intent) {
        try {
        try {
            intent.send();
            intent.send();
            mAgentsWithPendingSnapshots.remove(recoveryAgentUid);
            Log.d(TAG, "Successfully notified listener.");
        } catch (PendingIntent.CanceledException e) {
        } catch (PendingIntent.CanceledException e) {
            Log.e(TAG,
            Log.e(TAG,
                    "Failed to trigger PendingIntent for " + recoveryAgentUid,
                    "Failed to trigger PendingIntent for " + recoveryAgentUid,
                    e);
                    e);
            }
            // As it failed to trigger, trigger immediately if a new intent is registered later.
            mAgentsWithPendingSnapshots.add(recoveryAgentUid);
        }
        }
    }
    }
}
}
+11 −15
Original line number Original line Diff line number Diff line
@@ -30,6 +30,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.never;
@@ -240,10 +241,8 @@ public class KeySyncTaskTest {
    }
    }


    @Test
    @Test
    public void run_doesNotSendAnythingIfNoRecoveryAgentPendingIntentRegistered() throws Exception {
    public void run_doesNotSendAnythingIfNoDeviceIdIsSet() throws Exception {
        SecretKey applicationKey = generateKey();
        SecretKey applicationKey = generateKey();
        mRecoverableKeyStoreDb.setServerParams(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_VAULT_HANDLE);
        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
        mRecoverableKeyStoreDb.insertKey(
        mRecoverableKeyStoreDb.insertKey(
                TEST_USER_ID,
                TEST_USER_ID,
@@ -252,6 +251,7 @@ public class KeySyncTaskTest {
                WrappedKey.fromSecretKey(mEncryptKey, applicationKey));
                WrappedKey.fromSecretKey(mEncryptKey, applicationKey));
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
        when(mSnapshotListenersStorage.hasListener(TEST_RECOVERY_AGENT_UID)).thenReturn(true);


        mKeySyncTask.run();
        mKeySyncTask.run();


@@ -259,21 +259,18 @@ public class KeySyncTaskTest {
    }
    }


    @Test
    @Test
    public void run_doesNotSendAnythingIfNoDeviceIdIsSet() throws Exception {
    public void run_stillCreatesSnapshotIfNoRecoveryAgentPendingIntentRegistered()
        SecretKey applicationKey = generateKey();
            throws Exception {
        mRecoverableKeyStoreDb.setServerParams(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_VAULT_HANDLE);
        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
        mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
        mRecoverableKeyStoreDb.insertKey(
        addApplicationKey(TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_APP_KEY_ALIAS);
                TEST_USER_ID,
                TEST_RECOVERY_AGENT_UID,
                TEST_APP_KEY_ALIAS,
                WrappedKey.fromSecretKey(mEncryptKey, applicationKey));
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
        when(mSnapshotListenersStorage.hasListener(TEST_RECOVERY_AGENT_UID)).thenReturn(true);


        mKeySyncTask.run();
        mKeySyncTask.run();


        assertNull(mRecoverySnapshotStorage.get(TEST_RECOVERY_AGENT_UID));
        assertNotNull(mRecoverySnapshotStorage.get(TEST_RECOVERY_AGENT_UID));
    }
    }


    @Test
    @Test
@@ -502,7 +499,7 @@ public class KeySyncTaskTest {
    }
    }


    @Test
    @Test
    public void run_doesNotSendKeyToNonregisteredAgent() throws Exception {
    public void run_notifiesNonregisteredAgent() throws Exception {
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
                TEST_USER_ID, TEST_RECOVERY_AGENT_UID, mKeyPair.getPublic());
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
        mRecoverableKeyStoreDb.setRecoveryServicePublicKey(
@@ -514,8 +511,7 @@ public class KeySyncTaskTest {
        mKeySyncTask.run();
        mKeySyncTask.run();


        verify(mSnapshotListenersStorage).recoverySnapshotAvailable(TEST_RECOVERY_AGENT_UID);
        verify(mSnapshotListenersStorage).recoverySnapshotAvailable(TEST_RECOVERY_AGENT_UID);
        verify(mSnapshotListenersStorage, never()).
        verify(mSnapshotListenersStorage).recoverySnapshotAvailable(TEST_RECOVERY_AGENT_UID2);
                recoverySnapshotAvailable(TEST_RECOVERY_AGENT_UID2);
    }
    }


    @Test
    @Test
+57 −1
Original line number Original line Diff line number Diff line
@@ -4,7 +4,10 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertTrue;


import android.app.PendingIntent;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.test.InstrumentationRegistry;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.runner.AndroidJUnit4;
@@ -12,10 +15,18 @@ import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.RunWith;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@SmallTest
@SmallTest
@RunWith(AndroidJUnit4.class)
@RunWith(AndroidJUnit4.class)
public class RecoverySnapshotListenersStorageTest {
public class RecoverySnapshotListenersStorageTest {


    private static final String TEST_INTENT_ACTION =
            "com.android.server.locksettings.recoverablekeystore.RECOVERY_SNAPSHOT_TEST_ACTION";

    private static final int TEST_TIMEOUT_SECONDS = 1;

    private final RecoverySnapshotListenersStorage mStorage =
    private final RecoverySnapshotListenersStorage mStorage =
            new RecoverySnapshotListenersStorage();
            new RecoverySnapshotListenersStorage();


@@ -34,4 +45,49 @@ public class RecoverySnapshotListenersStorageTest {


        assertTrue(mStorage.hasListener(recoveryAgentUid));
        assertTrue(mStorage.hasListener(recoveryAgentUid));
    }
    }

    @Test
    public void setSnapshotListener_invokesIntentImmediatelyIfPreviouslyNotified()
            throws Exception {
        Context context = InstrumentationRegistry.getTargetContext();
        int recoveryAgentUid = 1000;
        mStorage.recoverySnapshotAvailable(recoveryAgentUid);
        PendingIntent intent = PendingIntent.getBroadcast(
                context, /*requestCode=*/ 0, new Intent(TEST_INTENT_ACTION), /*flags=*/0);
        CountDownLatch latch = new CountDownLatch(1);
        context.registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                context.unregisterReceiver(this);
                latch.countDown();
            }
        }, new IntentFilter(TEST_INTENT_ACTION));

        mStorage.setSnapshotListener(recoveryAgentUid, intent);

        assertTrue(latch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
    }

    @Test
    public void setSnapshotListener_doesNotRepeatedlyInvokeListener() throws Exception {
        Context context = InstrumentationRegistry.getTargetContext();
        int recoveryAgentUid = 1000;
        mStorage.recoverySnapshotAvailable(recoveryAgentUid);
        PendingIntent intent = PendingIntent.getBroadcast(
                context, /*requestCode=*/ 0, new Intent(TEST_INTENT_ACTION), /*flags=*/0);
        CountDownLatch latch = new CountDownLatch(2);
        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                latch.countDown();
            }
        };
        context.registerReceiver(broadcastReceiver, new IntentFilter(TEST_INTENT_ACTION));

        mStorage.setSnapshotListener(recoveryAgentUid, intent);
        mStorage.setSnapshotListener(recoveryAgentUid, intent);

        assertFalse(latch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS));
        context.unregisterReceiver(broadcastReceiver);
    }
}
}