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.KeyChainProtectionParams.TYPE_LOCKSCREEN;
  
  import android.annotation.Nullable;
  import android.content.Context;
  import android.os.RemoteException;
  import android.security.Scrypt;
  import android.security.keystore.recovery.KeyChainProtectionParams;
  import android.security.keystore.recovery.KeyChainSnapshot;
  import android.security.keystore.recovery.KeyDerivationParams;
  import android.security.keystore.recovery.WrappedApplicationKey;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.widget.LockPatternUtils;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
  
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  import java.security.GeneralSecurityException;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.KeyStoreException;
  import java.security.MessageDigest;
  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.CertificateException;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Map;
  
  import javax.crypto.KeyGenerator;
  import javax.crypto.NoSuchPaddingException;
  import javax.crypto.SecretKey;
  
  /**
   * Task to sync application keys to a remote vault service.
   *
   * @hide
   */
  public class KeySyncTask implements Runnable {
      private static final String TAG = "KeySyncTask";
  
      private static final String RECOVERY_KEY_ALGORITHM = "AES";
      private static final int RECOVERY_KEY_SIZE_BITS = 256;
      private static final int SALT_LENGTH_BYTES = 16;
      private static final int LENGTH_PREFIX_BYTES = Integer.BYTES;
      private static final String LOCK_SCREEN_HASH_ALGORITHM = "SHA-256";
      private static final int TRUSTED_HARDWARE_MAX_ATTEMPTS = 10;
  
      @VisibleForTesting
      static final int SCRYPT_PARAM_N = 4096;
      @VisibleForTesting
      static final int SCRYPT_PARAM_R = 8;
      @VisibleForTesting
      static final int SCRYPT_PARAM_P = 1;
      @VisibleForTesting
      static final int SCRYPT_PARAM_OUTLEN_BYTES = 32;
  
      private final RecoverableKeyStoreDb mRecoverableKeyStoreDb;
      private final int mUserId;
      private final int mCredentialType;
      private final byte[] mCredential;
      private final boolean mCredentialUpdated;
      private final PlatformKeyManager mPlatformKeyManager;
      private final RecoverySnapshotStorage mRecoverySnapshotStorage;
      private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
      private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
      private final Scrypt mScrypt;
  
      public static KeySyncTask newInstance(
              Context context,
              RecoverableKeyStoreDb recoverableKeyStoreDb,
              RecoverySnapshotStorage snapshotStorage,
              RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
              int userId,
              int credentialType,
              byte[] credential,
              boolean credentialUpdated
      ) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
          return new KeySyncTask(
                  recoverableKeyStoreDb,
                  snapshotStorage,
                  recoverySnapshotListenersStorage,
                  userId,
                  credentialType,
                  credential,
                  credentialUpdated,
                  PlatformKeyManager.getInstance(context, recoverableKeyStoreDb),
                  new TestOnlyInsecureCertificateHelper(),
                  new Scrypt());
      }
  
      /**
       * A new task.
       *
       * @param recoverableKeyStoreDb Database where the keys are stored.
       * @param userId The uid of the user whose profile has been unlocked.
       * @param credentialType The type of credential as defined in {@code LockPatternUtils}
       * @param credential The credential, encoded as a {@link String}.
       * @param credentialUpdated signals weather credentials were updated.
       * @param platformKeyManager platform key manager
       * @param testOnlyInsecureCertificateHelper utility class used for end-to-end tests
       */
      @VisibleForTesting
      KeySyncTask(
              RecoverableKeyStoreDb recoverableKeyStoreDb,
              RecoverySnapshotStorage snapshotStorage,
              RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
              int userId,
              int credentialType,
              byte[] credential,
              boolean credentialUpdated,
              PlatformKeyManager platformKeyManager,
              TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
              Scrypt scrypt) {
          mSnapshotListenersStorage = recoverySnapshotListenersStorage;
          mRecoverableKeyStoreDb = recoverableKeyStoreDb;
          mUserId = userId;
          mCredentialType = credentialType;
          mCredential = credential;
          mCredentialUpdated = credentialUpdated;
          mPlatformKeyManager = platformKeyManager;
          mRecoverySnapshotStorage = snapshotStorage;
          mTestOnlyInsecureCertificateHelper = testOnlyInsecureCertificateHelper;
          mScrypt = scrypt;
      }
  
      @Override
      public void run() {
          try {
              // Only one task is active If user unlocks phone many times in a short time interval.
              synchronized(KeySyncTask.class) {
                  syncKeys();
              }
          } catch (Exception e) {
              Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e);
          }
      }
  
      private void syncKeys() throws RemoteException {
          if (mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
              // Application keys for the user will not be available for sync.
              Log.w(TAG, "Credentials are not set for user " + mUserId);
              int generation = mPlatformKeyManager.getGenerationId(mUserId);
              mPlatformKeyManager.invalidatePlatformKey(mUserId, generation);
              return;
          }
          if (isCustomLockScreen()) {
              Log.w(TAG, "Unsupported credential type " + mCredentialType + "for user " + mUserId);
              mRecoverableKeyStoreDb.invalidateKeysForUserIdOnCustomScreenLock(mUserId);
              return;
          }
  
          List<Integer> recoveryAgents = mRecoverableKeyStoreDb.getRecoveryAgents(mUserId);
          for (int uid : recoveryAgents) {
              try {
                syncKeysForAgent(uid);
              } catch (IOException e) {
                  Log.e(TAG, "IOException during sync for agent " + uid, e);
              }
          }
          if (recoveryAgents.isEmpty()) {
              Log.w(TAG, "No recovery agent initialized for user " + mUserId);
          }
      }
  
      private boolean isCustomLockScreen() {
          return mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_NONE
              && mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_PATTERN
              && mCredentialType != LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
      }
  
      private void syncKeysForAgent(int recoveryAgentUid) throws IOException, RemoteException {
          boolean shouldRecreateCurrentVersion = false;
          if (!shouldCreateSnapshot(recoveryAgentUid)) {
              shouldRecreateCurrentVersion =
                      (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null)
                      && (mRecoverySnapshotStorage.get(recoveryAgentUid) == null);
              if (shouldRecreateCurrentVersion) {
                  Log.d(TAG, "Recreating most recent snapshot");
              } else {
                  Log.d(TAG, "Key sync not needed.");
                  return;
              }
          }
  
          String rootCertAlias =
                  mRecoverableKeyStoreDb.getActiveRootOfTrust(mUserId, recoveryAgentUid);
          rootCertAlias = mTestOnlyInsecureCertificateHelper
                  .getDefaultCertificateAliasIfEmpty(rootCertAlias);
  
          PublicKey publicKey;
          CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId,
                  recoveryAgentUid, rootCertAlias);
          if (certPath != null) {
              Log.d(TAG, "Using the public key in stored CertPath for syncing");
              publicKey = certPath.getCertificates().get(0).getPublicKey();
          } else {
              Log.d(TAG, "Using the stored raw public key for syncing");
              publicKey = mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId,
                      recoveryAgentUid);
          }
          if (publicKey == null) {
              Log.w(TAG, "Not initialized for KeySync: no public key set. Cancelling task.");
              return;
          }
  
          byte[] vaultHandle = mRecoverableKeyStoreDb.getServerParams(mUserId, recoveryAgentUid);
          if (vaultHandle == null) {
              Log.w(TAG, "No device ID set for user " + mUserId);
              return;
          }
  
          if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias)) {
              Log.w(TAG, "Insecure root certificate is used by recovery agent "
                      + recoveryAgentUid);
              if (mTestOnlyInsecureCertificateHelper.doesCredentialSupportInsecureMode(
                      mCredentialType, mCredential)) {
                  Log.w(TAG, "Whitelisted credential is used to generate snapshot by "
                          + "recovery agent "+ recoveryAgentUid);
              } else {
                  Log.w(TAG, "Non whitelisted credential is used to generate recovery snapshot by "
                          + recoveryAgentUid + " - ignore attempt.");
                  return; // User secret will not be used.
              }
          }
  
          boolean useScryptToHashCredential = shouldUseScryptToHashCredential();
          byte[] salt = generateSalt();
          byte[] localLskfHash;
          if (useScryptToHashCredential) {
              localLskfHash = hashCredentialsByScrypt(salt, mCredential);
          } else {
              localLskfHash = hashCredentialsBySaltedSha256(salt, mCredential);
          }
  
          Map<String, Pair<SecretKey, byte[]>> rawKeysWithMetadata;
          try {
              rawKeysWithMetadata = getKeysToSync(recoveryAgentUid);
          } catch (GeneralSecurityException e) {
              Log.e(TAG, "Failed to load recoverable keys for sync", e);
              return;
          } catch (InsecureUserException e) {
              Log.e(TAG, "A screen unlock triggered the key sync flow, so user must have "
                      + "lock screen. This should be impossible.", e);
              return;
          } catch (BadPlatformKeyException e) {
              Log.e(TAG, "Loaded keys for same generation ID as platform key, so "
                      + "BadPlatformKeyException should be impossible.", e);
              return;
          } catch (IOException e) {
              Log.e(TAG, "Local database error.", e);
              return;
          }
          // Only include insecure key material for test
          if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificateAlias(rootCertAlias)) {
              rawKeysWithMetadata =
                      mTestOnlyInsecureCertificateHelper.keepOnlyWhitelistedInsecureKeys(
                              rawKeysWithMetadata);
          }
  
          SecretKey recoveryKey;
          try {
              recoveryKey = generateRecoveryKey();
          } catch (NoSuchAlgorithmException e) {
              Log.wtf("AES should never be unavailable", e);
              return;
          }
  
          Map<String, byte[]> encryptedApplicationKeys;
          try {
              encryptedApplicationKeys = KeySyncUtils.encryptKeysWithRecoveryKey(
                      recoveryKey, rawKeysWithMetadata);
          } catch (InvalidKeyException | NoSuchAlgorithmException e) {
              Log.wtf(TAG,
                      "Should be impossible: could not encrypt application keys with random key",
                      e);
              return;
          }
  
          Long counterId;
          // counter id is generated exactly once for each credentials value.
          if (mCredentialUpdated) {
              counterId = generateAndStoreCounterId(recoveryAgentUid);
          } else {
              counterId = mRecoverableKeyStoreDb.getCounterId(mUserId, recoveryAgentUid);
              if (counterId == null) {
                  counterId = generateAndStoreCounterId(recoveryAgentUid);
              }
          }
  
          byte[] vaultParams = KeySyncUtils.packVaultParams(
                  publicKey,
                  counterId,
                  TRUSTED_HARDWARE_MAX_ATTEMPTS,
                  vaultHandle);
  
          byte[] encryptedRecoveryKey;
          try {
              encryptedRecoveryKey = KeySyncUtils.thmEncryptRecoveryKey(
                      publicKey,
                      localLskfHash,
                      vaultParams,
                      recoveryKey);
          } catch (NoSuchAlgorithmException e) {
              Log.wtf(TAG, "SecureBox encrypt algorithms unavailable", e);
              return;
          } catch (InvalidKeyException e) {
              Log.e(TAG,"Could not encrypt with recovery key", e);
              return;
          }
  
          KeyDerivationParams keyDerivationParams;
          if (useScryptToHashCredential) {
              keyDerivationParams = KeyDerivationParams.createScryptParams(
                      salt, /*memoryDifficulty=*/ SCRYPT_PARAM_N);
          } else {
              keyDerivationParams = KeyDerivationParams.createSha256Params(salt);
          }
          KeyChainProtectionParams keyChainProtectionParams = new KeyChainProtectionParams.Builder()
                  .setUserSecretType(TYPE_LOCKSCREEN)
                  .setLockScreenUiFormat(getUiFormat(mCredentialType, mCredential))
                  .setKeyDerivationParams(keyDerivationParams)
                  .setSecret(new byte[0])
                  .build();
  
          ArrayList<KeyChainProtectionParams> metadataList = new ArrayList<>();
          metadataList.add(keyChainProtectionParams);
  
          KeyChainSnapshot.Builder keyChainSnapshotBuilder = new KeyChainSnapshot.Builder()
                  .setSnapshotVersion(
                          getSnapshotVersion(recoveryAgentUid, shouldRecreateCurrentVersion))
                  .setMaxAttempts(TRUSTED_HARDWARE_MAX_ATTEMPTS)
                  .setCounterId(counterId)
                  .setServerParams(vaultHandle)
                  .setKeyChainProtectionParams(metadataList)
                  .setWrappedApplicationKeys(
                          createApplicationKeyEntries(encryptedApplicationKeys, rawKeysWithMetadata))
                  .setEncryptedRecoveryKeyBlob(encryptedRecoveryKey);
          try {
              keyChainSnapshotBuilder.setTrustedHardwareCertPath(certPath);
          } catch(CertificateException e) {
              // Should not happen, as it's just deserialized from bytes stored in the db
              Log.wtf(TAG, "Cannot serialize CertPath when calling setTrustedHardwareCertPath", e);
              return;
          }
          mRecoverySnapshotStorage.put(recoveryAgentUid, keyChainSnapshotBuilder.build());
          mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
  
          mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, false);
      }
  
      @VisibleForTesting
      int getSnapshotVersion(int recoveryAgentUid, boolean shouldRecreateCurrentVersion)
              throws IOException {
          Long snapshotVersion = mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid);
          if (shouldRecreateCurrentVersion) {
              // version shouldn't be null at this moment.
              snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion;
          } else {
              snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion + 1;
          }
  
          long updatedRows = mRecoverableKeyStoreDb.setSnapshotVersion(mUserId, recoveryAgentUid,
                  snapshotVersion);
          if (updatedRows < 0) {
              Log.e(TAG, "Failed to set the snapshot version in the local DB.");
              throw new IOException("Failed to set the snapshot version in the local DB.");
          }
  
          return snapshotVersion.intValue();
      }
  
      private long generateAndStoreCounterId(int recoveryAgentUid) throws IOException {
          long counter = new SecureRandom().nextLong();
          long updatedRows = mRecoverableKeyStoreDb.setCounterId(mUserId, recoveryAgentUid, counter);
          if (updatedRows < 0) {
              Log.e(TAG, "Failed to set the snapshot version in the local DB.");
              throw new IOException("Failed to set counterId in the local DB.");
          }
          return counter;
      }
  
      /**
       * Returns all of the recoverable keys for the user.
       */
      private Map<String, Pair<SecretKey, byte[]>> getKeysToSync(int recoveryAgentUid)
              throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
              NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
              InvalidKeyException, InvalidAlgorithmParameterException, IOException, RemoteException {
          PlatformDecryptionKey decryptKey = mPlatformKeyManager.getDecryptKey(mUserId);;
          Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
                  mUserId, recoveryAgentUid, decryptKey.getGenerationId());
          return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
      }
  
      /**
       * Returns {@code true} if a sync is pending.
       * @param recoveryAgentUid uid of the recovery agent.
       */
      private boolean shouldCreateSnapshot(int recoveryAgentUid) {
          int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid);
          if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) {
              // Only lockscreen type is supported.
              // We will need to pass extra argument to KeySyncTask to support custom pass phrase.
              return false;
          }
          if (mCredentialUpdated) {
              // Sync credential if at least one snapshot was created.
              if (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) {
                  mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, true);
                  return true;
              }
          }
  
          return mRecoverableKeyStoreDb.getShouldCreateSnapshot(mUserId, recoveryAgentUid);
      }
  
      /**
       * The UI best suited to entering the given lock screen. This is synced with the vault so the
       * user can be shown the same UI when recovering the vault on another device.
       *
       * @return The format - either pattern, pin, or password.
       */
      @VisibleForTesting
      @KeyChainProtectionParams.LockScreenUiFormat static int getUiFormat(
              int credentialType, byte[] credential) {
          if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
              return KeyChainProtectionParams.UI_FORMAT_PATTERN;
          } else if (isPin(credential)) {
              return KeyChainProtectionParams.UI_FORMAT_PIN;
          } else {
              return KeyChainProtectionParams.UI_FORMAT_PASSWORD;
          }
      }
  
      /**
       * Generates a salt to include with the lock screen hash.
       *
       * @return The salt.
       */
      private static byte[] generateSalt() {
          byte[] salt = new byte[SALT_LENGTH_BYTES];
          new SecureRandom().nextBytes(salt);
          return salt;
      }
  
      /**
       * Returns {@code true} if {@code credential} looks like a pin.
       */
      @VisibleForTesting
      static boolean isPin(@Nullable byte[] credential) {
          if (credential == null) {
              return false;
          }
          int length = credential.length;
          for (int i = 0; i < length; i++) {
              if (!Character.isDigit((char) credential[i])) {
                  return false;
              }
          }
          return true;
      }
  
      /**
       * Hashes {@code credentials} with the given {@code salt}.
       *
       * @return The SHA-256 hash.
       */
      @VisibleForTesting
      static byte[] hashCredentialsBySaltedSha256(byte[] salt, byte[] credentialsBytes) {
          ByteBuffer byteBuffer = ByteBuffer.allocate(
                  salt.length + credentialsBytes.length + LENGTH_PREFIX_BYTES * 2);
          byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
          byteBuffer.putInt(salt.length);
          byteBuffer.put(salt);
          byteBuffer.putInt(credentialsBytes.length);
          byteBuffer.put(credentialsBytes);
          byte[] bytes = byteBuffer.array();
  
          try {
              byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes);
              Arrays.fill(bytes, (byte) 0);
              return hash;
          } catch (NoSuchAlgorithmException e) {
              // Impossible, SHA-256 must be supported on Android.
              throw new RuntimeException(e);
          }
      }
  
      private byte[] hashCredentialsByScrypt(byte[] salt, byte[] credentials) {
          return mScrypt.scrypt(
                  credentials, salt, SCRYPT_PARAM_N, SCRYPT_PARAM_R, SCRYPT_PARAM_P,
                  SCRYPT_PARAM_OUTLEN_BYTES);
      }
  
      private static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
          KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
          keyGenerator.init(RECOVERY_KEY_SIZE_BITS);
          return keyGenerator.generateKey();
      }
  
      private static List<WrappedApplicationKey> createApplicationKeyEntries(
              Map<String, byte[]> encryptedApplicationKeys,
              Map<String, Pair<SecretKey, byte[]>> originalKeysWithMetadata) {
          ArrayList<WrappedApplicationKey> keyEntries = new ArrayList<>();
          for (String alias : encryptedApplicationKeys.keySet()) {
              keyEntries.add(new WrappedApplicationKey.Builder()
                      .setAlias(alias)
                      .setEncryptedKeyMaterial(encryptedApplicationKeys.get(alias))
                      .setMetadata(originalKeysWithMetadata.get(alias).second)
                      .build());
          }
          return keyEntries;
      }
  
      private boolean shouldUseScryptToHashCredential() {
          return mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
      }
  }
  