--- /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.security.keystore.AndroidKeyStoreProvider;
+
+import java.security.KeyStoreException;
+import java.security.NoSuchProviderException;
+
+public interface AndroidKeyStoreFactory {
+ KeyStoreProxy getKeyStoreForUid(int uid) throws KeyStoreException, NoSuchProviderException;
+
+ class Impl implements AndroidKeyStoreFactory {
+ @Override
+ public KeyStoreProxy getKeyStoreForUid(int uid)
+ throws KeyStoreException, NoSuchProviderException {
+ return new KeyStoreProxyImpl(AndroidKeyStoreProvider.getKeyStoreForUid(uid));
+ }
+ }
+}
/** @see KeyStore#setEntry(String, KeyStore.Entry, KeyStore.ProtectionParameter) */
void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
throws KeyStoreException;
+
+ /** @see KeyStore#deleteEntry(String) */
+ void deleteEntry(String alias) throws KeyStoreException;
}
throws KeyStoreException {
mKeyStore.setEntry(alias, entry, protParam);
}
+
+ @Override
+ public void deleteEntry(String alias) throws KeyStoreException {
+ mKeyStore.deleteEntry(alias);
+ }
}
package com.android.server.locksettings.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 com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
+
import java.security.InvalidKeyException;
+import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableEntryException;
+import java.security.NoSuchProviderException;
+import java.util.Locale;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
*/
public class RecoverableKeyGenerator {
private static final String TAG = "RecoverableKeyGenerator";
+
+ private static final int RESULT_CANNOT_INSERT_ROW = -1;
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(
- PlatformEncryptionKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
+ public static RecoverableKeyGenerator newInstance(RecoverableKeyStoreDb database)
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);
+ return new RecoverableKeyGenerator(
+ keyGenerator, database, new AndroidKeyStoreFactory.Impl());
}
private final KeyGenerator mKeyGenerator;
- private final RecoverableKeyStorage mRecoverableKeyStorage;
- private final PlatformEncryptionKey mPlatformKey;
+ private final RecoverableKeyStoreDb mDatabase;
+ private final AndroidKeyStoreFactory mAndroidKeyStoreFactory;
private RecoverableKeyGenerator(
KeyGenerator keyGenerator,
- PlatformEncryptionKey platformKey,
- RecoverableKeyStorage recoverableKeyStorage) {
+ RecoverableKeyStoreDb recoverableKeyStoreDb,
+ AndroidKeyStoreFactory androidKeyStoreFactory) {
mKeyGenerator = keyGenerator;
- mRecoverableKeyStorage = recoverableKeyStorage;
- mPlatformKey = platformKey;
+ mAndroidKeyStoreFactory = androidKeyStoreFactory;
+ mDatabase = recoverableKeyStoreDb;
}
/**
* 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 platformKey The user's platform key, with which to wrap the generated key.
+ * @param uid The uid of the application that will own the key.
* @param alias The alias by which the key will be known in AndroidKeyStore.
+ * @throws RecoverableKeyStorageException if there is some error persisting the key either to
+ * the AndroidKeyStore or the database.
+ * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
* @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 {
+ public void generateAndStoreKey(PlatformEncryptionKey platformKey, int uid, String alias)
+ throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
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);
+ KeyStoreProxy keyStore;
+
+ try {
+ keyStore = mAndroidKeyStoreFactory.getKeyStoreForUid(uid);
+ } catch (NoSuchProviderException e) {
+ throw new RecoverableKeyStorageException(
+ "Impossible: AndroidKeyStore provider did not exist", e);
+ } catch (KeyStoreException e) {
+ throw new RecoverableKeyStorageException(
+ "Could not load AndroidKeyStore for " + uid, e);
+ }
+
+ try {
+ keyStore.setEntry(
+ alias,
+ new KeyStore.SecretKeyEntry(key),
+ new KeyProtection.Builder(
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .build());
+ } catch (KeyStoreException e) {
+ throw new RecoverableKeyStorageException(
+ "Failed to load (%d, %s) into AndroidKeyStore", e);
+ }
+ WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key);
try {
// Keep raw key material in memory for minimum possible time.
key.destroy();
} catch (DestroyFailedException e) {
Log.w(TAG, "Could not destroy SecretKey.");
}
+ long result = mDatabase.insertKey(uid, alias, wrappedKey);
- mRecoverableKeyStorage.persistToDisk(alias, wrappedKey);
+ if (result == RESULT_CANNOT_INSERT_ROW) {
+ // Attempt to clean up
+ try {
+ keyStore.deleteEntry(alias);
+ } catch (KeyStoreException e) {
+ Log.e(TAG, String.format(Locale.US,
+ "Could not delete recoverable key (%d, %s) from "
+ + "AndroidKeyStore after error writing to database.", uid, alias),
+ e);
+ }
- 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);
+ throw new RecoverableKeyStorageException(
+ String.format(
+ Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
}
}
}
+++ /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.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;
-
- /**
- * Removes the entry with the given {@code alias} from AndroidKeyStore.
- *
- * @throws KeyStoreException if an error occurred deleting the key from AndroidKeyStore.
- *
- * @hide
- */
- void removeFromAndroidKeyStore(String alias) 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;
+
+/**
+ * Error thrown when there was a problem writing or reading recoverable key information to or from
+ * storage.
+ *
+ * <p>Storage is typically
+ * {@link com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb} or
+ * AndroidKeyStore.
+ */
+public class RecoverableKeyStorageException extends Exception {
+ public RecoverableKeyStorageException(String message) {
+ super(message);
+ }
+
+ public RecoverableKeyStorageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
+++ /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.security.keystore.AndroidKeyStoreProvider;
-import android.security.keystore.KeyProtection;
-
-import java.io.IOException;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.UnrecoverableEntryException;
-
-import javax.crypto.SecretKey;
-
-/**
- * Implementation of {@link RecoverableKeyStorage} for a specific application.
- *
- * <p>Persists wrapped keys to disk, and loads raw keys into AndroidKeyStore.
- *
- * @hide
- */
-public class RecoverableKeyStorageImpl implements RecoverableKeyStorage {
- private final KeyStore mKeyStore;
-
- /**
- * A new instance, storing recoverable keys for the given {@code userId}.
- *
- * @throws KeyStoreException if unable to load AndroidKeyStore.
- * @throws NoSuchProviderException if AndroidKeyStore is not in this version of Android. Should
- * never occur.
- *
- * @hide
- */
- public static RecoverableKeyStorageImpl newInstance(int userId) throws KeyStoreException,
- NoSuchProviderException {
- KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(userId);
- 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 the application's AndroidKeyStore, keyed by {@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 the application's 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 ((SecretKey) mKeyStore.getKey(alias, /*password=*/ null));
- }
-
- /**
- * Removes the entry with the given {@code alias} from the application's AndroidKeyStore.
- *
- * @throws KeyStoreException if an error occurred deleting the key from AndroidKeyStore.
- *
- * @hide
- */
- @Override
- public void removeFromAndroidKeyStore(String alias) throws KeyStoreException {
- mKeyStore.deleteEntry(alias);
- }
-}
package com.android.server.locksettings.recoverablekeystore;
+import static junit.framework.Assert.fail;
+
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 static org.junit.Assert.assertTrue;
+import android.content.Context;
+import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
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 com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
+
+import com.google.common.collect.ImmutableMap;
+
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.io.File;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
import java.security.KeyStore;
+import java.util.Arrays;
+import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class RecoverableKeyGeneratorTest {
+ private static final String DATABASE_FILE_NAME = "recoverablekeystore.db";
private static final int TEST_GENERATION_ID = 3;
private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
private static final String KEY_ALGORITHM = "AES";
+ private static final String SUPPORTED_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+ private static final String UNSUPPORTED_CIPHER_ALGORITHM = "AES/CTR/NoPadding";
private static final String TEST_ALIAS = "karlin";
private static final String WRAPPING_KEY_ALIAS = "RecoverableKeyGeneratorTestWrappingKey";
-
- @Mock
- RecoverableKeyStorage mRecoverableKeyStorage;
-
- @Captor ArgumentCaptor<KeyProtection> mKeyProtectionArgumentCaptor;
+ private static final int KEYSTORE_UID_SELF = -1;
+ private static final int GCM_TAG_LENGTH_BITS = 128;
+ private static final int GCM_NONCE_LENGTH_BYTES = 12;
private PlatformEncryptionKey mPlatformKey;
- private SecretKey mKeyHandle;
+ private PlatformDecryptionKey mDecryptKey;
+ private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
+ private File mDatabaseFile;
private RecoverableKeyGenerator mRecoverableKeyGenerator;
@Before
public void setUp() throws Exception {
- MockitoAnnotations.initMocks(this);
- mPlatformKey = new PlatformEncryptionKey(TEST_GENERATION_ID, generateAndroidKeyStoreKey());
- mKeyHandle = generateKey();
- mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(
- mPlatformKey, mRecoverableKeyStorage);
-
- when(mRecoverableKeyStorage.loadFromAndroidKeyStore(any())).thenReturn(mKeyHandle);
+ Context context = InstrumentationRegistry.getTargetContext();
+ mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
+ mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
+
+ AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
+ mPlatformKey = new PlatformEncryptionKey(TEST_GENERATION_ID, platformKey);
+ mDecryptKey = new PlatformDecryptionKey(TEST_GENERATION_ID, platformKey);
+ mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mRecoverableKeyStoreDb);
}
@After
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());
+ mRecoverableKeyStoreDb.close();
+ mDatabaseFile.delete();
}
@Test
- public void generateAndStoreKey_storesKeyEnabledForEncryptDecrypt() throws Exception {
- mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
+ public void generateAndStoreKey_setsKeyInKeyStore() throws Exception {
+ mRecoverableKeyGenerator.generateAndStoreKey(mPlatformKey, KEYSTORE_UID_SELF, TEST_ALIAS);
- KeyProtection keyProtection = getKeyProtectionUsed();
- assertEquals(KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT,
- keyProtection.getPurposes());
+ KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(KEYSTORE_UID_SELF);
+ assertTrue(keyStore.containsAlias(TEST_ALIAS));
}
@Test
- public void generateAndStoreKey_storesKeyEnabledForGCM() throws Exception {
- mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
-
- KeyProtection keyProtection = getKeyProtectionUsed();
- assertArrayEquals(new String[] { KeyProperties.BLOCK_MODE_GCM },
- keyProtection.getBlockModes());
+ public void generateAndStoreKey_storesKeyEnabledForAesGcmNoPaddingEncryptDecrypt()
+ throws Exception {
+ mRecoverableKeyGenerator.generateAndStoreKey(mPlatformKey, KEYSTORE_UID_SELF, TEST_ALIAS);
+
+ KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(KEYSTORE_UID_SELF);
+ SecretKey key = (SecretKey) keyStore.getKey(TEST_ALIAS, /*password=*/ null);
+ Cipher cipher = Cipher.getInstance(SUPPORTED_CIPHER_ALGORITHM);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
+ Arrays.fill(nonce, (byte) 0);
+ cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
}
@Test
- public void generateAndStoreKey_storesKeyEnabledForNoPadding() throws Exception {
- mRecoverableKeyGenerator.generateAndStoreKey(TEST_ALIAS);
-
- KeyProtection keyProtection = getKeyProtectionUsed();
- assertArrayEquals(new String[] { KeyProperties.ENCRYPTION_PADDING_NONE },
- keyProtection.getEncryptionPaddings());
+ public void generateAndStoreKey_storesKeyDisabledForOtherModes() throws Exception {
+ mRecoverableKeyGenerator.generateAndStoreKey(mPlatformKey, KEYSTORE_UID_SELF, TEST_ALIAS);
+
+ KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(KEYSTORE_UID_SELF);
+ SecretKey key = (SecretKey) keyStore.getKey(TEST_ALIAS, /*password=*/ null);
+ Cipher cipher = Cipher.getInstance(UNSUPPORTED_CIPHER_ALGORITHM);
+
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ fail("Should not be able to use key for " + UNSUPPORTED_CIPHER_ALGORITHM);
+ } catch (InvalidKeyException e) {
+ // expected
+ }
}
@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();
+ mRecoverableKeyGenerator.generateAndStoreKey(mPlatformKey, KEYSTORE_UID_SELF, TEST_ALIAS);
+
+ KeyStore keyStore = AndroidKeyStoreProvider.getKeyStoreForUid(KEYSTORE_UID_SELF);
+ SecretKey key = (SecretKey) keyStore.getKey(TEST_ALIAS, /*password=*/ null);
+ WrappedKey wrappedKey = mRecoverableKeyStoreDb.getKey(KEYSTORE_UID_SELF, TEST_ALIAS);
+ SecretKey unwrappedKey = WrappedKey
+ .unwrapKeys(mDecryptKey, ImmutableMap.of(TEST_ALIAS, wrappedKey))
+ .get(TEST_ALIAS);
+
+ // key and unwrappedKey should be equivalent. let's check!
+ byte[] plaintext = getUtf8Bytes("dtianpos");
+ Cipher cipher = Cipher.getInstance(SUPPORTED_CIPHER_ALGORITHM);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] encrypted = cipher.doFinal(plaintext);
+ byte[] iv = cipher.getIV();
+ cipher.init(Cipher.DECRYPT_MODE, unwrappedKey, new GCMParameterSpec(128, iv));
+ byte[] decrypted = cipher.doFinal(encrypted);
+ assertArrayEquals(decrypted, plaintext);
}
private AndroidKeyStoreSecretKey generateAndroidKeyStoreKey() throws Exception {
.build());
return (AndroidKeyStoreSecretKey) keyGenerator.generateKey();
}
+
+ private static byte[] getUtf8Bytes(String s) {
+ return s.getBytes(StandardCharsets.UTF_8);
+ }
}
+++ /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.assertNull;
-import static org.junit.Assert.fail;
-
-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 java.nio.charset.StandardCharsets;
-import java.security.InvalidKeyException;
-import java.security.KeyStoreException;
-import java.util.Random;
-
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class RecoverableKeyStorageImplTest {
- private static final String KEY_ALGORITHM = "AES";
- 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;
- private static final int GCM_NONCE_LENGTH_BYTES = 12;
- private static final String TEST_KEY_ALIAS = "RecoverableKeyStorageImplTestKey";
- private static final int KEYSTORE_UID_SELF = -1;
-
- private RecoverableKeyStorageImpl mRecoverableKeyStorage;
-
- @Before
- public void setUp() throws Exception {
- mRecoverableKeyStorage = RecoverableKeyStorageImpl.newInstance(
- /*userId=*/ KEYSTORE_UID_SELF);
- }
-
- @After
- public void tearDown() {
- try {
- mRecoverableKeyStorage.removeFromAndroidKeyStore(TEST_KEY_ALIAS);
- } catch (KeyStoreException e) {
- // Do nothing.
- }
- }
-
- @Test
- public void loadFromAndroidKeyStore_loadsAKeyThatWasImported() throws Exception {
- SecretKey key = generateKey();
- mRecoverableKeyStorage.importIntoAndroidKeyStore(
- TEST_KEY_ALIAS,
- key,
- getKeyProperties());
-
- assertKeysAreEquivalent(
- key, mRecoverableKeyStorage.loadFromAndroidKeyStore(TEST_KEY_ALIAS));
- }
-
- @Test
- public void importIntoAndroidKeyStore_importsWithKeyProperties() throws Exception {
- mRecoverableKeyStorage.importIntoAndroidKeyStore(
- TEST_KEY_ALIAS,
- generateKey(),
- getKeyProperties());
-
- SecretKey key = mRecoverableKeyStorage.loadFromAndroidKeyStore(TEST_KEY_ALIAS);
-
- Mac mac = Mac.getInstance("HmacSHA256");
- try {
- // Fails because missing PURPOSE_SIGN or PURPOSE_VERIFY
- mac.init(key);
- fail("Was able to initialize Mac with an ENCRYPT/DECRYPT-only key.");
- } catch (InvalidKeyException e) {
- // expect exception
- }
- }
-
- @Test
- public void removeFromAndroidKeyStore_removesAnEntry() throws Exception {
- mRecoverableKeyStorage.importIntoAndroidKeyStore(
- TEST_KEY_ALIAS,
- generateKey(),
- getKeyProperties());
-
- mRecoverableKeyStorage.removeFromAndroidKeyStore(TEST_KEY_ALIAS);
-
- assertNull(mRecoverableKeyStorage.loadFromAndroidKeyStore(TEST_KEY_ALIAS));
- }
-
- private static KeyProtection getKeyProperties() {
- return new KeyProtection.Builder(
- KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
- .build();
- }
-
- /**
- * Asserts that {@code b} key can decrypt data encrypted with {@code a} key. Otherwise throws.
- */
- private static void assertKeysAreEquivalent(SecretKey a, SecretKey b) throws Exception {
- byte[] plaintext = "doge".getBytes(StandardCharsets.UTF_8);
- byte[] nonce = generateGcmNonce();
-
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
- cipher.init(Cipher.ENCRYPT_MODE, a, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
- byte[] encrypted = cipher.doFinal(plaintext);
-
- cipher.init(Cipher.DECRYPT_MODE, b, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));
- byte[] decrypted = cipher.doFinal(encrypted);
-
- assertArrayEquals(decrypted, plaintext);
- }
-
- /**
- * Returns a new random GCM nonce.
- */
- private static byte[] generateGcmNonce() {
- Random random = new Random();
- byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
- random.nextBytes(nonce);
- return nonce;
- }
-
- private static SecretKey generateKey() throws Exception {
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
- keyGenerator.init(/*keySize=*/ 256);
- return keyGenerator.generateKey();
- }
-}