OSDN Git Service

Use RecoverableKeyStoreDb in RecoverableKeyGenerator
authorRobert Berry <robertberry@google.com>
Tue, 19 Dec 2017 10:44:56 +0000 (10:44 +0000)
committerRobert Berry <robertberry@google.com>
Wed, 20 Dec 2017 13:41:49 +0000 (13:41 +0000)
This removes the layer of abstraction provided by RecoverableKeyStorage,
as it doesn't seem particularly useful, given how easy it is to just use
the real classes in tests. This also hooks up actually saving to the
database. I've modified the class so that you can have a single instance
for generating keys and just pass the 'uid' through, rather than having
to create a new instance per uid. I think this will simplify its use.
Also it no longer returns the key handle. As you can just get it out of
the AndroidKeyStore yourself, it doesn't seem useful to have the method
also do that.

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

services/core/java/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxy.java
services/core/java/com/android/server/locksettings/recoverablekeystore/KeyStoreProxyImpl.java
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorage.java [deleted file]
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageImpl.java [deleted file]
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageImplTest.java [deleted file]

diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java
new file mode 100644 (file)
index 0000000..9a4d051
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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));
+        }
+    }
+}
index 7c9b395..8103177 100644 (file)
@@ -40,4 +40,7 @@ public interface KeyStoreProxy {
     /** @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;
 }
index ceee381..59132da 100644 (file)
@@ -52,4 +52,9 @@ public class KeyStoreProxyImpl implements KeyStoreProxy {
             throws KeyStoreException {
         mKeyStore.setEntry(alias, entry, protParam);
     }
+
+    @Override
+    public void deleteEntry(String alias) throws KeyStoreException {
+        mKeyStore.deleteEntry(alias);
+    }
 }
index 54deec2..d50a736 100644 (file)
 
 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;
@@ -42,39 +44,39 @@ import javax.security.auth.DestroyFailedException;
  */
 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;
     }
 
     /**
@@ -84,50 +86,70 @@ public class RecoverableKeyGenerator {
      * 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));
         }
     }
 }
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorage.java
deleted file mode 100644 (file)
index 6a189ef..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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;
-}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageException.java
new file mode 100644 (file)
index 0000000..f9d28f1
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageImpl.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageImpl.java
deleted file mode 100644 (file)
index d4dede1..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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);
-    }
-}
index 12dbdb3..3012931 100644 (file)
 
 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
@@ -79,67 +88,69 @@ public class RecoverableKeyGeneratorTest {
         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 {
@@ -153,4 +164,8 @@ public class RecoverableKeyGeneratorTest {
                     .build());
         return (AndroidKeyStoreSecretKey) keyGenerator.generateKey();
     }
+
+    private static byte[] getUtf8Bytes(String s) {
+        return s.getBytes(StandardCharsets.UTF_8);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageImplTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStorageImplTest.java
deleted file mode 100644 (file)
index fb4e75e..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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();
-    }
-}