content: /*
   * 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 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_DOWNGRADE_CERTIFICATE;
  import static android.security.keystore.recovery.RecoveryController.ERROR_INSECURE_USER;
  import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE;
  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;
  
  import android.Manifest;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.PendingIntent;
  import android.content.Context;
  import android.os.Binder;
  import android.os.RemoteException;
  import android.os.ServiceSpecificException;
  import android.os.UserHandle;
  import android.security.KeyStore;
  import android.security.keystore.recovery.KeyChainProtectionParams;
  import android.security.keystore.recovery.KeyChainSnapshot;
  import android.security.keystore.recovery.RecoveryCertPath;
  import android.security.keystore.recovery.RecoveryController;
  import android.security.keystore.recovery.WrappedApplicationKey;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.HexDump;
  import com.android.internal.util.Preconditions;
  import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException;
  import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
  import com.android.server.locksettings.recoverablekeystore.certificate.CertValidationException;
  import com.android.server.locksettings.recoverablekeystore.certificate.CertXml;
  import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
  import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
  import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
  
  import java.io.IOException;
  import java.security.InvalidKeyException;
  import java.security.KeyStoreException;
  import java.security.NoSuchAlgorithmException;
  import java.security.PublicKey;
  import java.security.SecureRandom;
  import java.security.UnrecoverableKeyException;
  import java.security.cert.CertPath;
  import java.security.cert.CertificateEncodingException;
  import java.security.cert.CertificateException;
  import java.security.cert.X509Certificate;
  import java.security.spec.InvalidKeySpecException;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  
  import javax.crypto.AEADBadTagException;
  
  /**
   * Class with {@link RecoveryController} API implementation and internal methods to interact
   * with {@code LockSettingsService}.
   *
   * @hide
   */
  public class RecoverableKeyStoreManager {
      private static final String TAG = "RecoverableKeyStoreMgr";
  
      private static RecoverableKeyStoreManager mInstance;
  
      private final Context mContext;
      private final RecoverableKeyStoreDb mDatabase;
      private final RecoverySessionStorage mRecoverySessionStorage;
      private final ExecutorService mExecutorService;
      private final RecoverySnapshotListenersStorage mListenersStorage;
      private final RecoverableKeyGenerator mRecoverableKeyGenerator;
      private final RecoverySnapshotStorage mSnapshotStorage;
      private final PlatformKeyManager mPlatformKeyManager;
      private final ApplicationKeyStorage mApplicationKeyStorage;
      private final TestOnlyInsecureCertificateHelper mTestCertHelper;
      private final CleanupManager mCleanupManager;
  
      /**
       * Returns a new or existing instance.
       *
       * @hide
       */
      public static synchronized RecoverableKeyStoreManager
              getInstance(Context context, KeyStore keystore) {
          if (mInstance == null) {
              RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(context);
              PlatformKeyManager platformKeyManager;
              ApplicationKeyStorage applicationKeyStorage;
              try {
                  platformKeyManager = PlatformKeyManager.getInstance(context, db);
                  applicationKeyStorage = ApplicationKeyStorage.getInstance(keystore);
              } catch (NoSuchAlgorithmException e) {
                  // Impossible: all algorithms must be supported by AOSP
                  throw new RuntimeException(e);
              } catch (KeyStoreException e) {
                  throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
              }
  
              RecoverySnapshotStorage snapshotStorage =
                      RecoverySnapshotStorage.newInstance();
              CleanupManager cleanupManager = CleanupManager.getInstance(
                      context.getApplicationContext(),
                      snapshotStorage,
                      db,
                      applicationKeyStorage);
              mInstance = new RecoverableKeyStoreManager(
                      context.getApplicationContext(),
                      db,
                      new RecoverySessionStorage(),
                      Executors.newSingleThreadExecutor(),
                      snapshotStorage,
                      new RecoverySnapshotListenersStorage(),
                      platformKeyManager,
                      applicationKeyStorage,
                      new TestOnlyInsecureCertificateHelper(),
                      cleanupManager);
          }
          return mInstance;
      }
  
      @VisibleForTesting
      RecoverableKeyStoreManager(
              Context context,
              RecoverableKeyStoreDb recoverableKeyStoreDb,
              RecoverySessionStorage recoverySessionStorage,
              ExecutorService executorService,
              RecoverySnapshotStorage snapshotStorage,
              RecoverySnapshotListenersStorage listenersStorage,
              PlatformKeyManager platformKeyManager,
              ApplicationKeyStorage applicationKeyStorage,
              TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
              CleanupManager cleanupManager) {
          mContext = context;
          mDatabase = recoverableKeyStoreDb;
          mRecoverySessionStorage = recoverySessionStorage;
          mExecutorService = executorService;
          mListenersStorage = listenersStorage;
          mSnapshotStorage = snapshotStorage;
          mPlatformKeyManager = platformKeyManager;
          mApplicationKeyStorage = applicationKeyStorage;
          mTestCertHelper = testOnlyInsecureCertificateHelper;
          mCleanupManager = cleanupManager;
          // Clears data for removed users.
          mCleanupManager.verifyKnownUsers();
          try {
              mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
          } catch (NoSuchAlgorithmException e) {
              Log.wtf(TAG, "AES keygen algorithm not available. AOSP must support this.", e);
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          }
      }
  
      /**
       * Used by {@link #initRecoveryServiceWithSigFile(String, byte[], byte[])}.
       */
      @VisibleForTesting
      void initRecoveryService(
              @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          int userId = UserHandle.getCallingUserId();
          int uid = Binder.getCallingUid();
  
          rootCertificateAlias
                  = mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
          if (!mTestCertHelper.isValidRootCertificateAlias(rootCertificateAlias)) {
              throw new ServiceSpecificException(
                      ERROR_INVALID_CERTIFICATE, "Invalid root certificate alias");
          }
          // Always set active alias to the argument of the last call to initRecoveryService method,
          // even if cert file is incorrect.
          String activeRootAlias = mDatabase.getActiveRootOfTrust(userId, uid);
          if (activeRootAlias == null) {
              Log.d(TAG, "Root of trust for recovery agent + " + uid
                  + " is assigned for the first time to " + rootCertificateAlias);
          } else if (!activeRootAlias.equals(rootCertificateAlias)) {
              Log.i(TAG, "Root of trust for recovery agent " + uid + " is changed to "
                      + rootCertificateAlias + " from  " + activeRootAlias);
          }
          long updatedRows = mDatabase.setActiveRootOfTrust(userId, uid, rootCertificateAlias);
          if (updatedRows < 0) {
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
                      "Failed to set the root of trust in the local DB.");
          }
  
          CertXml certXml;
          try {
              certXml = CertXml.parse(recoveryServiceCertFile);
          } catch (CertParsingException e) {
              Log.d(TAG, "Failed to parse the input as a cert file: " + HexDump.toHexString(
                      recoveryServiceCertFile));
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
          }
  
          // Check serial number
          long newSerial = certXml.getSerial();
          Long oldSerial = mDatabase.getRecoveryServiceCertSerial(userId, uid, rootCertificateAlias);
          if (oldSerial != null && oldSerial >= newSerial
                  && !mTestCertHelper.isTestOnlyCertificateAlias(rootCertificateAlias)) {
              if (oldSerial == newSerial) {
                  Log.i(TAG, "The cert file serial number is the same, so skip updating.");
              } else {
                  Log.e(TAG, "The cert file serial number is older than the one in database.");
                  throw new ServiceSpecificException(ERROR_DOWNGRADE_CERTIFICATE,
                          "The cert file serial number is older than the one in database.");
              }
              return;
          }
          Log.i(TAG, "Updating the certificate with the new serial number " + newSerial);
  
          // Randomly choose and validate an endpoint certificate from the list
          CertPath certPath;
          X509Certificate rootCert =
                  mTestCertHelper.getRootCertificate(rootCertificateAlias);
          try {
              Log.d(TAG, "Getting and validating a random endpoint certificate");
              certPath = certXml.getRandomEndpointCert(rootCert);
          } catch (CertValidationException e) {
              Log.e(TAG, "Invalid endpoint cert", e);
              throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
          }
  
          // Save the chosen and validated certificate into database
          try {
              Log.d(TAG, "Saving the randomly chosen endpoint certificate to database");
              long updatedCertPathRows = mDatabase.setRecoveryServiceCertPath(userId, uid,
                      rootCertificateAlias, certPath);
              if (updatedCertPathRows > 0) {
                  long updatedCertSerialRows = mDatabase.setRecoveryServiceCertSerial(userId, uid,
                          rootCertificateAlias, newSerial);
                  if (updatedCertSerialRows < 0) {
                      // Ideally CertPath and CertSerial should be updated together in single
                      // transaction, but since their mismatch doesn't create many problems
                      // extra complexity is unnecessary.
                      throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
                          "Failed to set the certificate serial number in the local DB.");
                  }
                  if (mDatabase.getSnapshotVersion(userId, uid) != null) {
                      mDatabase.setShouldCreateSnapshot(userId, uid, true);
                      Log.i(TAG, "This is a certificate change. Snapshot must be updated");
                  } else {
                      Log.i(TAG, "This is a certificate change. Snapshot didn't exist");
                  }
                  long updatedCounterIdRows =
                          mDatabase.setCounterId(userId, uid, new SecureRandom().nextLong());
                  if (updatedCounterIdRows < 0) {
                      Log.e(TAG, "Failed to set the counter id in the local DB.");
                  }
              } else if (updatedCertPathRows < 0) {
                  throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
                          "Failed to set the certificate path in the local DB.");
              }
          } catch (CertificateEncodingException e) {
              Log.e(TAG, "Failed to encode CertPath", e);
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
          }
      }
  
      /**
       * Initializes the recovery service with the two files {@code recoveryServiceCertFile} and
       * {@code recoveryServiceSigFile}.
       *
       * @param rootCertificateAlias the alias for the root certificate that is used for validating
       *     the recovery service certificates.
       * @param recoveryServiceCertFile the content of the XML file containing a list of certificates
       *     for the recovery service.
       * @param recoveryServiceSigFile the content of the XML file containing the public-key signature
       *     over the entire content of {@code recoveryServiceCertFile}.
       */
      public void initRecoveryServiceWithSigFile(
              @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile,
              @NonNull byte[] recoveryServiceSigFile)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          rootCertificateAlias =
                  mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
          Preconditions.checkNotNull(recoveryServiceCertFile, "recoveryServiceCertFile is null");
          Preconditions.checkNotNull(recoveryServiceSigFile, "recoveryServiceSigFile is null");
  
          SigXml sigXml;
          try {
              sigXml = SigXml.parse(recoveryServiceSigFile);
          } catch (CertParsingException e) {
              Log.d(TAG, "Failed to parse the sig file: " + HexDump.toHexString(
                      recoveryServiceSigFile));
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
          }
  
          X509Certificate rootCert =
                  mTestCertHelper.getRootCertificate(rootCertificateAlias);
          try {
              sigXml.verifyFileSignature(rootCert, recoveryServiceCertFile);
          } catch (CertValidationException e) {
              Log.d(TAG, "The signature over the cert file is invalid."
                      + " Cert: " + HexDump.toHexString(recoveryServiceCertFile)
                      + " Sig: " + HexDump.toHexString(recoveryServiceSigFile));
              throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
          }
  
          initRecoveryService(rootCertificateAlias, recoveryServiceCertFile);
      }
  
      /**
       * Gets all data necessary to recover application keys on new device.
       *
       * @return KeyChain Snapshot.
       * @throws ServiceSpecificException if no snapshot is pending.
       * @hide
       */
      public @NonNull KeyChainSnapshot getKeyChainSnapshot()
              throws RemoteException {
          checkRecoverKeyStorePermission();
          int uid = Binder.getCallingUid();
          KeyChainSnapshot snapshot = mSnapshotStorage.get(uid);
          if (snapshot == null) {
              throw new ServiceSpecificException(ERROR_NO_SNAPSHOT_PENDING);
          }
          return snapshot;
      }
  
      public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          int uid = Binder.getCallingUid();
          mListenersStorage.setSnapshotListener(uid, intent);
      }
  
      /**
       * Set the server params for the user's key chain. This is used to uniquely identify a key
       * chain. Along with the counter ID, it is used to uniquely identify an instance of a vault.
       */
      public void setServerParams(@NonNull byte[] serverParams) throws RemoteException {
          checkRecoverKeyStorePermission();
          int userId = UserHandle.getCallingUserId();
          int uid = Binder.getCallingUid();
  
          byte[] currentServerParams = mDatabase.getServerParams(userId, uid);
  
          if (Arrays.equals(serverParams, currentServerParams)) {
              Log.v(TAG, "Not updating server params - same as old value.");
              return;
          }
  
          long updatedRows = mDatabase.setServerParams(userId, uid, serverParams);
          if (updatedRows < 0) {
              throw new ServiceSpecificException(
                      ERROR_SERVICE_INTERNAL_ERROR, "Database failure trying to set server params.");
          }
  
          if (currentServerParams == null) {
              Log.i(TAG, "Initialized server params.");
              return;
          }
  
          if (mDatabase.getSnapshotVersion(userId, uid) != null) {
              mDatabase.setShouldCreateSnapshot(userId, uid, true);
              Log.i(TAG, "Updated server params. Snapshot must be updated");
          } else {
              Log.i(TAG, "Updated server params. Snapshot didn't exist");
          }
      }
  
      /**
       * Sets the recovery status of key with {@code alias} to {@code status}.
       */
      public void setRecoveryStatus(@NonNull String alias, int status) throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(alias, "alias is null");
          long updatedRows = mDatabase.setRecoveryStatus(Binder.getCallingUid(), alias, status);
          if (updatedRows < 0) {
              throw new ServiceSpecificException(
                      ERROR_SERVICE_INTERNAL_ERROR,
                      "Failed to set the key recovery status in the local DB.");
          }
      }
  
      /**
       * Returns recovery statuses for all keys belonging to the calling uid.
       *
       * @return {@link Map} from key alias to recovery status. Recovery status is one of
       *     {@link RecoveryController#RECOVERY_STATUS_SYNCED},
       *     {@link RecoveryController#RECOVERY_STATUS_SYNC_IN_PROGRESS} or
       *     {@link RecoveryController#RECOVERY_STATUS_PERMANENT_FAILURE}.
       */
      public @NonNull Map<String, Integer> getRecoveryStatus() throws RemoteException {
          checkRecoverKeyStorePermission();
          return mDatabase.getStatusForAllKeys(Binder.getCallingUid());
      }
  
      /**
       * Sets recovery secrets list used by all recovery agents for given {@code userId}
       *
       * @hide
       */
      public void setRecoverySecretTypes(
              @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(secretTypes, "secretTypes is null");
          int userId = UserHandle.getCallingUserId();
          int uid = Binder.getCallingUid();
  
          int[] currentSecretTypes = mDatabase.getRecoverySecretTypes(userId, uid);
          if (Arrays.equals(secretTypes, currentSecretTypes)) {
              Log.v(TAG, "Not updating secret types - same as old value.");
              return;
          }
  
          long updatedRows = mDatabase.setRecoverySecretTypes(userId, uid, secretTypes);
          if (updatedRows < 0) {
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
                      "Database error trying to set secret types.");
          }
  
          if (currentSecretTypes.length == 0) {
              Log.i(TAG, "Initialized secret types.");
              return;
          }
  
          Log.i(TAG, "Updated secret types. Snapshot pending.");
          if (mDatabase.getSnapshotVersion(userId, uid) != null) {
              mDatabase.setShouldCreateSnapshot(userId, uid, true);
              Log.i(TAG, "Updated secret types. Snapshot must be updated");
          } else {
              Log.i(TAG, "Updated secret types. Snapshot didn't exist");
          }
      }
  
      /**
       * Gets secret types necessary to create Recovery Data.
       *
       * @return secret types
       * @hide
       */
      public @NonNull int[] getRecoverySecretTypes() throws RemoteException {
          checkRecoverKeyStorePermission();
          return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(),
              Binder.getCallingUid());
      }
  
      /**
       * Initializes recovery session given the X509-encoded public key of the recovery service.
       *
       * @param sessionId A unique ID to identify the recovery session.
       * @param verifierPublicKey X509-encoded public key.
       * @param vaultParams Additional params associated with vault.
       * @param vaultChallenge Challenge issued by vault service.
       * @param secrets Lock-screen hashes. For now only a single secret is supported.
       * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
       * @deprecated Use {@link #startRecoverySessionWithCertPath(String, String, RecoveryCertPath,
       *         byte[], byte[], List)} instead.
       *
       * @hide
       */
      @VisibleForTesting
      @NonNull byte[] startRecoverySession(
              @NonNull String sessionId,
              @NonNull byte[] verifierPublicKey,
              @NonNull byte[] vaultParams,
              @NonNull byte[] vaultChallenge,
              @NonNull List<KeyChainProtectionParams> secrets)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          int uid = Binder.getCallingUid();
  
          if (secrets.size() != 1) {
              throw new UnsupportedOperationException(
                      "Only a single KeyChainProtectionParams is supported");
          }
  
          PublicKey publicKey;
          try {
              publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey);
          } catch (InvalidKeySpecException e) {
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
          }
          // The raw public key bytes contained in vaultParams must match the ones given in
          // verifierPublicKey; otherwise, the user secret may be decrypted by a key that is not owned
          // by the original recovery service.
          if (!publicKeysMatch(publicKey, vaultParams)) {
              throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE,
                      "The public keys given in verifierPublicKey and vaultParams do not match.");
          }
  
          byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
          byte[] kfHash = secrets.get(0).getSecret();
          mRecoverySessionStorage.add(
                  uid,
                  new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams));
  
          Log.i(TAG, "Received VaultParams for recovery: " + HexDump.toHexString(vaultParams));
          try {
              byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
              return KeySyncUtils.encryptRecoveryClaim(
                      publicKey,
                      vaultParams,
                      vaultChallenge,
                      thmKfHash,
                      keyClaimant);
          } catch (NoSuchAlgorithmException e) {
              Log.wtf(TAG, "SecureBox algorithm missing. AOSP must support this.", e);
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          } catch (InvalidKeyException e) {
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
          }
      }
  
      /**
       * Initializes recovery session given the certificate path of the recovery service.
       *
       * @param sessionId A unique ID to identify the recovery session.
       * @param verifierCertPath The certificate path of the recovery service.
       * @param vaultParams Additional params associated with vault.
       * @param vaultChallenge Challenge issued by vault service.
       * @param secrets Lock-screen hashes. For now only a single secret is supported.
       * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
       *
       * @hide
       */
      public @NonNull byte[] startRecoverySessionWithCertPath(
              @NonNull String sessionId,
              @NonNull String rootCertificateAlias,
              @NonNull RecoveryCertPath verifierCertPath,
              @NonNull byte[] vaultParams,
              @NonNull byte[] vaultChallenge,
              @NonNull List<KeyChainProtectionParams> secrets)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          rootCertificateAlias =
                  mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
          Preconditions.checkNotNull(sessionId, "invalid session");
          Preconditions.checkNotNull(verifierCertPath, "verifierCertPath is null");
          Preconditions.checkNotNull(vaultParams, "vaultParams is null");
          Preconditions.checkNotNull(vaultChallenge, "vaultChallenge is null");
          Preconditions.checkNotNull(secrets, "secrets is null");
          CertPath certPath;
          try {
              certPath = verifierCertPath.getCertPath();
          } catch (CertificateException e) {
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
          }
  
          try {
              CertUtils.validateCertPath(
                      mTestCertHelper.getRootCertificate(rootCertificateAlias), certPath);
          } catch (CertValidationException e) {
              Log.e(TAG, "Failed to validate the given cert path", e);
              throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
          }
  
          byte[] verifierPublicKey = certPath.getCertificates().get(0).getPublicKey().getEncoded();
          if (verifierPublicKey == null) {
              Log.e(TAG, "Failed to encode verifierPublicKey");
              throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT,
                      "Failed to encode verifierPublicKey");
          }
  
          return startRecoverySession(
                  sessionId, verifierPublicKey, vaultParams, vaultChallenge, secrets);
      }
  
      /**
       * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault
       * service.
       *
       * @param sessionId The session ID used to generate the claim. See
       *     {@link #startRecoverySession(String, byte[], byte[], byte[], List)}.
       * @param encryptedRecoveryKey The encrypted recovery key blob returned by the remote vault
       *     service.
       * @param applicationKeys The encrypted key blobs returned by the remote vault service. These
       *     were wrapped with the recovery key.
       * @throws RemoteException if an error occurred recovering the keys.
       */
      public @NonNull Map<String, String> recoverKeyChainSnapshot(
              @NonNull String sessionId,
              @NonNull byte[] encryptedRecoveryKey,
              @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
          checkRecoverKeyStorePermission();
          int userId = UserHandle.getCallingUserId();
          int uid = Binder.getCallingUid();
          RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
          if (sessionEntry == null) {
              throw new ServiceSpecificException(ERROR_SESSION_EXPIRED,
                      String.format(Locale.US,
                              "Application uid=%d does not have pending session '%s'",
                              uid,
                              sessionId));
          }
  
          try {
              byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey);
              Map<String, byte[]> keysByAlias = recoverApplicationKeys(recoveryKey,
                      applicationKeys);
              return importKeyMaterials(userId, uid, keysByAlias);
          } catch (KeyStoreException e) {
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          } finally {
              sessionEntry.destroy();
              mRecoverySessionStorage.remove(uid);
          }
      }
  
      /**
       * Imports the key materials, returning a map from alias to grant alias for the calling user.
       *
       * @param userId The calling user ID.
       * @param uid The calling uid.
       * @param keysByAlias The key materials, keyed by alias.
       * @throws KeyStoreException if an error occurs importing the key or getting the grant.
       */
      private @NonNull Map<String, String> importKeyMaterials(
              int userId, int uid, Map<String, byte[]> keysByAlias)
              throws KeyStoreException {
          ArrayMap<String, String> grantAliasesByAlias = new ArrayMap<>(keysByAlias.size());
          for (String alias : keysByAlias.keySet()) {
              mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keysByAlias.get(alias));
              String grantAlias = getAlias(userId, uid, alias);
              Log.i(TAG, String.format(Locale.US, "Import %s -> %s", alias, grantAlias));
              grantAliasesByAlias.put(alias, grantAlias);
          }
          return grantAliasesByAlias;
      }
  
      /**
       * Returns an alias for the key.
       *
       * @param userId The user ID of the calling process.
       * @param uid The uid of the calling process.
       * @param alias The alias of the key.
       * @return The alias in the calling process's keystore.
       */
      private @Nullable String getAlias(int userId, int uid, String alias) {
          return mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
      }
  
      /**
       * Destroys the session with the given {@code sessionId}.
       */
      public void closeSession(@NonNull String sessionId) throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(sessionId, "invalid session");
          mRecoverySessionStorage.remove(Binder.getCallingUid(), sessionId);
      }
  
      public void removeKey(@NonNull String alias) throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(alias, "alias is null");
          int uid = Binder.getCallingUid();
          int userId = UserHandle.getCallingUserId();
  
          boolean wasRemoved = mDatabase.removeKey(uid, alias);
          if (wasRemoved) {
              mDatabase.setShouldCreateSnapshot(userId, uid, true);
              mApplicationKeyStorage.deleteEntry(userId, uid, alias);
          }
      }
  
      /**
       * Generates a key named {@code alias} in caller's namespace.
       * The key is stored in system service keystore namespace.
       *
       * @param alias the alias provided by caller as a reference to the key.
       * @return grant alias, which caller can use to access the key.
       * @throws RemoteException if certain internal errors occur.
       *
       * @deprecated Use {@link #generateKeyWithMetadata(String, byte[])} instead.
       */
      @Deprecated
      public String generateKey(@NonNull String alias) throws RemoteException {
          return generateKeyWithMetadata(alias, /*metadata=*/ null);
      }
  
      /**
       * Generates a key named {@code alias} with the {@code metadata} in caller's namespace.
       * The key is stored in system service keystore namespace.
       *
       * @param alias the alias provided by caller as a reference to the key.
       * @param metadata the optional metadata blob that will authenticated (but unencrypted) together
       *         with the key material when the key is uploaded to cloud.
       * @return grant alias, which caller can use to access the key.
       * @throws RemoteException if certain internal errors occur.
       */
      public String generateKeyWithMetadata(@NonNull String alias, @Nullable byte[] metadata)
              throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(alias, "alias is null");
          int uid = Binder.getCallingUid();
          int userId = UserHandle.getCallingUserId();
  
          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 | IOException e) {
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          } catch (InsecureUserException e) {
              throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage());
          }
  
          try {
              byte[] secretKey = mRecoverableKeyGenerator.generateAndStoreKey(encryptionKey, userId,
                      uid, alias, metadata);
              mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, secretKey);
              return getAlias(userId, uid, alias);
          } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          }
      }
  
      /**
       * 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.
       *
       * @deprecated Use {{@link #importKeyWithMetadata(String, byte[], byte[])}} instead.
       *
       * @hide
       */
      @Deprecated
      public @Nullable String importKey(@NonNull String alias, @NonNull byte[] keyBytes)
              throws RemoteException {
          return importKeyWithMetadata(alias, keyBytes, /*metadata=*/ null);
      }
  
      /**
       * Imports a 256-bit AES-GCM key named {@code alias} with the given {@code metadata}. 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.
       * @param metadata the metadata to be authenticated (but unencrypted) together with the 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 @Nullable String importKeyWithMetadata(@NonNull String alias, @NonNull byte[] keyBytes,
              @Nullable byte[] metadata) throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(alias, "alias is null");
          Preconditions.checkNotNull(keyBytes, "keyBytes is null");
          if (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();
  
          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 | IOException 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,
                      metadata);
  
              // Import the key to Android KeyStore and get grant
              mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keyBytes);
              return getAlias(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.
       */
      public @Nullable String getKey(@NonNull String alias) throws RemoteException {
          checkRecoverKeyStorePermission();
          Preconditions.checkNotNull(alias, "alias is null");
          int uid = Binder.getCallingUid();
          int userId = UserHandle.getCallingUserId();
          return getAlias(userId, uid, alias);
      }
  
      private byte[] decryptRecoveryKey(
              RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)
              throws RemoteException, ServiceSpecificException {
          byte[] locallyEncryptedKey;
          try {
              locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse(
                      sessionEntry.getKeyClaimant(),
                      sessionEntry.getVaultParams(),
                      encryptedClaimResponse);
          } catch (InvalidKeyException e) {
              Log.e(TAG, "Got InvalidKeyException during decrypting recovery claim response", e);
              throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
                      "Failed to decrypt recovery key " + e.getMessage());
          } catch (AEADBadTagException e) {
              Log.e(TAG, "Got AEADBadTagException during decrypting recovery claim response", e);
              throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
                      "Failed to decrypt recovery key " + e.getMessage());
          } catch (NoSuchAlgorithmException e) {
              // Should never happen: all the algorithms used are required by AOSP implementations
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          }
  
          try {
              return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey);
          } catch (InvalidKeyException e) {
              Log.e(TAG, "Got InvalidKeyException during decrypting recovery key", e);
              throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
                      "Failed to decrypt recovery key " + e.getMessage());
          } catch (AEADBadTagException e) {
              Log.e(TAG, "Got AEADBadTagException during decrypting recovery key", e);
              throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
                      "Failed to decrypt recovery key " + e.getMessage());
          } catch (NoSuchAlgorithmException e) {
              // Should never happen: all the algorithms used are required by AOSP implementations
              throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
          }
      }
  
      /**
       * Uses {@code recoveryKey} to decrypt {@code applicationKeys}.
       *
       * @return Map from alias to raw key material.
       * @throws RemoteException if an error occurred decrypting the keys.
       */
      private @NonNull Map<String, byte[]> recoverApplicationKeys(@NonNull byte[] recoveryKey,
              @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
          HashMap<String, byte[]> keyMaterialByAlias = new HashMap<>();
          for (WrappedApplicationKey applicationKey : applicationKeys) {
              String alias = applicationKey.getAlias();
              byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial();
              byte[] keyMetadata = applicationKey.getMetadata();
  
              try {
                  byte[] keyMaterial = KeySyncUtils.decryptApplicationKey(recoveryKey,
                          encryptedKeyMaterial, keyMetadata);
                  keyMaterialByAlias.put(alias, keyMaterial);
              } catch (NoSuchAlgorithmException e) {
                  Log.wtf(TAG, "Missing SecureBox algorithm. AOSP required to support this.", e);
                  throw new ServiceSpecificException(
                          ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
              } catch (InvalidKeyException e) {
                  Log.e(TAG, "Got InvalidKeyException during decrypting application key with alias: "
                          + alias, e);
                  throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
                          "Failed to recover key with alias '" + alias + "': " + e.getMessage());
              } catch (AEADBadTagException e) {
                  Log.e(TAG, "Got AEADBadTagException during decrypting application key with alias: "
                          + alias, e);
                  // Ignore the exception to continue to recover the other application keys.
              }
          }
          if (!applicationKeys.isEmpty() && keyMaterialByAlias.isEmpty()) {
              Log.e(TAG, "Failed to recover any of the application keys.");
              throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
                      "Failed to recover any of the application keys.");
          }
          return keyMaterialByAlias;
      }
  
      /**
       * This function can only be used inside LockSettingsService.
       *
       * @param storedHashType from {@code CredentialHash}
       * @param credential - unencrypted byte array. Password length should be at most 16 symbols
       *     {@code mPasswordMaxLength}
       * @param userId for user who just unlocked the device.
       * @hide
       */
      public void lockScreenSecretAvailable(
              int storedHashType, @NonNull byte[] credential, int userId) {
          // So as not to block the critical path unlocking the phone, defer to another thread.
          try {
              mExecutorService.execute(KeySyncTask.newInstance(
                      mContext,
                      mDatabase,
                      mSnapshotStorage,
                      mListenersStorage,
                      userId,
                      storedHashType,
                      credential,
                      /*credentialUpdated=*/ false));
          } catch (NoSuchAlgorithmException e) {
              Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
          } catch (KeyStoreException e) {
              Log.e(TAG, "Key store error encountered during recoverable key sync", e);
          } catch (InsecureUserException e) {
              Log.wtf(TAG, "Impossible - insecure user, but user just entered lock screen", e);
          }
      }
  
      /**
       * This function can only be used inside LockSettingsService.
       *
       * @param storedHashType from {@code CredentialHash}
       * @param credential - unencrypted byte array
       * @param userId for the user whose lock screen credentials were changed.
       * @hide
       */
      public void lockScreenSecretChanged(
              int storedHashType,
              @Nullable byte[] credential,
              int userId) {
          // So as not to block the critical path unlocking the phone, defer to another thread.
          try {
              mExecutorService.execute(KeySyncTask.newInstance(
                      mContext,
                      mDatabase,
                      mSnapshotStorage,
                      mListenersStorage,
                      userId,
                      storedHashType,
                      credential,
                      /*credentialUpdated=*/ true));
          } catch (NoSuchAlgorithmException e) {
              Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
          } catch (KeyStoreException e) {
              Log.e(TAG, "Key store error encountered during recoverable key sync", e);
          } catch (InsecureUserException e) {
              Log.e(TAG, "InsecureUserException during lock screen secret update", e);
          }
      }
  
      private void checkRecoverKeyStorePermission() {
          mContext.enforceCallingOrSelfPermission(
                  Manifest.permission.RECOVER_KEYSTORE,
                  "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission.");
          int userId = UserHandle.getCallingUserId();
          int uid = Binder.getCallingUid();
          mCleanupManager.registerRecoveryAgent(userId, uid);
      }
  
      private boolean publicKeysMatch(PublicKey publicKey, byte[] vaultParams) {
          byte[] encodedPublicKey = SecureBox.encodePublicKey(publicKey);
          return Arrays.equals(encodedPublicKey, Arrays.copyOf(vaultParams, encodedPublicKey.length));
      }
  }
  