Merge "Add PlatformEncryptionKey (again)"
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Private key stored in AndroidKeyStore. Used to wrap recoverable keys before writing them to disk.
|
||||
*
|
||||
* <p>Identified by a generation ID, which increments whenever a new platform key is generated. A
|
||||
* new key must be generated whenever the user disables their lock screen, as the decryption key is
|
||||
* tied to lock-screen authentication.
|
||||
*
|
||||
* <p>One current platform key exists per profile on the device. (As each must be tied to a
|
||||
* different user's lock screen.)
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class PlatformEncryptionKey {
|
||||
|
||||
private final int mGenerationId;
|
||||
private final AndroidKeyStoreSecretKey mKey;
|
||||
|
||||
/**
|
||||
* A new instance.
|
||||
*
|
||||
* @param generationId The generation ID of the key.
|
||||
* @param key The secret key handle. Can be used to encrypt WITHOUT requiring screen unlock.
|
||||
*/
|
||||
public PlatformEncryptionKey(int generationId, AndroidKeyStoreSecretKey key) {
|
||||
mGenerationId = generationId;
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generation ID of the key.
|
||||
*/
|
||||
public int getGenerationId() {
|
||||
return mGenerationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual key, which can only be used to encrypt.
|
||||
*/
|
||||
public AndroidKeyStoreSecretKey getKey() {
|
||||
return mKey;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class RecoverableKeyGenerator {
|
||||
* @hide
|
||||
*/
|
||||
public static RecoverableKeyGenerator newInstance(
|
||||
AndroidKeyStoreSecretKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
|
||||
PlatformEncryptionKey platformKey, RecoverableKeyStorage recoverableKeyStorage)
|
||||
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.
|
||||
@@ -66,11 +66,11 @@ public class RecoverableKeyGenerator {
|
||||
|
||||
private final KeyGenerator mKeyGenerator;
|
||||
private final RecoverableKeyStorage mRecoverableKeyStorage;
|
||||
private final AndroidKeyStoreSecretKey mPlatformKey;
|
||||
private final PlatformEncryptionKey mPlatformKey;
|
||||
|
||||
private RecoverableKeyGenerator(
|
||||
KeyGenerator keyGenerator,
|
||||
AndroidKeyStoreSecretKey platformKey,
|
||||
PlatformEncryptionKey platformKey,
|
||||
RecoverableKeyStorage recoverableKeyStorage) {
|
||||
mKeyGenerator = keyGenerator;
|
||||
mRecoverableKeyStorage = recoverableKeyStorage;
|
||||
|
||||
@@ -44,6 +44,7 @@ public class WrappedKey {
|
||||
private static final String APPLICATION_KEY_ALGORITHM = "AES";
|
||||
private static final int GCM_TAG_LENGTH_BITS = 128;
|
||||
|
||||
private final int mPlatformKeyGenerationId;
|
||||
private final byte[] mNonce;
|
||||
private final byte[] mKeyMaterial;
|
||||
|
||||
@@ -55,8 +56,8 @@ public class WrappedKey {
|
||||
* {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
|
||||
* not expose its key material.
|
||||
*/
|
||||
public static WrappedKey fromSecretKey(
|
||||
SecretKey wrappingKey, SecretKey key) throws InvalidKeyException, KeyStoreException {
|
||||
public static WrappedKey fromSecretKey(PlatformEncryptionKey wrappingKey, SecretKey key)
|
||||
throws InvalidKeyException, KeyStoreException {
|
||||
if (key.getEncoded() == null) {
|
||||
throw new InvalidKeyException(
|
||||
"key does not expose encoded material. It cannot be wrapped.");
|
||||
@@ -70,7 +71,7 @@ public class WrappedKey {
|
||||
"Android does not support AES/GCM/NoPadding. This should never happen.");
|
||||
}
|
||||
|
||||
cipher.init(Cipher.WRAP_MODE, wrappingKey);
|
||||
cipher.init(Cipher.WRAP_MODE, wrappingKey.getKey());
|
||||
byte[] encryptedKeyMaterial;
|
||||
try {
|
||||
encryptedKeyMaterial = cipher.wrap(key);
|
||||
@@ -90,7 +91,10 @@ public class WrappedKey {
|
||||
}
|
||||
}
|
||||
|
||||
return new WrappedKey(/*mNonce=*/ cipher.getIV(), /*mKeyMaterial=*/ encryptedKeyMaterial);
|
||||
return new WrappedKey(
|
||||
/*nonce=*/ cipher.getIV(),
|
||||
/*keyMaterial=*/ encryptedKeyMaterial,
|
||||
/*platformKeyGenerationId=*/ wrappingKey.getGenerationId());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,12 +102,14 @@ public class WrappedKey {
|
||||
*
|
||||
* @param nonce The nonce with which the key material was encrypted.
|
||||
* @param keyMaterial The encrypted bytes of the key material.
|
||||
* @param platformKeyGenerationId The generation ID of the key used to wrap this key.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public WrappedKey(byte[] nonce, byte[] keyMaterial) {
|
||||
public WrappedKey(byte[] nonce, byte[] keyMaterial, int platformKeyGenerationId) {
|
||||
mNonce = nonce;
|
||||
mKeyMaterial = keyMaterial;
|
||||
mPlatformKeyGenerationId = platformKeyGenerationId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,8 +137,7 @@ public class WrappedKey {
|
||||
* @hide
|
||||
*/
|
||||
public int getPlatformKeyGenerationId() {
|
||||
// TODO(robertberry) Implement. See ag/3362855.
|
||||
return 1;
|
||||
return mPlatformKeyGenerationId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,13 +62,12 @@ public class RecoverableKeyStoreDb {
|
||||
*
|
||||
* @param uid Uid of the application to whom the key belongs.
|
||||
* @param alias The alias of the key in the AndroidKeyStore.
|
||||
* @param wrappedKey The wrapped bytes of the key.
|
||||
* @param generationId The generation ID of the platform key that wrapped the key.
|
||||
* @param wrappedKey The wrapped key.
|
||||
* @return The primary key of the inserted row, or -1 if failed.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public long insertKey(int uid, String alias, WrappedKey wrappedKey, int generationId) {
|
||||
public long insertKey(int uid, String alias, WrappedKey wrappedKey) {
|
||||
SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KeysEntry.COLUMN_NAME_UID, uid);
|
||||
@@ -76,7 +75,7 @@ public class RecoverableKeyStoreDb {
|
||||
values.put(KeysEntry.COLUMN_NAME_NONCE, wrappedKey.getNonce());
|
||||
values.put(KeysEntry.COLUMN_NAME_WRAPPED_KEY, wrappedKey.getKeyMaterial());
|
||||
values.put(KeysEntry.COLUMN_NAME_LAST_SYNCED_AT, -1);
|
||||
values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, generationId);
|
||||
values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, wrappedKey.getPlatformKeyGenerationId());
|
||||
return db.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
|
||||
}
|
||||
|
||||
@@ -123,7 +122,9 @@ public class RecoverableKeyStoreDb {
|
||||
cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
|
||||
byte[] keyMaterial = cursor.getBlob(
|
||||
cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
|
||||
return new WrappedKey(nonce, keyMaterial);
|
||||
int generationId = cursor.getInt(
|
||||
cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_GENERATION_ID));
|
||||
return new WrappedKey(nonce, keyMaterial, generationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +169,7 @@ public class RecoverableKeyStoreDb {
|
||||
cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
|
||||
String alias = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
|
||||
keys.put(alias, new WrappedKey(nonce, keyMaterial));
|
||||
keys.put(alias, new WrappedKey(nonce, keyMaterial, platformKeyGenerationId));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import javax.crypto.SecretKey;
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RecoverableKeyGeneratorTest {
|
||||
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 TEST_ALIAS = "karlin";
|
||||
@@ -58,14 +59,14 @@ public class RecoverableKeyGeneratorTest {
|
||||
|
||||
@Captor ArgumentCaptor<KeyProtection> mKeyProtectionArgumentCaptor;
|
||||
|
||||
private AndroidKeyStoreSecretKey mPlatformKey;
|
||||
private PlatformEncryptionKey mPlatformKey;
|
||||
private SecretKey mKeyHandle;
|
||||
private RecoverableKeyGenerator mRecoverableKeyGenerator;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mPlatformKey = generateAndroidKeyStoreKey();
|
||||
mPlatformKey = new PlatformEncryptionKey(TEST_GENERATION_ID, generateAndroidKeyStoreKey());
|
||||
mKeyHandle = generateKey();
|
||||
mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(
|
||||
mPlatformKey, mRecoverableKeyStorage);
|
||||
|
||||
@@ -61,7 +61,8 @@ public class WrappedKeyTest {
|
||||
|
||||
@Test
|
||||
public void fromSecretKey_createsWrappedKeyThatCanBeUnwrapped() throws Exception {
|
||||
SecretKey wrappingKey = generateAndroidKeyStoreKey();
|
||||
PlatformEncryptionKey wrappingKey = new PlatformEncryptionKey(
|
||||
GENERATION_ID, generateAndroidKeyStoreKey());
|
||||
SecretKey rawKey = generateKey();
|
||||
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(wrappingKey, rawKey);
|
||||
@@ -69,23 +70,36 @@ public class WrappedKeyTest {
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
|
||||
cipher.init(
|
||||
Cipher.UNWRAP_MODE,
|
||||
wrappingKey,
|
||||
wrappingKey.getKey(),
|
||||
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
|
||||
SecretKey unwrappedKey = (SecretKey) cipher.unwrap(
|
||||
wrappedKey.getKeyMaterial(), KEY_ALGORITHM, Cipher.SECRET_KEY);
|
||||
assertEquals(rawKey, unwrappedKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromSecretKey_returnsAKeyWithTheGenerationIdOfTheWrappingKey() throws Exception {
|
||||
PlatformEncryptionKey wrappingKey = new PlatformEncryptionKey(
|
||||
GENERATION_ID, generateAndroidKeyStoreKey());
|
||||
SecretKey rawKey = generateKey();
|
||||
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(wrappingKey, rawKey);
|
||||
|
||||
assertEquals(GENERATION_ID, wrappedKey.getPlatformKeyGenerationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptWrappedKeys_decryptsWrappedKeys() throws Exception {
|
||||
String alias = "karlin";
|
||||
PlatformDecryptionKey platformKey = generatePlatformDecryptionKey();
|
||||
AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
|
||||
SecretKey appKey = generateKey();
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey.getKey(), appKey);
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(
|
||||
new PlatformEncryptionKey(GENERATION_ID, platformKey), appKey);
|
||||
HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
|
||||
keysByAlias.put(alias, wrappedKey);
|
||||
|
||||
Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(platformKey, keysByAlias);
|
||||
Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
|
||||
new PlatformDecryptionKey(GENERATION_ID, platformKey), keysByAlias);
|
||||
|
||||
assertEquals(1, unwrappedKeys.size());
|
||||
assertTrue(unwrappedKeys.containsKey(alias));
|
||||
@@ -95,26 +109,32 @@ public class WrappedKeyTest {
|
||||
@Test
|
||||
public void decryptWrappedKeys_doesNotDieIfSomeKeysAreUnwrappable() throws Exception {
|
||||
String alias = "karlin";
|
||||
AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
|
||||
SecretKey appKey = generateKey();
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), appKey);
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(
|
||||
new PlatformEncryptionKey(GENERATION_ID, platformKey), appKey);
|
||||
HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
|
||||
keysByAlias.put(alias, wrappedKey);
|
||||
|
||||
Map<String, SecretKey> unwrappedKeys = WrappedKey.unwrapKeys(
|
||||
generatePlatformDecryptionKey(), keysByAlias);
|
||||
new PlatformDecryptionKey(GENERATION_ID, generateAndroidKeyStoreKey()),
|
||||
keysByAlias);
|
||||
|
||||
assertEquals(0, unwrappedKeys.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptWrappedKeys_throwsIfPlatformKeyGenerationIdDoesNotMatch() throws Exception {
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(generateKey(), generateKey());
|
||||
AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
|
||||
WrappedKey wrappedKey = WrappedKey.fromSecretKey(
|
||||
new PlatformEncryptionKey(GENERATION_ID, platformKey), generateKey());
|
||||
HashMap<String, WrappedKey> keysByAlias = new HashMap<>();
|
||||
keysByAlias.put("benji", wrappedKey);
|
||||
|
||||
try {
|
||||
WrappedKey.unwrapKeys(
|
||||
generatePlatformDecryptionKey(/*generationId=*/ 2), keysByAlias);
|
||||
new PlatformDecryptionKey(/*generationId=*/ 2, platformKey),
|
||||
keysByAlias);
|
||||
fail("Should have thrown.");
|
||||
} catch (BadPlatformKeyException e) {
|
||||
assertEquals(
|
||||
|
||||
@@ -63,19 +63,23 @@ public class RecoverableKeyStoreDbTest {
|
||||
int userId = 12;
|
||||
String alias = "test";
|
||||
WrappedKey oldWrappedKey = new WrappedKey(
|
||||
getUtf8Bytes("nonce1"), getUtf8Bytes("keymaterial1"));
|
||||
getUtf8Bytes("nonce1"),
|
||||
getUtf8Bytes("keymaterial1"),
|
||||
/*platformKeyGenerationId=*/1);
|
||||
mRecoverableKeyStoreDb.insertKey(
|
||||
userId, alias, oldWrappedKey, /*generationId=*/ 1);
|
||||
userId, alias, oldWrappedKey);
|
||||
byte[] nonce = getUtf8Bytes("nonce2");
|
||||
byte[] keyMaterial = getUtf8Bytes("keymaterial2");
|
||||
WrappedKey newWrappedKey = new WrappedKey(nonce, keyMaterial);
|
||||
WrappedKey newWrappedKey = new WrappedKey(
|
||||
nonce, keyMaterial, /*platformKeyGenerationId=*/2);
|
||||
|
||||
mRecoverableKeyStoreDb.insertKey(
|
||||
userId, alias, newWrappedKey, /*generationId=*/ 2);
|
||||
userId, alias, newWrappedKey);
|
||||
|
||||
WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
|
||||
assertArrayEquals(nonce, retrievedKey.getNonce());
|
||||
assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
|
||||
assertEquals(2, retrievedKey.getPlatformKeyGenerationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,13 +97,14 @@ public class RecoverableKeyStoreDbTest {
|
||||
String alias = "test";
|
||||
byte[] nonce = getUtf8Bytes("nonce");
|
||||
byte[] keyMaterial = getUtf8Bytes("keymaterial");
|
||||
WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
|
||||
mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
|
||||
WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial, generationId);
|
||||
mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey);
|
||||
|
||||
WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
|
||||
|
||||
assertArrayEquals(nonce, retrievedKey.getNonce());
|
||||
assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
|
||||
assertEquals(generationId, retrievedKey.getPlatformKeyGenerationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,8 +114,8 @@ public class RecoverableKeyStoreDbTest {
|
||||
String alias = "test";
|
||||
byte[] nonce = getUtf8Bytes("nonce");
|
||||
byte[] keyMaterial = getUtf8Bytes("keymaterial");
|
||||
WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
|
||||
mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
|
||||
WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial, generationId);
|
||||
mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey);
|
||||
|
||||
Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(userId, generationId);
|
||||
|
||||
@@ -119,15 +124,18 @@ public class RecoverableKeyStoreDbTest {
|
||||
WrappedKey retrievedKey = keys.get(alias);
|
||||
assertArrayEquals(nonce, retrievedKey.getNonce());
|
||||
assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
|
||||
assertEquals(generationId, retrievedKey.getPlatformKeyGenerationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllKeys_doesNotReturnKeysWithBadGenerationId() {
|
||||
int userId = 12;
|
||||
WrappedKey wrappedKey = new WrappedKey(
|
||||
getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
|
||||
getUtf8Bytes("nonce"),
|
||||
getUtf8Bytes("keymaterial"),
|
||||
/*platformKeyGenerationId=*/ 5);
|
||||
mRecoverableKeyStoreDb.insertKey(
|
||||
userId, /*alias=*/ "test", wrappedKey, /*generationId=*/ 5);
|
||||
userId, /*alias=*/ "test", wrappedKey);
|
||||
|
||||
Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
|
||||
userId, /*generationId=*/ 7);
|
||||
@@ -139,9 +147,9 @@ public class RecoverableKeyStoreDbTest {
|
||||
public void getAllKeys_doesNotReturnKeysWithBadUserId() {
|
||||
int generationId = 12;
|
||||
WrappedKey wrappedKey = new WrappedKey(
|
||||
getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
|
||||
getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"), generationId);
|
||||
mRecoverableKeyStoreDb.insertKey(
|
||||
/*userId=*/ 1, /*alias=*/ "test", wrappedKey, generationId);
|
||||
/*userId=*/ 1, /*alias=*/ "test", wrappedKey);
|
||||
|
||||
Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
|
||||
/*userId=*/ 2, generationId);
|
||||
|
||||
Reference in New Issue
Block a user