--- /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 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() {}
+}
--- /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 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.");
+ }
+}
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.
* @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;
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;
+ }
}
--- /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.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();
+ }
+}
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;
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;
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);
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());