Merge "Throw on attempt to unwrap a WrappedKey with old PlatformKey"
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java
new file mode 100644
index 0000000..c7a98b6
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/BadPlatformKeyException.java
@@ -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 the {@link PlatformDecryptionKey} instance has a different generation ID from
+ * the {@link WrappedKey} instance.
+ *
+ * @hide
+ */
+public class BadPlatformKeyException extends Exception {
+
+    /**
+     * A new instance with {@code message}.
+     *
+     * @hide
+     */
+    public BadPlatformKeyException(String message) {
+        super(message);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java
new file mode 100644
index 0000000..35571f1
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/PlatformDecryptionKey.java
@@ -0,0 +1,65 @@
+/*
+ * 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.AndroidKeyStoreSecretKey;
+
+/**
+ * Used to unwrap recoverable keys before syncing them with remote storage.
+ *
+ * <p>This is a private key stored in AndroidKeyStore. Has an associated generation ID, which is
+ * stored with wrapped keys, allowing us to ensure the wrapped key has the same version as the
+ * platform key.
+ *
+ * @hide
+ */
+public class PlatformDecryptionKey {
+
+    private final int mGenerationId;
+    private final AndroidKeyStoreSecretKey mKey;
+
+    /**
+     * A new instance.
+     *
+     * @param generationId The generation ID of the platform key.
+     * @param key The key handle in AndroidKeyStore.
+     *
+     * @hide
+     */
+    public PlatformDecryptionKey(int generationId, AndroidKeyStoreSecretKey key) {
+        mGenerationId = generationId;
+        mKey = key;
+    }
+
+    /**
+     * Returns the generation ID.
+     *
+     * @hide
+     */
+    public int getGenerationId() {
+        return mGenerationId;
+    }
+
+    /**
+     * Returns the actual key, which can be used to decrypt.
+     *
+     * @hide
+     */
+    public AndroidKeyStoreSecretKey getKey() {
+        return mKey;
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
index 9002292..f18e796 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
@@ -124,27 +124,51 @@
         return mKeyMaterial;
     }
 
+
+    /**
+     * Returns the generation ID of the platform key, with which this key was wrapped.
+     *
+     * @hide
+     */
+    public int getPlatformKeyGenerationId() {
+        // TODO(robertberry) Implement. See ag/3362855.
+        return 1;
+    }
+
     /**
      * Unwraps the {@code wrappedKeys} with the {@code platformKey}.
      *
      * @return The unwrapped keys, indexed by alias.
      * @throws NoSuchAlgorithmException if AES/GCM/NoPadding Cipher or AES key type is unavailable.
+     * @throws BadPlatformKeyException if the {@code platformKey} has a different generation ID to
+     *     any of the {@code wrappedKeys}.
      *
      * @hide
      */
     public static Map<String, SecretKey> unwrapKeys(
-            SecretKey platformKey,
+            PlatformDecryptionKey platformKey,
             Map<String, WrappedKey> wrappedKeys)
-            throws NoSuchAlgorithmException, NoSuchPaddingException {
+            throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
         HashMap<String, SecretKey> unwrappedKeys = new HashMap<>();
         Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
+        int platformKeyGenerationId = platformKey.getGenerationId();
 
         for (String alias : wrappedKeys.keySet()) {
             WrappedKey wrappedKey = wrappedKeys.get(alias);
+            if (wrappedKey.getPlatformKeyGenerationId() != platformKeyGenerationId) {
+                throw new BadPlatformKeyException(String.format(
+                        Locale.US,
+                        "WrappedKey with alias '%s' was wrapped with platform key %d, not "
+                                + "platform key %d",
+                        alias,
+                        wrappedKey.getPlatformKeyGenerationId(),
+                        platformKey.getGenerationId()));
+            }
+
             try {
                 cipher.init(
                         Cipher.UNWRAP_MODE,
-                        platformKey,
+                        platformKey.getKey(),
                         new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
             } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
                 Log.e(TAG,
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java
index fa73722..8371fe5 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import android.security.keystore.AndroidKeyStoreSecretKey;
 import android.security.keystore.KeyGenParameterSpec;
@@ -46,6 +47,7 @@
     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 GENERATION_ID = 1;
     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;
@@ -77,9 +79,9 @@
     @Test
     public void decryptWrappedKeys_decryptsWrappedKeys() throws Exception {
         String alias = "karlin";
-        SecretKey platformKey = generateAndroidKeyStoreKey();
+        PlatformDecryptionKey platformKey = generatePlatformDecryptionKey();
         SecretKey appKey = generateKey();
-        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, appKey);
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey.getKey(), appKey);
         HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
         keysByAlias.put(alias, wrappedKey);
 
@@ -99,11 +101,29 @@
         keysByAlias.put(alias, wrappedKey);
 
         Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
-                generateAndroidKeyStoreKey(), keysByAlias);
+                generatePlatformDecryptionKey(), keysByAlias);
 
         assertEquals(0, unwrappedKeys.size());
     }
 
+    @Test
+    public void decryptWrappedKeys_throwsIfPlatformKeyGenerationIdDoesNotMatch() throws Exception {
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), generateKey());
+        HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
+        keysByAlias.put("benji", wrappedKey);
+
+        try {
+            WrappedKey.unwrapKeys(
+                    generatePlatformDecryptionKey(/*generationId=*/ 2), keysByAlias);
+            fail("Should have thrown.");
+        } catch (BadPlatformKeyException e) {
+            assertEquals(
+                    "WrappedKey with alias 'benji' was wrapped with platform key 1,"
+                            + " not platform key 2",
+                    e.getMessage());
+        }
+    }
+
     private SecretKey generateKey() throws Exception {
         KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
         keyGenerator.init(/*keySize=*/ 256);
@@ -122,4 +142,12 @@
                 .build());
         return (AndroidKeyStoreSecretKey) keyGenerator.generateKey();
     }
+
+    private PlatformDecryptionKey generatePlatformDecryptionKey() throws Exception {
+        return generatePlatformDecryptionKey(GENERATION_ID);
+    }
+
+    private PlatformDecryptionKey generatePlatformDecryptionKey(int generationId) throws Exception {
+        return new PlatformDecryptionKey(generationId, generateAndroidKeyStoreKey());
+    }
 }