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

Commit 8cb41716 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add key encryption to KeySyncTask"

parents 36a65372 f0a4bea6
Loading
Loading
Loading
Loading
+101 −15
Original line number Diff line number Diff line
@@ -28,20 +28,24 @@ import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKe
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.util.Map;

import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

/**
 * Task to sync application keys to a remote vault service.
 *
 * TODO: implement fully
 * @hide
 */
public class KeySyncTask implements Runnable {
    private static final String TAG = "KeySyncTask";
@@ -52,11 +56,13 @@ public class KeySyncTask implements Runnable {
    private static final int LENGTH_PREFIX_BYTES = Integer.BYTES;
    private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";

    private final Context mContext;
    private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
    private final int mUserId;
    private final RecoverableSnapshotConsumer mSnapshotConsumer;
    private final int mCredentialType;
    private final String mCredential;
    private final PlatformKeyManager.Factory mPlatformKeyManagerFactory;
    private final VaultKeySupplier mVaultKeySupplier;

    public static KeySyncTask newInstance(
            Context context,
@@ -66,11 +72,17 @@ public class KeySyncTask implements Runnable {
            String credential
    ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
        return new KeySyncTask(
                context.getApplicationContext(),
                recoverableKeyStoreDb,
                userId,
                credentialType,
                credential);
                credential,
                () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId),
                (salt, recoveryKey, applicationKeys) -> {
                    // TODO: implement sending intent
                },
                () -> {
                    throw new UnsupportedOperationException("Not implemented vault key.");
                });
    }

    /**
@@ -80,19 +92,26 @@ public class KeySyncTask implements Runnable {
     * @param userId The uid of the user whose profile has been unlocked.
     * @param credentialType The type of credential - i.e., pattern or password.
     * @param credential The credential, encoded as a {@link String}.
     * @param platformKeyManagerFactory Instantiates a {@link PlatformKeyManager} for the user.
     *     This is a factory to enable unit testing, as otherwise it would be impossible to test
     *     without a screen unlock occurring!
     */
    @VisibleForTesting
    KeySyncTask(
            Context context,
            RecoverableKeyStoreDb recoverableKeyStoreDb,
            int userId,
            int credentialType,
            String credential) {
        mContext = context;
            String credential,
            PlatformKeyManager.Factory platformKeyManagerFactory,
            RecoverableSnapshotConsumer snapshotConsumer,
            VaultKeySupplier vaultKeySupplier) {
        mRecoverableKeyStoreDb = recoverableKeyStoreDb;
        mUserId = userId;
        mCredentialType = credentialType;
        mCredential = credential;
        mPlatformKeyManagerFactory = platformKeyManagerFactory;
        mSnapshotConsumer = snapshotConsumer;
        mVaultKeySupplier = vaultKeySupplier;
    }

    @Override
@@ -105,10 +124,29 @@ public class KeySyncTask implements Runnable {
    }

    private void syncKeys() {
        if (!isSyncPending()) {
            Log.d(TAG, "Key sync not needed.");
            return;
        }

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

        // TODO: decrypt local wrapped application keys, ready for sync
        Map<String, SecretKey> rawKeys;
        try {
            rawKeys = getKeysToSync();
        } catch (GeneralSecurityException e) {
            Log.e(TAG, "Failed to load recoverable keys for sync", e);
            return;
        } catch (InsecureUserException e) {
            Log.wtf(TAG, "A screen unlock triggered the key sync flow, so user must have "
                    + "lock screen. This should be impossible.", e);
            return;
        } catch (BadPlatformKeyException e) {
            Log.wtf(TAG, "Loaded keys for same generation ID as platform key, so "
                    + "BadPlatformKeyException should be impossible.", e);
            return;
        }

        SecretKey recoveryKey;
        try {
@@ -118,17 +156,24 @@ public class KeySyncTask implements Runnable {
            return;
        }

        // TODO: encrypt each application key with recovery key

        PublicKey vaultKey = getVaultPublicKey();
        Map<String, byte[]> encryptedApplicationKeys;
        try {
            encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey(
                    recoveryKey, rawKeys);
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
            Log.wtf(TAG,
                    "Should be impossible: could not encrypt application keys with random key",
                    e);
            return;
        }

        // TODO: construct vault params and vault metadata
        byte[] vaultParams = {};

        byte[] locallyEncryptedRecoveryKey;
        byte[] encryptedRecoveryKey;
        try {
            locallyEncryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
                    vaultKey,
            encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
                    mVaultKeySupplier.get(),
                    localLskfHash,
                    vaultParams,
                    recoveryKey);
@@ -140,7 +185,7 @@ public class KeySyncTask implements Runnable {
            return;
        }

        // TODO: send RECOVERABLE_KEYSTORE_SNAPSHOT intent
        mSnapshotConsumer.accept(salt, encryptedRecoveryKey, encryptedApplicationKeys);
    }

    private PublicKey getVaultPublicKey() {
@@ -148,6 +193,29 @@ public class KeySyncTask implements Runnable {
        throw new UnsupportedOperationException("TODO: get vault public key.");
    }

    /**
     * Returns all of the recoverable keys for the user.
     */
    private Map<String, SecretKey> getKeysToSync()
            throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
            NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
        PlatformKeyManager platformKeyManager = mPlatformKeyManagerFactory.newInstance();
        PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey();
        Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
                mUserId, decryptKey.getGenerationId());
        return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
    }

    /**
     * Returns {@code true} if a sync is pending.
     */
    private boolean isSyncPending() {
        // TODO: implement properly. For now just always syncing if the user has any recoverable
        // keys. We need to keep track of when the store's state actually changes.
        return !mRecoverableKeyStoreDb.getAllKeys(
                mUserId, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(mUserId)).isEmpty();
    }

    /**
     * The UI best suited to entering the given lock screen. This is synced with the vault so the
     * user can be shown the same UI when recovering the vault on another device.
@@ -221,4 +289,22 @@ public class KeySyncTask implements Runnable {
        keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
        return keyGenerator.generateKey();
    }

    /**
     * TODO: just replace with the Intent call. I'm not sure exactly what this looks like, hence
     * this interface, so I can test in the meantime.
     */
    public interface RecoverableSnapshotConsumer {
        void accept(
                byte[] salt,
                byte[] encryptedRecoveryKey,
                Map<String, byte[]> encryptedApplicationKeys);
    }

    /**
     * TODO: until this is in the database, so we can test.
     */
    public interface VaultKeySupplier {
        PublicKey get();
    }
}
+13 −0
Original line number Diff line number Diff line
@@ -332,4 +332,17 @@ public class PlatformKeyManager {
        }
        return keyStore;
    }

    /**
     * @hide
     */
    public interface Factory {
        /**
         * New PlatformKeyManager instance.
         *
         * @hide
         */
        PlatformKeyManager newInstance()
                throws NoSuchAlgorithmException, InsecureUserException, KeyStoreException;
    }
}
+147 −0
Original line number Diff line number Diff line
@@ -27,20 +27,99 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class KeySyncTaskTest {
    private static final String KEY_ALGORITHM = "AES";
    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
    private static final String WRAPPING_KEY_ALIAS = "KeySyncTaskTest/WrappingKey";
    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 String TEST_APP_KEY_ALIAS = "rcleaver";
    private static final int TEST_GENERATION_ID = 2;
    private static final int TEST_CREDENTIAL_TYPE = CREDENTIAL_TYPE_PASSWORD;
    private static final String TEST_CREDENTIAL = "password1234";
    private static final byte[] THM_ENCRYPTED_RECOVERY_KEY_HEADER =
            "V1 THM_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);

    @Mock private KeySyncTask.RecoverableSnapshotConsumer mRecoverableSnapshotConsumer;
    @Mock private PlatformKeyManager mPlatformKeyManager;

    @Captor private ArgumentCaptor<byte[]> mSaltCaptor;
    @Captor private ArgumentCaptor<byte[]> mEncryptedRecoveryKeyCaptor;
    @Captor private ArgumentCaptor<Map<String, byte[]>> mEncryptedApplicationKeysCaptor;

    private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
    private File mDatabaseFile;
    private KeyPair mKeyPair;
    private AndroidKeyStoreSecretKey mWrappingKey;
    private PlatformEncryptionKey mEncryptKey;

    private KeySyncTask mKeySyncTask;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        Context context = InstrumentationRegistry.getTargetContext();
        mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
        mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
        mKeyPair = SecureBox.genKeyPair();

        mKeySyncTask = new KeySyncTask(
                mRecoverableKeyStoreDb,
                TEST_USER_ID,
                TEST_CREDENTIAL_TYPE,
                TEST_CREDENTIAL,
                () -> mPlatformKeyManager,
                mRecoverableSnapshotConsumer,
                () -> mKeyPair.getPublic());

        mWrappingKey = generateAndroidKeyStoreKey();
        mEncryptKey = new PlatformEncryptionKey(TEST_GENERATION_ID, mWrappingKey);
        when(mPlatformKeyManager.getDecryptKey()).thenReturn(
                new PlatformDecryptionKey(TEST_GENERATION_ID, mWrappingKey));
    }

    @After
    public void tearDown() {
        mRecoverableKeyStoreDb.close();
        mDatabaseFile.delete();
    }

    @Test
    public void isPin_isTrueForNumericString() {
@@ -114,6 +193,74 @@ public class KeySyncTaskTest {

    }

    @Test
    public void run_doesNotSendAnythingIfNoKeysToSync() throws Exception {
        // TODO: proper test here, once we have proper implementation for checking that keys need
        // to be synced.
        mKeySyncTask.run();

        verifyZeroInteractions(mRecoverableSnapshotConsumer);
    }

    @Test
    public void run_sendsEncryptedKeysIfAvailableToSync() 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));

        mKeySyncTask.run();

        verify(mRecoverableSnapshotConsumer).accept(
                mSaltCaptor.capture(),
                mEncryptedRecoveryKeyCaptor.capture(),
                mEncryptedApplicationKeysCaptor.capture());
        byte[] lockScreenHash = KeySyncTask.hashCredentials(
                mSaltCaptor.getValue(), TEST_CREDENTIAL);
        // TODO: what should vault params be here?
        byte[] recoveryKey = decryptThmEncryptedKey(
                lockScreenHash,
                mEncryptedRecoveryKeyCaptor.getValue(),
                /*vaultParams=*/ new byte[0]);
        Map<String, byte[]> applicationKeys = mEncryptedApplicationKeysCaptor.getValue();
        assertEquals(1, applicationKeys.size());
        byte[] appKey = KeySyncUtils.decryptApplicationKey(
                recoveryKey, applicationKeys.get(TEST_APP_KEY_ALIAS));
        assertArrayEquals(applicationKey.getEncoded(), appKey);
    }

    private byte[] decryptThmEncryptedKey(
            byte[] lockScreenHash, byte[] encryptedKey, byte[] vaultParams) throws Exception {
        byte[] locallyEncryptedKey = SecureBox.decrypt(
                mKeyPair.getPrivate(),
                /*sharedSecret=*/ KeySyncUtils.calculateThmKfHash(lockScreenHash),
                /*header=*/ KeySyncUtils.concat(THM_ENCRYPTED_RECOVERY_KEY_HEADER, vaultParams),
                encryptedKey
        );
        return KeySyncUtils.decryptRecoveryKey(lockScreenHash, locallyEncryptedKey);
    }

    private SecretKey generateKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
        keyGenerator.init(/*keySize=*/ 256);
        return keyGenerator.generateKey();
    }

    private AndroidKeyStoreSecretKey generateAndroidKeyStoreKey() throws Exception {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
                KEY_ALGORITHM,
                ANDROID_KEY_STORE_PROVIDER);
        keyGenerator.init(new KeyGenParameterSpec.Builder(
                WRAPPING_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build());
        return (AndroidKeyStoreSecretKey) keyGenerator.generateKey();
    }

    private static byte[] utf8Bytes(String s) {
        return s.getBytes(StandardCharsets.UTF_8);
    }