OSDN Git Service

Add PlatformKeyManager helper for RecoverableKeyStoreLoader
authorRobert Berry <robertberry@google.com>
Mon, 11 Dec 2017 14:34:29 +0000 (14:34 +0000)
committerRobert Berry <robertberry@google.com>
Mon, 18 Dec 2017 23:25:33 +0000 (23:25 +0000)
Manages generating the platform key and then loading it into AndroidKeyStore
with different permissions for 'decrypt' and 'encrypt'. Encrypt should be always
available, so as to enable us to generate application keys at any time, and be
able to sync them wrapped with the platform key to disk. Decrypt should only be
available shortly after a screen unlock - i.e., so that we can unwrap the keys
persisted to disk, then rewrap them with the recovery key and sync them to the
remote storage.

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: I7575ea1c3c78d5544ef763324ac47dffb3993b55

services/core/java/com/android/server/locksettings/recoverablekeystore/InsecureUserException.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java [new file with mode: 0644]
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/PlatformKeyManagerTest.java [new file with mode: 0644]

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 (file)
index 0000000..5155a99
--- /dev/null
@@ -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 (file)
index 0000000..7c9b395
--- /dev/null
@@ -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 (file)
index 0000000..ceee381
--- /dev/null
@@ -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 (file)
index 0000000..074c596
--- /dev/null
@@ -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.
+ *
+ * <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;
+    }
+}
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 (file)
index 0000000..a997770
--- /dev/null
@@ -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<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();
+    }
+}