--- /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 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);
+ }
+ }
+}
--- /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 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;
+}
--- /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 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();
+ }
+}
--- /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 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;
+ }
+}
--- /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 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();
+ }
+}
--- /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 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();
+ }
+}