OSDN Git Service

Add KeySyncUtils
authorRobert Berry <robertberry@google.com>
Wed, 13 Dec 2017 15:58:19 +0000 (15:58 +0000)
committerRobert Berry <robertberry@google.com>
Wed, 13 Dec 2017 22:30:22 +0000 (22:30 +0000)
Static methods to help with the RecoverableKeyStoreLoader remote sync
flow.

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

services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java [new file with mode: 0644]
services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java [new file with mode: 0644]
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java

diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
new file mode 100644 (file)
index 0000000..e4d2b95
--- /dev/null
@@ -0,0 +1,167 @@
+/*
+ * 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 java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/**
+ * Utility functions for the flow where the RecoverableKeyStoreLoader syncs keys with remote
+ * storage.
+ *
+ * @hide
+ */
+public class KeySyncUtils {
+
+    private static final String RECOVERY_KEY_ALGORITHM = "AES";
+    private static final int RECOVERY_KEY_SIZE_BITS = 256;
+
+    private static final byte[] THM_ENCRYPTED_RECOVERY_KEY_HEADER =
+            "V1 THM_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER =
+            "V1 locally_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] ENCRYPTED_APPLICATION_KEY_HEADER =
+            "V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
+
+    private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);
+
+    /**
+     * Encrypts the recovery key using both the lock screen hash and the remote storage's public
+     * key.
+     *
+     * @param publicKey The public key of the remote storage.
+     * @param lockScreenHash The user's lock screen hash.
+     * @param vaultParams Additional parameters to send to the remote storage.
+     * @param recoveryKey The recovery key.
+     * @return The encrypted bytes.
+     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+     * @throws InvalidKeyException if the public key or the lock screen could not be used to encrypt
+     *     the data.
+     *
+     * @hide
+     */
+    public byte[] thmEncryptRecoveryKey(
+            PublicKey publicKey,
+            byte[] lockScreenHash,
+            byte[] vaultParams,
+            SecretKey recoveryKey
+    ) throws NoSuchAlgorithmException, InvalidKeyException {
+        byte[] encryptedRecoveryKey = locallyEncryptRecoveryKey(lockScreenHash, recoveryKey);
+        byte[] thmKfHash = calculateThmKfHash(lockScreenHash);
+        byte[] header = concat(THM_ENCRYPTED_RECOVERY_KEY_HEADER, vaultParams);
+        return SecureBox.encrypt(
+                /*theirPublicKey=*/ publicKey,
+                /*sharedSecret=*/ thmKfHash,
+                /*header=*/ header,
+                /*payload=*/ encryptedRecoveryKey);
+    }
+
+    /**
+     * Calculates the THM_KF hash of the lock screen hash.
+     *
+     * @param lockScreenHash The lock screen hash.
+     * @return The hash.
+     * @throws NoSuchAlgorithmException if SHA-256 is unavailable (should never happen).
+     *
+     * @hide
+     */
+    public static byte[] calculateThmKfHash(byte[] lockScreenHash)
+            throws NoSuchAlgorithmException {
+        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+        messageDigest.update(THM_KF_HASH_PREFIX);
+        messageDigest.update(lockScreenHash);
+        return messageDigest.digest();
+    }
+
+    /**
+     * Encrypts the recovery key using the lock screen hash.
+     *
+     * @param lockScreenHash The raw lock screen hash.
+     * @param recoveryKey The recovery key.
+     * @return The encrypted bytes.
+     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+     * @throws InvalidKeyException if the hash cannot be used to encrypt for some reason.
+     */
+    private static byte[] locallyEncryptRecoveryKey(byte[] lockScreenHash, SecretKey recoveryKey)
+            throws NoSuchAlgorithmException, InvalidKeyException {
+        return SecureBox.encrypt(
+                /*theirPublicKey=*/ null,
+                /*sharedSecret=*/ lockScreenHash,
+                /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
+                /*payload=*/ recoveryKey.getEncoded());
+    }
+
+    /**
+     * Returns a new random 256-bit AES recovery key.
+     *
+     * @hide
+     */
+    public static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
+        KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
+        keyGenerator.init(RECOVERY_KEY_SIZE_BITS, SecureRandom.getInstanceStrong());
+        return keyGenerator.generateKey();
+    }
+
+    /**
+     * Encrypts all of the given keys with the recovery key, using SecureBox.
+     *
+     * @param recoveryKey The recovery key.
+     * @param keys The keys, indexed by their aliases.
+     * @return The encrypted key material, indexed by aliases.
+     * @throws NoSuchAlgorithmException if any of the SecureBox algorithms are unavailable.
+     * @throws InvalidKeyException if the recovery key is not appropriate for encrypting the keys.
+     *
+     * @hide
+     */
+    public static Map<String, byte[]> encryptKeysWithRecoveryKey(
+            SecretKey recoveryKey, Map<String, SecretKey> keys)
+            throws NoSuchAlgorithmException, InvalidKeyException {
+        HashMap<String, byte[]> encryptedKeys = new HashMap<>();
+        for (String alias : keys.keySet()) {
+            SecretKey key = keys.get(alias);
+            byte[] encryptedKey = SecureBox.encrypt(
+                    /*theirPublicKey=*/ null,
+                    /*sharedSecret=*/ recoveryKey.getEncoded(),
+                    /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
+                    /*payload=*/ key.getEncoded());
+            encryptedKeys.put(alias, encryptedKey);
+        }
+        return encryptedKeys;
+    }
+
+    /**
+     * Returns a new array, the contents of which are the concatenation of {@code a} and {@code b}.
+     */
+    private static byte[] concat(byte[] a, byte[] b) {
+        byte[] result = new byte[a.length + b.length];
+        System.arraycopy(a, 0, result, 0, a.length);
+        System.arraycopy(b, 0, result, a.length, b.length);
+        return result;
+    }
+
+    // Statics only
+    private KeySyncUtils() {}
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
new file mode 100644 (file)
index 0000000..457fdc1
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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 java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+
+/**
+ * TODO(b/69056040) Add implementation of SecureBox. This is a placeholder so KeySyncUtils compiles.
+ *
+ * @hide
+ */
+public class SecureBox {
+    /**
+     * TODO(b/69056040) Add implementation of encrypt.
+     *
+     * @hide
+     */
+    public static byte[] encrypt(
+            PublicKey theirPublicKey, byte[] sharedSecret, byte[] header, byte[] payload)
+            throws NoSuchAlgorithmException, InvalidKeyException {
+        throw new UnsupportedOperationException("Needs to be implemented.");
+    }
+}
index c97c66e..9002292 100644 (file)
 
 package com.android.server.locksettings.recoverablekeystore;
 
+import android.util.Log;
+
+import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 
 import javax.crypto.Cipher;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
 
 /**
  * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
@@ -31,7 +38,11 @@ import javax.crypto.SecretKey;
  * @hide
  */
 public class WrappedKey {
+    private static final String TAG = "WrappedKey";
+
     private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
+    private static final String APPLICATION_KEY_ALGORITHM = "AES";
+    private static final int GCM_TAG_LENGTH_BITS = 128;
 
     private final byte[] mNonce;
     private final byte[] mKeyMaterial;
@@ -112,4 +123,54 @@ public class WrappedKey {
     public byte[] getKeyMaterial() {
         return mKeyMaterial;
     }
+
+    /**
+     * 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.
+     *
+     * @hide
+     */
+    public static Map<String, SecretKey> unwrapKeys(
+            SecretKey platformKey,
+            Map<String, WrappedKey> wrappedKeys)
+            throws NoSuchAlgorithmException, NoSuchPaddingException {
+        HashMap<String, SecretKey> unwrappedKeys = new HashMap<>();
+        Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
+
+        for (String alias : wrappedKeys.keySet()) {
+            WrappedKey wrappedKey = wrappedKeys.get(alias);
+            try {
+                cipher.init(
+                        Cipher.UNWRAP_MODE,
+                        platformKey,
+                        new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
+            } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+                Log.e(TAG,
+                        String.format(
+                                Locale.US,
+                                "Could not init Cipher to unwrap recoverable key with alias '%s'",
+                                alias),
+                        e);
+                continue;
+            }
+            SecretKey key;
+            try {
+                key = (SecretKey) cipher.unwrap(
+                        wrappedKey.getKeyMaterial(), APPLICATION_KEY_ALGORITHM, Cipher.SECRET_KEY);
+            } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+                Log.e(TAG,
+                        String.format(
+                                Locale.US,
+                                "Error unwrapping recoverable key with alias '%s'",
+                                alias),
+                        e);
+                continue;
+            }
+            unwrappedKeys.put(alias, key);
+        }
+
+        return unwrappedKeys;
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
new file mode 100644 (file)
index 0000000..c918e8c
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+import javax.crypto.SecretKey;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class KeySyncUtilsTest {
+    private static final int RECOVERY_KEY_LENGTH_BITS = 256;
+    private static final int THM_KF_HASH_SIZE = 256;
+    private static final String SHA_256_ALGORITHM = "SHA-256";
+
+    @Test
+    public void calculateThmKfHash_isShaOfLockScreenHashWithPrefix() throws Exception {
+        byte[] lockScreenHash = utf8Bytes("012345678910");
+
+        byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(lockScreenHash);
+
+        assertArrayEquals(calculateSha256(utf8Bytes("THM_KF_hash012345678910")), thmKfHash);
+    }
+
+    @Test
+    public void calculateThmKfHash_is256BitsLong() throws Exception {
+        byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(utf8Bytes("1234"));
+
+        assertEquals(THM_KF_HASH_SIZE / Byte.SIZE, thmKfHash.length);
+    }
+
+    @Test
+    public void generateRecoveryKey_returnsA256BitKey() throws Exception {
+        SecretKey key = KeySyncUtils.generateRecoveryKey();
+
+        assertEquals(RECOVERY_KEY_LENGTH_BITS / Byte.SIZE, key.getEncoded().length);
+    }
+
+    @Test
+    public void generateRecoveryKey_generatesANewKeyEachTime() throws Exception {
+        SecretKey a = KeySyncUtils.generateRecoveryKey();
+        SecretKey b = KeySyncUtils.generateRecoveryKey();
+
+        assertFalse(Arrays.equals(a.getEncoded(), b.getEncoded()));
+    }
+
+    private static byte[] utf8Bytes(String s) {
+        return s.getBytes(StandardCharsets.UTF_8);
+    }
+
+    private static byte[] calculateSha256(byte[] bytes) throws Exception {
+        MessageDigest messageDigest = MessageDigest.getInstance(SHA_256_ALGORITHM);
+        messageDigest.update(bytes);
+        return messageDigest.digest();
+    }
+}
index 4cd5631..fa73722 100644 (file)
@@ -16,7 +16,9 @@
 
 package com.android.server.locksettings.recoverablekeystore;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.security.keystore.AndroidKeyStoreSecretKey;
 import android.security.keystore.KeyGenParameterSpec;
@@ -29,6 +31,8 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.security.KeyStore;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.crypto.Cipher;
 import javax.crypto.KeyGenerator;
@@ -70,6 +74,36 @@ public class WrappedKeyTest {
         assertEquals(rawKey, unwrappedKey);
     }
 
+    @Test
+    public void decryptWrappedKeys_decryptsWrappedKeys() throws Exception {
+        String alias = "karlin";
+        SecretKey platformKey = generateAndroidKeyStoreKey();
+        SecretKey appKey = generateKey();
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, appKey);
+        HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
+        keysByAlias.put(alias, wrappedKey);
+
+        Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(platformKey, keysByAlias);
+
+        assertEquals(1, unwrappedKeys.size());
+        assertTrue(unwrappedKeys.containsKey(alias));
+        assertArrayEquals(appKey.getEncoded(), unwrappedKeys.get(alias).getEncoded());
+    }
+
+    @Test
+    public void decryptWrappedKeys_doesNotDieIfSomeKeysAreUnwrappable() throws Exception {
+        String alias = "karlin";
+        SecretKey appKey = generateKey();
+        WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), appKey);
+        HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
+        keysByAlias.put(alias, wrappedKey);
+
+        Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
+                generateAndroidKeyStoreKey(), keysByAlias);
+
+        assertEquals(0, unwrappedKeys.size());
+    }
+
     private SecretKey generateKey() throws Exception {
         KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
         keyGenerator.init(/*keySize=*/ 256);
@@ -81,7 +115,8 @@ public class WrappedKeyTest {
                 KEY_ALGORITHM,
                 ANDROID_KEY_STORE_PROVIDER);
         keyGenerator.init(new KeyGenParameterSpec.Builder(
-                WRAPPING_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                WRAPPING_KEY_ALIAS,
+                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                 .build());