--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>Each platform key has two entries in AndroidKeyStore:
+ *
+ * <ul>
+ * <li>Encrypt entry - this entry enables the root user to at any time encrypt.
+ * <li>Decrypt entry - this entry enables the root user to decrypt only after recent user
+ * authentication, i.e., within 15 seconds after a screen unlock.
+ * </ul>
+ *
+ * <p>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.
+ *
+ * <p>These IDs look as follows:
+ * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/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.
+ *
+ * <p>These IDs look as follows:
+ * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/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;
+ }
+}
--- /dev/null
+/*
+ * 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<KeyStore.ProtectionParameter> mProtectionParameterCaptor;
+ @Captor private ArgumentCaptor<KeyStore.Entry> 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<KeyStore.Entry> 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();
+ }
+}