OSDN Git Service

Add helpers for generating keys, wrapping them, and persisting them
authorRobert Berry <robertberry@google.com>
Thu, 7 Dec 2017 14:33:54 +0000 (14:33 +0000)
committerRobert Berry <robertberry@google.com>
Fri, 8 Dec 2017 17:47:56 +0000 (17:47 +0000)
As part of the RecoverableKeyStoreLoader, we need to be able to generate new
256-bit AES keys, sync them with AndroidKeyStore, and persist them, wrapped
to disk. This allows us to recover them later, using a Platform key, and
sync them with remote storage.

Test: manual for now (how do we do automated tests on Framework?)
Change-Id: I32e0beabaecc9bea9f95ca2beea851e9be833358

core/java/android/security/recoverablekeystore/RecoverableKeyGenerator.java [new file with mode: 0644]
core/java/android/security/recoverablekeystore/RecoverableKeyStorage.java [new file with mode: 0644]
core/java/android/security/recoverablekeystore/RecoverableKeyStorageImpl.java [new file with mode: 0644]
core/java/android/security/recoverablekeystore/WrappedKey.java [new file with mode: 0644]
core/tests/coretests/src/android/security/recoverablekeystore/RecoverableKeyGeneratorTest.java [new file with mode: 0644]
core/tests/coretests/src/android/security/recoverablekeystore/WrappedKeyTest.java [new file with mode: 0644]

diff --git a/core/java/android/security/recoverablekeystore/RecoverableKeyGenerator.java b/core/java/android/security/recoverablekeystore/RecoverableKeyGenerator.java
new file mode 100644 (file)
index 0000000..4125f0b
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * 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 android.security.recoverablekeystore;
+
+import android.security.keystore.AndroidKeyStoreSecretKey;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+import android.util.Log;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.security.auth.DestroyFailedException;
+
+/**
+ * Generates keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
+ *
+ * <p>Generates 256-bit AES keys, which can be used for encrypt / decrypt with AES/GCM/NoPadding.
+ * They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
+ * service.
+ *
+ * @hide
+ */
+public class RecoverableKeyGenerator {
+    private static final String TAG = "RecoverableKeyGenerator";
+    private static final String KEY_GENERATOR_ALGORITHM = "AES";
+    private static final int KEY_SIZE_BITS = 256;
+
+    /**
+     * A new {@link RecoverableKeyGenerator} instance.
+     *
+     * @param platformKey Secret key used to wrap generated keys before persisting to disk.
+     * @param recoverableKeyStorage Class that manages persisting wrapped keys to disk.
+     * @throws NoSuchAlgorithmException if "AES" key generation or "AES/GCM/NoPadding" cipher is
+     *     unavailable. Should never happen.
+     *
+     * @hide
+     */
+    public static RecoverableKeyGenerator newInstance(
+            AndroidKeyStoreSecretKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
+            throws NoSuchAlgorithmException {
+        // NB: This cannot use AndroidKeyStore as the provider, as we need access to the raw key
+        // material, so that it can be synced to disk in encrypted form.
+        KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_GENERATOR_ALGORITHM);
+        return new RecoverableKeyGenerator(keyGenerator, platformKey, recoverableKeyStorage);
+    }
+
+    private final KeyGenerator mKeyGenerator;
+    private final RecoverableKeyStorage mRecoverableKeyStorage;
+    private final AndroidKeyStoreSecretKey mPlatformKey;
+
+    private RecoverableKeyGenerator(
+            KeyGenerator keyGenerator,
+            AndroidKeyStoreSecretKey platformKey,
+            RecoverableKeyStorage recoverableKeyStorage) {
+        mKeyGenerator = keyGenerator;
+        mRecoverableKeyStorage = recoverableKeyStorage;
+        mPlatformKey = platformKey;
+    }
+
+    /**
+     * Generates a 256-bit AES key with the given alias.
+     *
+     * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
+     * persisted to disk so that it can be synced remotely, and then recovered on another device.
+     * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
+     *
+     * <p>The key handle returned to the caller is a reference to the AndroidKeyStore key,
+     * meaning that the caller is never able to access the raw, unencrypted key.
+     *
+     * @param alias The alias by which the key will be known in AndroidKeyStore.
+     * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
+     * @throws IOException if there was an issue writing the wrapped key to the wrapped key store.
+     * @throws UnrecoverableEntryException if could not retrieve key after putting it in
+     *     AndroidKeyStore. This should not happen.
+     * @return A handle to the AndroidKeyStore key.
+     *
+     * @hide
+     */
+    public SecretKey generateAndStoreKey(String alias) throws KeyStoreException,
+            InvalidKeyException, IOException, UnrecoverableEntryException {
+        mKeyGenerator.init(KEY_SIZE_BITS);
+        SecretKey key = mKeyGenerator.generateKey();
+
+        mRecoverableKeyStorage.importIntoAndroidKeyStore(
+                alias,
+                key,
+                new KeyProtection.Builder(
+                        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+                        .build());
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(mPlatformKey, key);
+
+        try {
+            // Keep raw key material in memory for minimum possible time.
+            key.destroy();
+        } catch (DestroyFailedException e) {
+            Log.w(TAG, "Could not destroy SecretKey.");
+        }
+
+        mRecoverableKeyStorage.persistToDisk(alias, wrappedKey);
+
+        try {
+            // Reload from the keystore, so that the caller is only provided with the handle of the
+            // key, not the raw key material.
+            return mRecoverableKeyStorage.loadFromAndroidKeyStore(alias);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(
+                    "Impossible: NoSuchAlgorithmException when attempting to retrieve a key "
+                            + "that has only just been stored in AndroidKeyStore.", e);
+        }
+    }
+}
diff --git a/core/java/android/security/recoverablekeystore/RecoverableKeyStorage.java b/core/java/android/security/recoverablekeystore/RecoverableKeyStorage.java
new file mode 100644 (file)
index 0000000..c239e00
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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 android.security.recoverablekeystore;
+
+import android.security.keystore.KeyProtection;
+
+import java.io.IOException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Stores wrapped keys to disk, so they can be synced on the next screen unlock event.
+ *
+ * @hide
+ */
+public interface RecoverableKeyStorage {
+
+    /**
+     * Writes {@code wrappedKey} to disk, keyed by the application's uid and the {@code alias}.
+     *
+     * @throws IOException if an error occurred writing to disk.
+     *
+     * @hide
+     */
+    void persistToDisk(String alias, WrappedKey wrappedKey) throws IOException;
+
+    /**
+     * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and
+     * the {@code alias}.
+     *
+     * @param alias The alias of the key.
+     * @param key The key.
+     * @param keyProtection Protection params denoting what the key can be used for. (e.g., what
+     *                      Cipher modes, whether for encrpyt/decrypt or signing, etc.)
+     * @throws KeyStoreException if an error occurred loading the key into the AndroidKeyStore.
+     *
+     * @hide
+     */
+    void importIntoAndroidKeyStore(String alias, SecretKey key, KeyProtection keyProtection) throws
+            KeyStoreException;
+
+    /**
+     * Loads a key handle from AndroidKeyStore.
+     *
+     * @param alias Alias of the key to load.
+     * @return The key handle.
+     * @throws KeyStoreException if an error occurred loading the key from AndroidKeyStore.
+     *
+     * @hide
+     */
+    SecretKey loadFromAndroidKeyStore(String alias) throws KeyStoreException,
+            NoSuchAlgorithmException,
+            UnrecoverableEntryException;
+}
diff --git a/core/java/android/security/recoverablekeystore/RecoverableKeyStorageImpl.java b/core/java/android/security/recoverablekeystore/RecoverableKeyStorageImpl.java
new file mode 100644 (file)
index 0000000..b9926dd
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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 android.security.recoverablekeystore;
+
+import android.security.keystore.KeyProtection;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateException;
+
+import javax.crypto.SecretKey;
+
+/**
+ * Implementation of {@link RecoverableKeyStorage}.
+ *
+ * <p>Persists wrapped keys to disk, and loads raw keys into AndroidKeyStore.
+ *
+ * @hide
+ */
+public class RecoverableKeyStorageImpl implements RecoverableKeyStorage {
+    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
+
+    private final KeyStore mKeyStore;
+
+    /**
+     * A new instance.
+     *
+     * @throws KeyStoreException if unable to load AndroidKeyStore.
+     *
+     * @hide
+     */
+    public static RecoverableKeyStorageImpl newInstance() 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 new RecoverableKeyStorageImpl(keyStore);
+    }
+
+    private RecoverableKeyStorageImpl(KeyStore keyStore) {
+        mKeyStore = keyStore;
+    }
+
+    /**
+     * Writes {@code wrappedKey} to disk, keyed by the application's uid and the {@code alias}.
+     *
+     * @throws IOException if an error occurred writing to disk.
+     *
+     * @hide
+     */
+    @Override
+    public void persistToDisk(String alias, WrappedKey wrappedKey) throws IOException {
+        // TODO(robertberry) Add implementation.
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Imports {@code key} into AndroidKeyStore, keyed by the application's uid and the
+     * {@code alias}.
+     *
+     * @param alias The alias of the key.
+     * @param key The key.
+     * @param keyProtection Protection params denoting what the key can be used for. (e.g., what
+     *                      Cipher modes, whether for encrpyt/decrypt or signing, etc.)
+     * @throws KeyStoreException if an error occurred loading the key into the AndroidKeyStore.
+     *
+     * @hide
+     */
+    @Override
+    public void importIntoAndroidKeyStore(String alias, SecretKey key, KeyProtection keyProtection)
+            throws KeyStoreException {
+        mKeyStore.setEntry(alias, new KeyStore.SecretKeyEntry(key), keyProtection);
+    }
+
+    /**
+     * Loads a key handle from AndroidKeyStore.
+     *
+     * @param alias Alias of the key to load.
+     * @return The key handle.
+     * @throws KeyStoreException if an error occurred loading the key from AndroidKeyStore.
+     *
+     * @hide
+     */
+    @Override
+    public SecretKey loadFromAndroidKeyStore(String alias)
+            throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableEntryException {
+        return ((KeyStore.SecretKeyEntry) mKeyStore.getEntry(alias, /*protParam=*/ null))
+                .getSecretKey();
+    }
+}
diff --git a/core/java/android/security/recoverablekeystore/WrappedKey.java b/core/java/android/security/recoverablekeystore/WrappedKey.java
new file mode 100644 (file)
index 0000000..51665ae
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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 android.security.recoverablekeystore;
+
+import java.security.InvalidKeyException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
+ *
+ * @hide
+ */
+public class WrappedKey {
+    private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+
+    private final byte[] mNonce;
+    private final byte[] mKeyMaterial;
+
+    /**
+     * Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material.
+     *
+     * @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or
+     *     if {@code key} does not expose its key material. See
+     *     {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
+     *     not expose its key material.
+     */
+    public static WrappedKey fromSecretKey(
+            SecretKey wrappingKey, SecretKey key) throws InvalidKeyException, KeyStoreException {
+        if (key.getEncoded() == null) {
+            throw new InvalidKeyException(
+                    "key does not expose encoded material. It cannot be wrapped.");
+        }
+
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            throw new RuntimeException(
+                    "Android does not support AES/GCM/NoPadding. This should never happen.");
+        }
+
+        cipher.init(Cipher.WRAP_MODE, wrappingKey);
+        byte[] encryptedKeyMaterial;
+        try {
+            encryptedKeyMaterial = cipher.wrap(key);
+        } catch (IllegalBlockSizeException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof KeyStoreException) {
+                // If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException
+                // with KeyStoreException as the cause. This is due to there being no better option
+                // here, as the Cipher#wrap only checked throws InvalidKeyException or
+                // IllegalBlockSizeException. If this is the case, we want to propagate it to the
+                // caller, so rethrow the cause.
+                throw (KeyStoreException) cause;
+            } else {
+                throw new RuntimeException(
+                        "IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.",
+                        e);
+            }
+        }
+
+        return new WrappedKey(/*mNonce=*/ cipher.getIV(), /*mKeyMaterial=*/ encryptedKeyMaterial);
+    }
+
+    /**
+     * A new instance.
+     *
+     * @param nonce The nonce with which the key material was encrypted.
+     * @param keyMaterial The encrypted bytes of the key material.
+     *
+     * @hide
+     */
+    public WrappedKey(byte[] nonce, byte[] keyMaterial) {
+        mNonce = nonce;
+        mKeyMaterial = keyMaterial;
+    }
+
+    /**
+     * Returns the nonce with which the key material was encrypted.
+     *
+     * @hide
+     */
+    public byte[] getNonce() {
+        return mNonce;
+    }
+
+    /**
+     * Returns the encrypted key material.
+     *
+     * @hide
+     */
+    public byte[] getKeyMaterial() {
+        return mKeyMaterial;
+    }
+}
diff --git a/core/tests/coretests/src/android/security/recoverablekeystore/RecoverableKeyGeneratorTest.java b/core/tests/coretests/src/android/security/recoverablekeystore/RecoverableKeyGeneratorTest.java
new file mode 100644 (file)
index 0000000..d85d3b8
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * 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 android.security.recoverablekeystore;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+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.security.keystore.AndroidKeyStoreSecretKey;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+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 javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RecoverableKeyGeneratorTest {
+    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
+    private static final String KEY_ALGORITHM = "AES";
+    private static final String TEST_ALIAS = "karlin";
+    private static final String WRAPPING_KEY_ALIAS = "RecoverableKeyGeneratorTestWrappingKey";
+
+    @Mock RecoverableKeyStorage mRecoverableKeyStorage;
+
+    @Captor ArgumentCaptor<KeyProtection> mKeyProtectionArgumentCaptor;
+
+    private AndroidKeyStoreSecretKey mPlatformKey;
+    private SecretKey mKeyHandle;
+    private RecoverableKeyGenerator mRecoverableKeyGenerator;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mPlatformKey = generateAndroidKeyStoreKey();
+        mKeyHandle = generateKey();
+        mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(
+                mPlatformKey, mRecoverableKeyStorage);
+
+        when(mRecoverableKeyStorage.loadFromAndroidKeyStore(any())).thenReturn(mKeyHandle);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
+        keyStore.load(/*param=*/ null);
+        keyStore.deleteEntry(WRAPPING_KEY_ALIAS);
+    }
+
+    @Test
+    public void generateAndStoreKey_setsKeyInKeyStore() throws Exception {
+        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+
+        verify(mRecoverableKeyStorage, times(1))
+                .importIntoAndroidKeyStore(eq(TEST_ALIAS), any(), any());
+    }
+
+    @Test
+    public void generateAndStoreKey_storesKeyEnabledForEncryptDecrypt() throws Exception {
+        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+
+        KeyProtection keyProtection = getKeyProtectionUsed();
+        assertEquals(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT,
+                keyProtection.getPurposes());
+    }
+
+    @Test
+    public void generateAndStoreKey_storesKeyEnabledForGCM() throws Exception {
+        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+
+        KeyProtection keyProtection = getKeyProtectionUsed();
+        assertArrayEquals(new String[] { KeyProperties.BLOCK_MODE_GCM },
+                keyProtection.getBlockModes());
+    }
+
+    @Test
+    public void generateAndStoreKey_storesKeyEnabledForNoPadding() throws Exception {
+        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+
+        KeyProtection keyProtection = getKeyProtectionUsed();
+        assertArrayEquals(new String[] { KeyProperties.ENCRYPTION_PADDING_NONE },
+                keyProtection.getEncryptionPaddings());
+    }
+
+    @Test
+    public void generateAndStoreKey_storesWrappedKey() throws Exception {
+        mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+
+        verify(mRecoverableKeyStorage, times(1)).persistToDisk(eq(TEST_ALIAS), any());
+    }
+
+    @Test
+    public void generateAndStoreKey_returnsKeyHandle() throws Exception {
+        SecretKey secretKey = mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+
+        assertEquals(mKeyHandle, secretKey);
+    }
+
+    private KeyProtection getKeyProtectionUsed() throws Exception {
+        verify(mRecoverableKeyStorage, times(1)).importIntoAndroidKeyStore(
+                any(), any(), mKeyProtectionArgumentCaptor.capture());
+        return mKeyProtectionArgumentCaptor.getValue();
+    }
+
+    private SecretKey generateKey() throws Exception {
+        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
+        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();
+    }
+}
diff --git a/core/tests/coretests/src/android/security/recoverablekeystore/WrappedKeyTest.java b/core/tests/coretests/src/android/security/recoverablekeystore/WrappedKeyTest.java
new file mode 100644 (file)
index 0000000..233c821
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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 android.security.recoverablekeystore;
+
+import static org.junit.Assert.assertEquals;
+
+import android.security.keystore.AndroidKeyStoreSecretKey;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.KeyStore;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WrappedKeyTest {
+    private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
+    private static final String KEY_ALGORITHM = "AES";
+    private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final String WRAPPING_KEY_ALIAS = "WrappedKeyTestWrappingKeyAlias";
+    private static final int GCM_TAG_LENGTH_BYTES = 16;
+    private static final int BITS_PER_BYTE = 8;
+    private static final int GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE;
+
+    @After
+    public void tearDown() throws Exception {
+        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
+        keyStore.load(/*param=*/ null);
+        keyStore.deleteEntry(WRAPPING_KEY_ALIAS);
+    }
+
+    @Test
+    public void fromSecretKey_createsWrappedKeyThatCanBeUnwrapped() throws Exception {
+        SecretKey wrappingKey = generateAndroidKeyStoreKey();
+        SecretKey rawKey = generateKey();
+
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(wrappingKey, rawKey);
+
+        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        cipher.init(
+                Cipher.UNWRAP_MODE,
+                wrappingKey,
+                new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
+        SecretKey unwrappedKey = (SecretKey) cipher.unwrap(
+                wrappedKey.getKeyMaterial(), KEY_ALGORITHM, Cipher.SECRET_KEY);
+        assertEquals(rawKey, unwrappedKey);
+    }
+
+    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();
+    }
+}