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();
+    }
 }