Loading services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * 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() {} } services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java 0 → 100644 +39 −0 Original line number Diff line number Diff line /* * 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."); } } services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java +61 −0 Original line number Diff line number Diff line Loading @@ -16,14 +16,21 @@ 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. Loading @@ -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; Loading Loading @@ -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; } } services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java 0 → 100644 +82 −0 Original line number Diff line number Diff line /* * 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(); } } services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java +36 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading @@ -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()); Loading Loading
services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * 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() {} }
services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java 0 → 100644 +39 −0 Original line number Diff line number Diff line /* * 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."); } }
services/core/java/com/android/server/locksettings/recoverablekeystore/WrappedKey.java +61 −0 Original line number Diff line number Diff line Loading @@ -16,14 +16,21 @@ 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. Loading @@ -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; Loading Loading @@ -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; } }
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java 0 → 100644 +82 −0 Original line number Diff line number Diff line /* * 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(); } }
services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/WrappedKeyTest.java +36 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading @@ -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()); Loading