Loading services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +101 −15 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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, Loading @@ -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."); }); } /** Loading @@ -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 Loading @@ -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 { Loading @@ -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); Loading @@ -140,7 +185,7 @@ public class KeySyncTask implements Runnable { return; } // TODO: send RECOVERABLE_KEYSTORE_SNAPSHOT intent mSnapshotConsumer.accept(salt, encryptedRecoveryKey, encryptedApplicationKeys); } private PublicKey getVaultPublicKey() { Loading @@ -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. Loading Loading @@ -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(); } } services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java +13 −0 Original line number Diff line number Diff line Loading @@ -332,4 +332,17 @@ public class PlatformKeyManager { } return keyStore; } /** * @hide */ public interface Factory { /** * New PlatformKeyManager instance. * * @hide */ PlatformKeyManager newInstance() throws NoSuchAlgorithmException, InsecureUserException, KeyStoreException; } } services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncTaskTest.java +147 −0 Original line number Diff line number Diff line Loading @@ -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() { Loading Loading @@ -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); } Loading Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +101 −15 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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, Loading @@ -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."); }); } /** Loading @@ -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 Loading @@ -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 { Loading @@ -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); Loading @@ -140,7 +185,7 @@ public class KeySyncTask implements Runnable { return; } // TODO: send RECOVERABLE_KEYSTORE_SNAPSHOT intent mSnapshotConsumer.accept(salt, encryptedRecoveryKey, encryptedApplicationKeys); } private PublicKey getVaultPublicKey() { Loading @@ -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. Loading Loading @@ -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(); } }
services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java +13 −0 Original line number Diff line number Diff line Loading @@ -332,4 +332,17 @@ public class PlatformKeyManager { } return keyStore; } /** * @hide */ public interface Factory { /** * New PlatformKeyManager instance. * * @hide */ PlatformKeyManager newInstance() throws NoSuchAlgorithmException, InsecureUserException, KeyStoreException; } }
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncTaskTest.java +147 −0 Original line number Diff line number Diff line Loading @@ -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() { Loading Loading @@ -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); } Loading