diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java new file mode 100644 index 0000000000000..5155a99ee04fa --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 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; + +/** + * Error thrown initializing {@link PlatformKeyManager} if the user is not secure (i.e., has no + * lock screen set). + */ +public class InsecureUserException extends Exception { + + /** + * A new instance with {@code message} error message. + */ + public InsecureUserException(String message) { + super(message); + } +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java new file mode 100644 index 0000000000000..7c9b39597ab2e --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2017 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; + +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +/** + * Proxies {@link java.security.KeyStore}. As all of its methods are final, it cannot otherwise be + * mocked for tests. + * + * @hide + */ +public interface KeyStoreProxy { + + /** @see KeyStore#containsAlias(String) */ + boolean containsAlias(String alias) throws KeyStoreException; + + /** @see KeyStore#getKey(String, char[]) */ + Key getKey(String alias, char[] password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException; + + /** @see KeyStore#setEntry(String, KeyStore.Entry, KeyStore.ProtectionParameter) */ + void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam) + throws KeyStoreException; +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java new file mode 100644 index 0000000000000..ceee3815845e4 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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; + +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +/** + * Implementation of {@link KeyStoreProxy} that delegates all method calls to the {@link KeyStore}. + */ +public class KeyStoreProxyImpl implements KeyStoreProxy { + + private final KeyStore mKeyStore; + + /** + * A new instance, delegating to {@code keyStore}. + */ + public KeyStoreProxyImpl(KeyStore keyStore) { + mKeyStore = keyStore; + } + + @Override + public boolean containsAlias(String alias) throws KeyStoreException { + return mKeyStore.containsAlias(alias); + } + + @Override + public Key getKey(String alias, char[] password) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + return mKeyStore.getKey(alias, password); + } + + @Override + public void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam) + throws KeyStoreException { + mKeyStore.setEntry(alias, entry, protParam); + } +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java new file mode 100644 index 0000000000000..074c596ec7274 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2017 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; + +import android.app.KeyguardManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.security.keystore.AndroidKeyStoreSecretKey; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.Locale; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; + +/** + * Manages creating and checking the validity of the platform key. + * + *

The platform key is used to wrap the material of recoverable keys before persisting them to + * disk. It is also used to decrypt the same keys on a screen unlock, before re-wrapping them with + * a recovery key and syncing them with remote storage. + * + *

Each platform key has two entries in AndroidKeyStore: + * + *

+ * + *

Both entries are enabled only for AES/GCM/NoPadding Cipher algorithm. + * + * @hide + */ +public class PlatformKeyManager { + private static final String TAG = "PlatformKeyManager"; + + private static final String KEY_ALGORITHM = "AES"; + private static final int KEY_SIZE_BITS = 256; + private static final String SHARED_PREFS_KEY_GENERATION_ID = "generationId"; + private static final String SHARED_PREFS_PATH = "/system/recoverablekeystore/platform_keys.xml"; + private static final String KEY_ALIAS_PREFIX = + "com.android.server.locksettings.recoverablekeystore/platform/"; + private static final String ENCRYPT_KEY_ALIAS_SUFFIX = "encrypt"; + private static final String DECRYPT_KEY_ALIAS_SUFFIX = "decrypt"; + private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15; + + private final Context mContext; + private final KeyStoreProxy mKeyStore; + private final SharedPreferences mSharedPreferences; + private final int mUserId; + + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; + + /** + * A new instance operating on behalf of {@code userId}, storing its prefs in the location + * defined by {@code context}. + * + * @param context This should be the context of the RecoverableKeyStoreLoader service. + * @param userId The ID of the user to whose lock screen the platform key must be bound. + * @throws KeyStoreException if failed to initialize AndroidKeyStore. + * @throws NoSuchAlgorithmException if AES is unavailable - should never happen. + * @throws InsecureUserException if the user does not have a lock screen set. + * @throws SecurityException if the caller does not have permission to write to /data/system. + * + * @hide + */ + public static PlatformKeyManager getInstance(Context context, int userId) + throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException { + context = context.getApplicationContext(); + File sharedPreferencesFile = new File( + Environment.getDataDirectory().getAbsoluteFile(), SHARED_PREFS_PATH); + sharedPreferencesFile.mkdirs(); + PlatformKeyManager keyManager = new PlatformKeyManager( + userId, + context, + new KeyStoreProxyImpl(getAndLoadAndroidKeyStore()), + context.getSharedPreferences(sharedPreferencesFile, Context.MODE_PRIVATE)); + keyManager.init(); + return keyManager; + } + + @VisibleForTesting + PlatformKeyManager( + int userId, + Context context, + KeyStoreProxy keyStore, + SharedPreferences sharedPreferences) { + mUserId = userId; + mKeyStore = keyStore; + mContext = context; + mSharedPreferences = sharedPreferences; + } + + /** + * Returns the current generation ID of the platform key. This increments whenever a platform + * key has to be replaced. (e.g., because the user has removed and then re-added their lock + * screen). + * + * @hide + */ + public int getGenerationId() { + return mSharedPreferences.getInt(getGenerationIdKey(), 1); + } + + /** + * Returns {@code true} if the platform key is available. A platform key won't be available if + * the user has not set up a lock screen. + * + * @hide + */ + public boolean isAvailable() { + return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mUserId); + } + + /** + * Generates a new key and increments the generation ID. Should be invoked if the platform key + * is corrupted and needs to be rotated. + * + * @throws NoSuchAlgorithmException if AES is unavailable - should never happen. + * @throws KeyStoreException if there is an error in AndroidKeyStore. + * + * @hide + */ + public void regenerate() throws NoSuchAlgorithmException, KeyStoreException { + int generationId = getGenerationId(); + generateAndLoadKey(generationId + 1); + setGenerationId(generationId + 1); + } + + /** + * Returns the platform key used for encryption. + * + * @throws KeyStoreException if there was an AndroidKeyStore error. + * @throws UnrecoverableKeyException if the key could not be recovered. + * @throws NoSuchAlgorithmException if AES is unavailable - should never occur. + * + * @hide + */ + public PlatformEncryptionKey getEncryptKey() + throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { + int generationId = getGenerationId(); + AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey( + getEncryptAlias(generationId), /*password=*/ null); + return new PlatformEncryptionKey(generationId, key); + } + + /** + * Returns the platform key used for decryption. Only works after a recent screen unlock. + * + * @throws KeyStoreException if there was an AndroidKeyStore error. + * @throws UnrecoverableKeyException if the key could not be recovered. + * @throws NoSuchAlgorithmException if AES is unavailable - should never occur. + * + * @hide + */ + public PlatformDecryptionKey getDecryptKey() + throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { + int generationId = getGenerationId(); + AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey( + getDecryptAlias(generationId), /*password=*/ null); + return new PlatformDecryptionKey(generationId, key); + } + + /** + * Initializes the class. If there is no current platform key, and the user has a lock screen + * set, will create the platform key and set the generation ID. + * + * @throws KeyStoreException if there was an error in AndroidKeyStore. + * @throws NoSuchAlgorithmException if AES is unavailable - should never happen. + * + * @hide + */ + public void init() throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException { + if (!isAvailable()) { + throw new InsecureUserException(String.format( + Locale.US, "%d does not have a lock screen set.", mUserId)); + } + + int generationId = getGenerationId(); + if (isKeyLoaded(generationId)) { + Log.i(TAG, String.format( + Locale.US, "Platform key generation %d exists already.", generationId)); + return; + } + if (generationId == 1) { + Log.i(TAG, "Generating initial platform ID."); + } else { + Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no " + + "entry was present in AndroidKeyStore. Generating fresh key.", generationId)); + } + + generateAndLoadKey(generationId); + } + + /** + * Returns the alias of the encryption key with the specific {@code generationId} in the + * AndroidKeyStore. + * + *

These IDs look as follows: + * {@code com.security.recoverablekeystore/platform///encrypt} + * + * @param generationId The generation ID. + * @return The alias. + */ + private String getEncryptAlias(int generationId) { + return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX; + } + + /** + * Returns the alias of the decryption key with the specific {@code generationId} in the + * AndroidKeyStore. + * + *

These IDs look as follows: + * {@code com.security.recoverablekeystore/platform///decrypt} + * + * @param generationId The generation ID. + * @return The alias. + */ + private String getDecryptAlias(int generationId) { + return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX; + } + + /** + * Sets the current generation ID to {@code generationId}. + */ + private void setGenerationId(int generationId) { + mSharedPreferences.edit().putInt(getGenerationIdKey(), generationId).commit(); + } + + /** + * Returns the current user's generation ID key in the shared preferences. + */ + private String getGenerationIdKey() { + return SHARED_PREFS_KEY_GENERATION_ID + "/" + mUserId; + } + + /** + * Returns {@code true} if a key has been loaded with the given {@code generationId} into + * AndroidKeyStore. + * + * @throws KeyStoreException if there was an error checking AndroidKeyStore. + */ + private boolean isKeyLoaded(int generationId) throws KeyStoreException { + return mKeyStore.containsAlias(getEncryptAlias(generationId)) + && mKeyStore.containsAlias(getDecryptAlias(generationId)); + } + + /** + * Generates a new 256-bit AES key, and loads it into AndroidKeyStore with the given + * {@code generationId} determining its aliases. + * + * @throws NoSuchAlgorithmException if AES is unavailable. This should never happen, as it is + * available since API version 1. + * @throws KeyStoreException if there was an issue loading the keys into AndroidKeyStore. + */ + private void generateAndLoadKey(int generationId) + throws NoSuchAlgorithmException, KeyStoreException { + String encryptAlias = getEncryptAlias(generationId); + String decryptAlias = getDecryptAlias(generationId); + SecretKey secretKey = generateAesKey(); + + mKeyStore.setEntry( + encryptAlias, + new KeyStore.SecretKeyEntry(secretKey), + new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()); + mKeyStore.setEntry( + decryptAlias, + new KeyStore.SecretKeyEntry(secretKey), + new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds( + USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setBoundToSpecificSecureUserId(mUserId) + .build()); + + try { + secretKey.destroy(); + } catch (DestroyFailedException e) { + Log.w(TAG, "Failed to destroy in-memory platform key.", e); + } + } + + /** + * Generates a new 256-bit AES key, in software. + * + * @return The software-generated AES key. + * @throws NoSuchAlgorithmException if AES key generation is not available. This should never + * happen, as AES has been supported since API level 1. + */ + private static SecretKey generateAesKey() throws NoSuchAlgorithmException { + KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM); + keyGenerator.init(KEY_SIZE_BITS); + return keyGenerator.generateKey(); + } + + /** + * Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked + * {@link KeyStore#load(KeyStore.LoadStoreParameter)}. + * + * @throws KeyStoreException if there was a problem getting or initializing the key store. + */ + private static KeyStore getAndLoadAndroidKeyStore() throws KeyStoreException { + KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + try { + keyStore.load(/*param=*/ null); + } catch (CertificateException | IOException | NoSuchAlgorithmException e) { + // Should never happen. + throw new KeyStoreException("Unable to load keystore.", e); + } + return keyStore; + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java new file mode 100644 index 0000000000000..a9977705aad68 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2017 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; + +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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.KeyguardManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.security.keystore.KeyProperties; +import android.security.keystore.KeyProtection; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +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.security.KeyStore; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class PlatformKeyManagerTest { + + private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15; + private static final int USER_ID_FIXTURE = 42; + private static final String TEST_SHARED_PREFS_NAME = "PlatformKeyManagerTestPrefs"; + + @Mock private Context mContext; + @Mock private KeyStoreProxy mKeyStoreProxy; + @Mock private KeyguardManager mKeyguardManager; + + @Captor private ArgumentCaptor mProtectionParameterCaptor; + @Captor private ArgumentCaptor mEntryArgumentCaptor; + + private SharedPreferences mSharedPreferences; + private PlatformKeyManager mPlatformKeyManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Context testContext = InstrumentationRegistry.getTargetContext(); + mSharedPreferences = testContext.getSharedPreferences( + TEST_SHARED_PREFS_NAME, Context.MODE_PRIVATE); + mPlatformKeyManager = new PlatformKeyManager( + USER_ID_FIXTURE, mContext, mKeyStoreProxy, mSharedPreferences); + + when(mContext.getSystemService(anyString())).thenReturn(mKeyguardManager); + when(mContext.getSystemServiceName(any())).thenReturn("test"); + when(mKeyguardManager.isDeviceSecure(USER_ID_FIXTURE)).thenReturn(true); + } + + @After + public void tearDown() { + mSharedPreferences.edit().clear().commit(); + } + + @Test + public void init_createsEncryptKeyWithCorrectAlias() throws Exception { + mPlatformKeyManager.init(); + + verify(mKeyStoreProxy).setEntry( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/encrypt"), + any(), + any()); + } + + @Test + public void init_createsEncryptKeyWithCorrectPurposes() throws Exception { + mPlatformKeyManager.init(); + + assertEquals(KeyProperties.PURPOSE_ENCRYPT, getEncryptKeyProtection().getPurposes()); + } + + @Test + public void init_createsEncryptKeyWithCorrectPaddings() throws Exception { + mPlatformKeyManager.init(); + + assertArrayEquals( + new String[] { KeyProperties.ENCRYPTION_PADDING_NONE }, + getEncryptKeyProtection().getEncryptionPaddings()); + } + + @Test + public void init_createsEncryptKeyWithCorrectBlockModes() throws Exception { + mPlatformKeyManager.init(); + + assertArrayEquals( + new String[] { KeyProperties.BLOCK_MODE_GCM }, + getEncryptKeyProtection().getBlockModes()); + } + + @Test + public void init_createsEncryptKeyWithoutAuthenticationRequired() throws Exception { + mPlatformKeyManager.init(); + + assertFalse(getEncryptKeyProtection().isUserAuthenticationRequired()); + } + + @Test + public void init_createsDecryptKeyWithCorrectAlias() throws Exception { + mPlatformKeyManager.init(); + + verify(mKeyStoreProxy).setEntry( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/decrypt"), + any(), + any()); + } + + @Test + public void init_createsDecryptKeyWithCorrectPurposes() throws Exception { + mPlatformKeyManager.init(); + + assertEquals(KeyProperties.PURPOSE_DECRYPT, getDecryptKeyProtection().getPurposes()); + } + + @Test + public void init_createsDecryptKeyWithCorrectPaddings() throws Exception { + mPlatformKeyManager.init(); + + assertArrayEquals( + new String[] { KeyProperties.ENCRYPTION_PADDING_NONE }, + getDecryptKeyProtection().getEncryptionPaddings()); + } + + @Test + public void init_createsDecryptKeyWithCorrectBlockModes() throws Exception { + mPlatformKeyManager.init(); + + assertArrayEquals( + new String[] { KeyProperties.BLOCK_MODE_GCM }, + getDecryptKeyProtection().getBlockModes()); + } + + @Test + public void init_createsDecryptKeyWithAuthenticationRequired() throws Exception { + mPlatformKeyManager.init(); + + assertTrue(getDecryptKeyProtection().isUserAuthenticationRequired()); + } + + @Test + public void init_createsDecryptKeyWithAuthenticationValidFor15Seconds() throws Exception { + mPlatformKeyManager.init(); + + assertEquals( + USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS, + getDecryptKeyProtection().getUserAuthenticationValidityDurationSeconds()); + } + + @Test + public void init_createsDecryptKeyBoundToTheUsersAuthentication() throws Exception { + mPlatformKeyManager.init(); + + assertEquals( + USER_ID_FIXTURE, + getDecryptKeyProtection().getBoundToSpecificSecureUserId()); + } + + @Test + public void init_createsBothKeysWithSameMaterial() throws Exception { + mPlatformKeyManager.init(); + + verify(mKeyStoreProxy, times(2)).setEntry(any(), mEntryArgumentCaptor.capture(), any()); + List entries = mEntryArgumentCaptor.getAllValues(); + assertArrayEquals( + ((KeyStore.SecretKeyEntry) entries.get(0)).getSecretKey().getEncoded(), + ((KeyStore.SecretKeyEntry) entries.get(1)).getSecretKey().getEncoded()); + } + + @Test + public void init_setsGenerationIdTo1() throws Exception { + mPlatformKeyManager.init(); + + assertEquals(1, mPlatformKeyManager.getGenerationId()); + } + + @Test + public void getDecryptKey_getsDecryptKeyWithCorrectAlias() throws Exception { + mPlatformKeyManager.getDecryptKey(); + + verify(mKeyStoreProxy).getKey( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/decrypt"), + any()); + } + + @Test + public void getEncryptKey_getsDecryptKeyWithCorrectAlias() throws Exception { + mPlatformKeyManager.getEncryptKey(); + + verify(mKeyStoreProxy).getKey( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/encrypt"), + any()); + } + + @Test + public void regenerate_incrementsTheGenerationId() throws Exception { + mPlatformKeyManager.init(); + + mPlatformKeyManager.regenerate(); + + assertEquals(2, mPlatformKeyManager.getGenerationId()); + } + + @Test + public void regenerate_generatesANewEncryptKeyWithTheCorrectAlias() throws Exception { + mPlatformKeyManager.init(); + + mPlatformKeyManager.regenerate(); + + verify(mKeyStoreProxy).setEntry( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/2/encrypt"), + any(), + any()); + } + + @Test + public void regenerate_generatesANewDecryptKeyWithTheCorrectAlias() throws Exception { + mPlatformKeyManager.init(); + + mPlatformKeyManager.regenerate(); + + verify(mKeyStoreProxy).setEntry( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/2/decrypt"), + any(), + any()); + } + + private KeyProtection getEncryptKeyProtection() throws Exception { + verify(mKeyStoreProxy).setEntry( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/encrypt"), + any(), + mProtectionParameterCaptor.capture()); + return (KeyProtection) mProtectionParameterCaptor.getValue(); + } + + private KeyProtection getDecryptKeyProtection() throws Exception { + verify(mKeyStoreProxy).setEntry( + eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/decrypt"), + any(), + mProtectionParameterCaptor.capture()); + return (KeyProtection) mProtectionParameterCaptor.getValue(); + } +}