Add a new API to import a key provided by the caller, such that this key
can also be synced to the remote service
This API may be useful for backward-compatibility work, e.g., recovering
a key that's backed up in Android Q+ to Android P without updating the
Android P Frameworks code. This API may also be useful for other use cases.
Bug: 73785182
Change-Id: I1022dffb6a12bdf3df2022db5739169fcc9347d2
Test: adb shell am instrument -w -e package \
com.android.server.locksettings.recoverablekeystore \
com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
diff --git a/core/java/android/security/keystore/recovery/RecoveryController.java b/core/java/android/security/keystore/recovery/RecoveryController.java
index 0683e02..426ca5c 100644
--- a/core/java/android/security/keystore/recovery/RecoveryController.java
+++ b/core/java/android/security/keystore/recovery/RecoveryController.java
@@ -113,6 +113,14 @@
*/
public static final int ERROR_DECRYPTION_FAILED = 26;
+ /**
+ * Error thrown if the format of a given key is invalid. This might be because the key has a
+ * wrong length, invalid content, etc.
+ *
+ * @hide
+ */
+ public static final int ERROR_INVALID_KEY_FORMAT = 27;
+
private final ILockSettings mBinder;
private final KeyStore mKeyStore;
@@ -461,6 +469,7 @@
}
}
+ // TODO: Unhide the following APIs, generateKey(), importKey(), and getKey()
/**
* @deprecated Use {@link #generateKey(String)}.
* @removed
@@ -503,6 +512,40 @@
}
/**
+ * Imports a 256-bit recoverable AES key with the given {@code alias} and the raw bytes {@code
+ * keyBytes}.
+ *
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ * @throws LockScreenRequiredException if the user does not have a lock screen set. A lock
+ * screen is required to generate recoverable keys.
+ *
+ * @hide
+ */
+ public Key importKey(@NonNull String alias, byte[] keyBytes)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ String grantAlias = mBinder.importKey(alias, keyBytes);
+ if (grantAlias == null) {
+ throw new InternalRecoveryServiceException("Null grant alias");
+ }
+ return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(
+ mKeyStore,
+ grantAlias,
+ KeyStore.UID_SELF);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (UnrecoverableKeyException e) {
+ throw new InternalRecoveryServiceException("Failed to get key from keystore", e);
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_INSECURE_USER) {
+ throw new LockScreenRequiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
* Gets a key called {@code alias} from the recoverable key store.
*
* @param alias The key alias.
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index d3fc644..7c9cf7a 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -68,6 +68,7 @@
KeyChainSnapshot getKeyChainSnapshot();
byte[] generateAndStoreKey(String alias);
String generateKey(String alias);
+ String importKey(String alias, in byte[] keyBytes);
String getKey(String alias);
void removeKey(String alias);
void setSnapshotCreatedPendingIntent(in PendingIntent intent);
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 9e00819..752ab8f 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -2079,6 +2079,11 @@
}
@Override
+ public String importKey(@NonNull String alias, byte[] keyBytes) throws RemoteException {
+ return mRecoverableKeyStoreManager.importKey(alias, keyBytes);
+ }
+
+ @Override
public String getKey(@NonNull String alias) throws RemoteException {
return mRecoverableKeyStoreManager.getKey(alias);
}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
index 2fe3f4e..7ebe8bf 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
@@ -16,6 +16,8 @@
package com.android.server.locksettings.recoverablekeystore;
+import android.annotation.NonNull;
+
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import java.security.InvalidKeyException;
@@ -25,20 +27,24 @@
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+// TODO: Rename RecoverableKeyGenerator to RecoverableKeyManager as it can import a key too now
/**
- * Generates keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
+ * Generates/imports keys and stores them both in AndroidKeyStore and on disk, in wrapped form.
*
- * <p>Generates 256-bit AES keys, which can be used for encrypt / decrypt with AES/GCM/NoPadding.
+ * <p>Generates/imports 256-bit AES keys, which can be used for encrypt and decrypt with AES-GCM.
* They are synced to disk wrapped by a platform key. This allows them to be exported to a remote
* service.
*
* @hide
*/
public class RecoverableKeyGenerator {
+
private static final int RESULT_CANNOT_INSERT_ROW = -1;
- private static final String KEY_GENERATOR_ALGORITHM = "AES";
- private static final int KEY_SIZE_BITS = 256;
+ private static final String SECRET_KEY_ALGORITHM = "AES";
+
+ static final int KEY_SIZE_BITS = 256;
/**
* A new {@link RecoverableKeyGenerator} instance.
@@ -52,7 +58,7 @@
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.
- KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_GENERATOR_ALGORITHM);
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(SECRET_KEY_ALGORITHM);
return new RecoverableKeyGenerator(keyGenerator, database);
}
@@ -102,4 +108,41 @@
mDatabase.setShouldCreateSnapshot(userId, uid, true);
return key.getEncoded();
}
+
+ /**
+ * Imports an AES key with the given alias.
+ *
+ * <p>Stores in the AndroidKeyStore, as well as persisting in wrapped form to disk. It is
+ * persisted to disk so that it can be synced remotely, and then recovered on another device.
+ * The generated key allows encrypt/decrypt only using AES/GCM/NoPadding.
+ *
+ * @param platformKey The user's platform key, with which to wrap the generated key.
+ * @param userId The user ID of the profile to which the calling app belongs.
+ * @param uid The uid of the application that will own the key.
+ * @param alias The alias by which the key will be known in the recoverable key store.
+ * @param keyBytes The raw bytes of the AES key to be imported.
+ * @throws RecoverableKeyStorageException if there is some error persisting the key either to
+ * the database.
+ * @throws KeyStoreException if there is a KeyStore error wrapping the generated key.
+ * @throws InvalidKeyException if the platform key cannot be used to wrap keys.
+ *
+ * @hide
+ */
+ public void importKey(
+ @NonNull PlatformEncryptionKey platformKey, int userId, int uid, @NonNull String alias,
+ @NonNull byte[] keyBytes)
+ throws RecoverableKeyStorageException, KeyStoreException, InvalidKeyException {
+ SecretKey key = new SecretKeySpec(keyBytes, SECRET_KEY_ALGORITHM);
+
+ WrappedKey wrappedKey = WrappedKey.fromSecretKey(platformKey, key);
+ long result = mDatabase.insertKey(userId, uid, alias, wrappedKey);
+
+ if (result == RESULT_CANNOT_INSERT_ROW) {
+ throw new RecoverableKeyStorageException(
+ String.format(
+ Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
+ }
+
+ mDatabase.setShouldCreateSnapshot(userId, uid, true);
+ }
}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index 72f72eb..da0b0d0 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -19,6 +19,7 @@
import static android.security.keystore.recovery.RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT;
import static android.security.keystore.recovery.RecoveryController.ERROR_DECRYPTION_FAILED;
import static android.security.keystore.recovery.RecoveryController.ERROR_INSECURE_USER;
+import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_KEY_FORMAT;
import static android.security.keystore.recovery.RecoveryController.ERROR_NO_SNAPSHOT_PENDING;
import static android.security.keystore.recovery.RecoveryController.ERROR_SERVICE_INTERNAL_ERROR;
import static android.security.keystore.recovery.RecoveryController.ERROR_SESSION_EXPIRED;
@@ -505,6 +506,7 @@
*
* <p>TODO: Once AndroidKeyStore has added move api, do not return raw bytes.
*
+ * @deprecated
* @hide
*/
public byte[] generateAndStoreKey(@NonNull String alias) throws RemoteException {
@@ -581,6 +583,57 @@
}
/**
+ * Imports a 256-bit AES-GCM key named {@code alias}. The key is stored in system service
+ * keystore namespace.
+ *
+ * @param alias the alias provided by caller as a reference to the key.
+ * @param keyBytes the raw bytes of the 256-bit AES key.
+ * @return grant alias, which caller can use to access the key.
+ * @throws RemoteException if the given key is invalid or some internal errors occur.
+ *
+ * @hide
+ */
+ public String importKey(@NonNull String alias, @NonNull byte[] keyBytes)
+ throws RemoteException {
+ if (keyBytes == null ||
+ keyBytes.length != RecoverableKeyGenerator.KEY_SIZE_BITS / Byte.SIZE) {
+ Log.e(TAG, "The given key for import doesn't have the required length "
+ + RecoverableKeyGenerator.KEY_SIZE_BITS);
+ throw new ServiceSpecificException(ERROR_INVALID_KEY_FORMAT,
+ "The given key does not contain " + RecoverableKeyGenerator.KEY_SIZE_BITS
+ + " bits.");
+ }
+
+ int uid = Binder.getCallingUid();
+ int userId = UserHandle.getCallingUserId();
+
+ // TODO: Refactor RecoverableKeyGenerator to wrap the PlatformKey logic
+
+ PlatformEncryptionKey encryptionKey;
+ try {
+ encryptionKey = mPlatformKeyManager.getEncryptKey(userId);
+ } catch (NoSuchAlgorithmException e) {
+ // Impossible: all algorithms must be supported by AOSP
+ throw new RuntimeException(e);
+ } catch (KeyStoreException | UnrecoverableKeyException e) {
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ } catch (InsecureUserException e) {
+ throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage());
+ }
+
+ try {
+ // Wrap the key by the platform key and store the wrapped key locally
+ mRecoverableKeyGenerator.importKey(encryptionKey, userId, uid, alias, keyBytes);
+
+ // Import the key to Android KeyStore and get grant
+ mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keyBytes);
+ return mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
+ } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ }
+ }
+
+ /**
* Gets a key named {@code alias} in caller's namespace.
*
* @return grant alias, which caller can use to access the key.
@@ -630,14 +683,6 @@
}
}
- private String constructLoggingMessage(String key, byte[] value) {
- if (value == null) {
- return key + " is null";
- } else {
- return key + ": " + HexDump.toHexString(value);
- }
- }
-
/**
* Uses {@code recoveryKey} to decrypt {@code applicationKeys}.
*
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
index 8a461ac..fd8b319 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGeneratorTest.java
@@ -39,6 +39,7 @@
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
+import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
@@ -51,7 +52,7 @@
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 int KEY_SIZE_BYTES = 32;
+ private static final int KEY_SIZE_BYTES = RecoverableKeyGenerator.KEY_SIZE_BITS / Byte.SIZE;
private static final String KEY_WRAP_ALGORITHM = "AES/GCM/NoPadding";
private static final String TEST_ALIAS = "karlin";
private static final String WRAPPING_KEY_ALIAS = "RecoverableKeyGeneratorTestWrappingKey";
@@ -71,7 +72,7 @@
mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
- AndroidKeyStoreSecretKey platformKey = generateAndroidKeyStoreKey();
+ AndroidKeyStoreSecretKey platformKey = generatePlatformKey();
mPlatformKey = new PlatformEncryptionKey(TEST_GENERATION_ID, platformKey);
mDecryptKey = new PlatformDecryptionKey(TEST_GENERATION_ID, platformKey);
mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mRecoverableKeyStoreDb);
@@ -117,7 +118,21 @@
assertArrayEquals(rawMaterial, unwrappedMaterial);
}
- private AndroidKeyStoreSecretKey generateAndroidKeyStoreKey() throws Exception {
+ @Test
+ public void importKey_storesTheWrappedVersionOfTheRawMaterial() throws Exception {
+ byte[] rawMaterial = randomBytes(KEY_SIZE_BYTES);
+ mRecoverableKeyGenerator.importKey(
+ mPlatformKey, TEST_USER_ID, KEYSTORE_UID_SELF, TEST_ALIAS, rawMaterial);
+
+ WrappedKey wrappedKey = mRecoverableKeyStoreDb.getKey(KEYSTORE_UID_SELF, TEST_ALIAS);
+ Cipher cipher = Cipher.getInstance(KEY_WRAP_ALGORITHM);
+ cipher.init(Cipher.DECRYPT_MODE, mDecryptKey.getKey(),
+ new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
+ byte[] unwrappedMaterial = cipher.doFinal(wrappedKey.getKeyMaterial());
+ assertArrayEquals(rawMaterial, unwrappedMaterial);
+ }
+
+ private AndroidKeyStoreSecretKey generatePlatformKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KEY_ALGORITHM,
ANDROID_KEY_STORE_PROVIDER);
@@ -132,4 +147,10 @@
private static byte[] getUtf8Bytes(String s) {
return s.getBytes(StandardCharsets.UTF_8);
}
+
+ private static byte[] randomBytes(int n) {
+ byte[] bytes = new byte[n];
+ new Random().nextBytes(bytes);
+ return bytes;
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
index b67659d..199410c 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java
@@ -132,6 +132,7 @@
private static final String TEST_ALIAS = "nick";
private static final String TEST_ALIAS2 = "bob";
private static final int RECOVERABLE_KEY_SIZE_BYTES = 32;
+ private static final int APPLICATION_KEY_SIZE_BYTES = 32;
private static final int GENERATION_ID = 1;
private static final byte[] NONCE = getUtf8Bytes("nonce");
private static final byte[] KEY_MATERIAL = getUtf8Bytes("keymaterial");
@@ -209,6 +210,39 @@
}
@Test
+ public void importKey_storesTheKey() throws Exception {
+ int uid = Binder.getCallingUid();
+ int userId = UserHandle.getCallingUserId();
+ byte[] keyMaterial = randomBytes(APPLICATION_KEY_SIZE_BYTES);
+
+ mRecoverableKeyStoreManager.importKey(TEST_ALIAS, keyMaterial);
+
+ assertThat(mRecoverableKeyStoreDb.getKey(uid, TEST_ALIAS)).isNotNull();
+ assertThat(mRecoverableKeyStoreDb.getShouldCreateSnapshot(userId, uid)).isTrue();
+ }
+
+ @Test
+ public void importKey_throwsIfInvalidLength() throws Exception {
+ byte[] keyMaterial = randomBytes(APPLICATION_KEY_SIZE_BYTES - 1);
+ try {
+ mRecoverableKeyStoreManager.importKey(TEST_ALIAS, keyMaterial);
+ fail("should have thrown");
+ } catch (ServiceSpecificException e) {
+ assertThat(e.getMessage()).contains("not contain 256 bits");
+ }
+ }
+
+ @Test
+ public void importKey_throwsIfNullKey() throws Exception {
+ try {
+ mRecoverableKeyStoreManager.importKey(TEST_ALIAS, /*keyBytes=*/ null);
+ fail("should have thrown");
+ } catch (ServiceSpecificException e) {
+ assertThat(e.getMessage()).contains("not contain 256 bits");
+ }
+ }
+
+ @Test
public void removeKey_removesAKey() throws Exception {
int uid = Binder.getCallingUid();
mRecoverableKeyStoreManager.generateAndStoreKey(TEST_ALIAS);