Add an optional metadata blob for recoverable application keys
This metadata, if present, will be authenticated (but unencrypted)
together with the application key material.
Bug: 112191661
Test: atest FrameworksCoreTests:android.security.keystore.recovery
atest FrameworksServicesTests:com.android.server.locksettings.recoverablekeystore
atest -m RecoveryControllerHostTest RecoverableKeyStoreEndtoEndHostTest RecoverySessionHostTest
Change-Id: I2846952758a2c1a7b1f0849e1adda1f05a3e305e
diff --git a/core/java/android/security/keystore/recovery/RecoveryController.java b/core/java/android/security/keystore/recovery/RecoveryController.java
index 31a5962..c43a666 100644
--- a/core/java/android/security/keystore/recovery/RecoveryController.java
+++ b/core/java/android/security/keystore/recovery/RecoveryController.java
@@ -533,7 +533,10 @@
* service.
* @throws LockScreenRequiredException if the user does not have a lock screen set. A lock
* screen is required to generate recoverable keys.
+ *
+ * @deprecated Use the method {@link #generateKey(String, byte[])} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
public @NonNull Key generateKey(@NonNull String alias) throws InternalRecoveryServiceException,
LockScreenRequiredException {
@@ -556,6 +559,47 @@
}
/**
+ * Generates a recoverable key with the given {@code alias} and {@code metadata}.
+ *
+ * <p>The metadata should contain any data that needs to be cryptographically bound to the
+ * generated key, but does not need to be encrypted by the key. For example, the metadata can
+ * be a byte string describing the algorithms and non-secret parameters to be used with the
+ * key. The supplied metadata can later be obtained via
+ * {@link WrappedApplicationKey#getMetadata()}.
+ *
+ * <p>During the key recovery process, the same metadata has to be supplied via
+ * {@link WrappedApplicationKey.Builder#setMetadata(byte[])}; otherwise, the recovery process
+ * will fail due to the checking of the cryptographic binding. This can help prevent
+ * potential attacks that try to swap key materials on the backup server and trick the
+ * application to use keys with different algorithms or parameters.
+ *
+ * @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.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public @NonNull Key generateKey(@NonNull String alias, @Nullable byte[] metadata)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ String grantAlias = mBinder.generateKeyWithMetadata(alias, metadata);
+ if (grantAlias == null) {
+ throw new InternalRecoveryServiceException("null grant alias");
+ }
+ return getKeyFromGrant(grantAlias);
+ } 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);
+ }
+ }
+
+ /**
* Imports a 256-bit recoverable AES key with the given {@code alias} and the raw bytes {@code
* keyBytes}.
*
@@ -564,7 +608,9 @@
* @throws LockScreenRequiredException if the user does not have a lock screen set. A lock
* screen is required to generate recoverable keys.
*
+ * @deprecated Use the method {@link #importKey(String, byte[], byte[])} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
public @NonNull Key importKey(@NonNull String alias, @NonNull byte[] keyBytes)
throws InternalRecoveryServiceException, LockScreenRequiredException {
@@ -587,6 +633,49 @@
}
/**
+ * Imports a recoverable 256-bit AES key with the given {@code alias}, the raw bytes {@code
+ * keyBytes}, and the {@code metadata}.
+ *
+ * <p>The metadata should contain any data that needs to be cryptographically bound to the
+ * imported key, but does not need to be encrypted by the key. For example, the metadata can
+ * be a byte string describing the algorithms and non-secret parameters to be used with the
+ * key. The supplied metadata can later be obtained via
+ * {@link WrappedApplicationKey#getMetadata()}.
+ *
+ * <p>During the key recovery process, the same metadata has to be supplied via
+ * {@link WrappedApplicationKey.Builder#setMetadata(byte[])}; otherwise, the recovery process
+ * will fail due to the checking of the cryptographic binding. This can help prevent
+ * potential attacks that try to swap key materials on the backup server and trick the
+ * application to use keys with different algorithms or parameters.
+ *
+ * @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.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public @NonNull Key importKey(@NonNull String alias, @NonNull byte[] keyBytes,
+ @Nullable byte[] metadata)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ String grantAlias = mBinder.importKeyWithMetadata(alias, keyBytes, metadata);
+ if (grantAlias == null) {
+ throw new InternalRecoveryServiceException("Null grant alias");
+ }
+ return getKeyFromGrant(grantAlias);
+ } 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/android/security/keystore/recovery/WrappedApplicationKey.java b/core/java/android/security/keystore/recovery/WrappedApplicationKey.java
index ae4448f..dbfd655 100644
--- a/core/java/android/security/keystore/recovery/WrappedApplicationKey.java
+++ b/core/java/android/security/keystore/recovery/WrappedApplicationKey.java
@@ -17,6 +17,7 @@
package android.security.keystore.recovery;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -41,6 +42,8 @@
private String mAlias;
// The only supported format is AES-256 symmetric key.
private byte[] mEncryptedKeyMaterial;
+ // The optional metadata that's authenticated (but unencrypted) with the key material.
+ private byte[] mMetadata;
// IMPORTANT! PLEASE READ!
// -----------------------
@@ -80,13 +83,23 @@
* @param encryptedKeyMaterial The key material
* @return This builder
*/
-
public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
return this;
}
/**
+ * Sets the metadata that is authenticated (but unecrypted) with the key material.
+ *
+ * @param metadata The metadata
+ * @return This builder
+ */
+ public Builder setMetadata(@Nullable byte[] metadata) {
+ mInstance.mMetadata = metadata;
+ return this;
+ }
+
+ /**
* Creates a new {@link WrappedApplicationKey} instance.
*
* @return new instance
@@ -102,9 +115,10 @@
private WrappedApplicationKey() { }
/**
- * Deprecated - consider using Builder.
+ * @deprecated Use the builder instead.
* @hide
*/
+ @Deprecated
public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
mAlias = Preconditions.checkNotNull(alias);
mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
@@ -124,6 +138,11 @@
return mEncryptedKeyMaterial;
}
+ /** The metadata with the key. */
+ public @Nullable byte[] getMetadata() {
+ return mMetadata;
+ }
+
public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
new Parcelable.Creator<WrappedApplicationKey>() {
public WrappedApplicationKey createFromParcel(Parcel in) {
@@ -139,6 +158,7 @@
public void writeToParcel(Parcel out, int flags) {
out.writeString(mAlias);
out.writeByteArray(mEncryptedKeyMaterial);
+ out.writeByteArray(mMetadata);
}
/**
@@ -147,6 +167,10 @@
protected WrappedApplicationKey(Parcel in) {
mAlias = in.readString();
mEncryptedKeyMaterial = in.createByteArray();
+ // Check if there is still data to be read.
+ if (in.dataAvail() > 0) {
+ mMetadata = in.createByteArray();
+ }
}
@Override
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index 591f15f..9a77802 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -62,7 +62,9 @@
in byte[] recoveryServiceCertFile, in byte[] recoveryServiceSigFile);
KeyChainSnapshot getKeyChainSnapshot();
String generateKey(String alias);
+ String generateKeyWithMetadata(String alias, in byte[] metadata);
String importKey(String alias, in byte[] keyBytes);
+ String importKeyWithMetadata(String alias, in byte[] keyBytes, in byte[] metadata);
String getKey(String alias);
void removeKey(String alias);
void setSnapshotCreatedPendingIntent(in PendingIntent intent);
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
index 61ab152..f78b0e1 100644
--- a/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
+++ b/core/tests/coretests/src/android/security/keystore/recovery/KeyChainSnapshotTest.java
@@ -44,6 +44,7 @@
private static final int USER_SECRET_TYPE = KeyChainProtectionParams.TYPE_LOCKSCREEN;
private static final String KEY_ALIAS = "steph";
private static final byte[] KEY_MATERIAL = new byte[] { 3, 5, 7, 9, 1 };
+ private static final byte[] KEY_METADATA = new byte[] { 5, 3, 11, 13 };
private static final CertPath CERT_PATH = TestData.getThmCertPath();
@Test
@@ -99,6 +100,7 @@
WrappedApplicationKey wrappedApplicationKey = snapshot.getWrappedApplicationKeys().get(0);
assertEquals(KEY_ALIAS, wrappedApplicationKey.getAlias());
assertArrayEquals(KEY_MATERIAL, wrappedApplicationKey.getEncryptedKeyMaterial());
+ assertArrayEquals(KEY_METADATA, wrappedApplicationKey.getMetadata());
}
@Test
@@ -165,6 +167,7 @@
WrappedApplicationKey wrappedApplicationKey = snapshot.getWrappedApplicationKeys().get(0);
assertEquals(KEY_ALIAS, wrappedApplicationKey.getAlias());
assertArrayEquals(KEY_MATERIAL, wrappedApplicationKey.getEncryptedKeyMaterial());
+ assertArrayEquals(KEY_METADATA, wrappedApplicationKey.getMetadata());
}
private static KeyChainSnapshot createKeyChainSnapshot() throws Exception {
@@ -196,6 +199,7 @@
return new WrappedApplicationKey.Builder()
.setAlias(KEY_ALIAS)
.setEncryptedKeyMaterial(KEY_MATERIAL)
+ .setMetadata(KEY_METADATA)
.build();
}
diff --git a/core/tests/coretests/src/android/security/keystore/recovery/WrappedApplicationKeyTest.java b/core/tests/coretests/src/android/security/keystore/recovery/WrappedApplicationKeyTest.java
index 15afbdd..fabc432 100644
--- a/core/tests/coretests/src/android/security/keystore/recovery/WrappedApplicationKeyTest.java
+++ b/core/tests/coretests/src/android/security/keystore/recovery/WrappedApplicationKeyTest.java
@@ -34,6 +34,7 @@
private static final String ALIAS = "karlin";
private static final byte[] KEY_MATERIAL = new byte[] { 0, 1, 2, 3, 4 };
+ private static final byte[] METADATA = new byte[] {3, 2, 1, 0};
private Parcel mParcel;
@@ -58,8 +59,18 @@
}
@Test
+ public void build_setsMetadata_nonNull() {
+ assertArrayEquals(METADATA, buildTestKeyWithMetadata(METADATA).getMetadata());
+ }
+
+ @Test
+ public void build_setsMetadata_null() {
+ assertArrayEquals(null, buildTestKeyWithMetadata(null).getMetadata());
+ }
+
+ @Test
public void writeToParcel_writesAliasToParcel() {
- buildTestKey().writeToParcel(mParcel, /*flags=*/ 0);
+ buildTestKeyWithMetadata(/*metadata=*/ null).writeToParcel(mParcel, /*flags=*/ 0);
mParcel.setDataPosition(0);
WrappedApplicationKey readFromParcel =
@@ -69,7 +80,7 @@
@Test
public void writeToParcel_writesKeyMaterial() {
- buildTestKey().writeToParcel(mParcel, /*flags=*/ 0);
+ buildTestKeyWithMetadata(/*metadata=*/ null).writeToParcel(mParcel, /*flags=*/ 0);
mParcel.setDataPosition(0);
WrappedApplicationKey readFromParcel =
@@ -77,10 +88,48 @@
assertArrayEquals(KEY_MATERIAL, readFromParcel.getEncryptedKeyMaterial());
}
+ @Test
+ public void writeToParcel_writesMetadata_nonNull() {
+ buildTestKeyWithMetadata(METADATA).writeToParcel(mParcel, /*flags=*/ 0);
+
+ mParcel.setDataPosition(0);
+ WrappedApplicationKey readFromParcel =
+ WrappedApplicationKey.CREATOR.createFromParcel(mParcel);
+ assertArrayEquals(METADATA, readFromParcel.getMetadata());
+ }
+
+ @Test
+ public void writeToParcel_writesMetadata_null() {
+ buildTestKeyWithMetadata(/*metadata=*/ null).writeToParcel(mParcel, /*flags=*/ 0);
+
+ mParcel.setDataPosition(0);
+ WrappedApplicationKey readFromParcel =
+ WrappedApplicationKey.CREATOR.createFromParcel(mParcel);
+ assertArrayEquals(null, readFromParcel.getMetadata());
+ }
+
+ @Test
+ public void writeToParcel_writesMetadata_absent() {
+ buildTestKey().writeToParcel(mParcel, /*flags=*/ 0);
+
+ mParcel.setDataPosition(0);
+ WrappedApplicationKey readFromParcel =
+ WrappedApplicationKey.CREATOR.createFromParcel(mParcel);
+ assertArrayEquals(null, readFromParcel.getMetadata());
+ }
+
private WrappedApplicationKey buildTestKey() {
return new WrappedApplicationKey.Builder()
.setAlias(ALIAS)
.setEncryptedKeyMaterial(KEY_MATERIAL)
.build();
}
+
+ private WrappedApplicationKey buildTestKeyWithMetadata(byte[] metadata) {
+ return new WrappedApplicationKey.Builder()
+ .setAlias(ALIAS)
+ .setEncryptedKeyMaterial(KEY_MATERIAL)
+ .setMetadata(metadata)
+ .build();
+ }
}