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 android.app.KeyguardManager;
  import android.content.Context;
  import android.os.RemoteException;
  import android.os.UserHandle;
  import android.security.GateKeeper;
  import android.security.keystore.AndroidKeyStoreSecretKey;
  import android.security.keystore.KeyPermanentlyInvalidatedException;
  import android.security.keystore.KeyProperties;
  import android.security.keystore.KeyProtection;
  import android.service.gatekeeper.IGateKeeperService;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
  
  import java.io.IOException;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.KeyStore;
  import java.security.KeyStoreException;
  import java.security.NoSuchAlgorithmException;
  import java.security.UnrecoverableKeyException;
  import java.security.cert.CertificateException;
  import java.util.Locale;
  
  import javax.crypto.Cipher;
  import javax.crypto.KeyGenerator;
  import javax.crypto.NoSuchPaddingException;
  import javax.crypto.SecretKey;
  import javax.crypto.spec.GCMParameterSpec;
  
  /**
   * Manages creating and checking the validity of the platform key.
   *
   * <p>The platform key is used to wrap the material of recoverable keys before persisting them to
   * disk. It is also used to decrypt the same keys on a screen unlock, before re-wrapping them with
   * a recovery key and syncing them with remote storage.
   *
   * <p>Each platform key has two entries in AndroidKeyStore:
   *
   * <ul>
   *     <li>Encrypt entry - this entry enables the root user to at any time encrypt.
   *     <li>Decrypt entry - this entry enables the root user to decrypt only after recent user
   *       authentication, i.e., within 15 seconds after a screen unlock.
   * </ul>
   *
   * <p>Both entries are enabled only for AES/GCM/NoPadding Cipher algorithm.
   *
   * @hide
   */
  public class PlatformKeyManager {
      private static final String TAG = "PlatformKeyManager";
  
      private static final String KEY_ALGORITHM = "AES";
      private static final int KEY_SIZE_BITS = 256;
      private static final String KEY_ALIAS_PREFIX =
              "com.android.server.locksettings.recoverablekeystore/platform/";
      private static final String ENCRYPT_KEY_ALIAS_SUFFIX = "encrypt";
      private static final String DECRYPT_KEY_ALIAS_SUFFIX = "decrypt";
      private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15;
      private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
      private static final int GCM_TAG_LENGTH_BITS = 128;
      // Only used for checking if a key is usable
      private static final byte[] GCM_INSECURE_NONCE_BYTES = new byte[12];
  
      private final Context mContext;
      private final KeyStoreProxy mKeyStore;
      private final RecoverableKeyStoreDb mDatabase;
  
      private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
  
      /**
       * A new instance operating on behalf of {@code userId}, storing its prefs in the location
       * defined by {@code context}.
       *
       * @param context This should be the context of the RecoverableKeyStoreLoader service.
       * @throws KeyStoreException if failed to initialize AndroidKeyStore.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
       * @throws SecurityException if the caller does not have permission to write to /data/system.
       *
       * @hide
       */
      public static PlatformKeyManager getInstance(Context context, RecoverableKeyStoreDb database)
              throws KeyStoreException, NoSuchAlgorithmException {
          return new PlatformKeyManager(
                  context.getApplicationContext(),
                  new KeyStoreProxyImpl(getAndLoadAndroidKeyStore()),
                  database);
      }
  
      @VisibleForTesting
      PlatformKeyManager(
              Context context,
              KeyStoreProxy keyStore,
              RecoverableKeyStoreDb database) {
          mKeyStore = keyStore;
          mContext = context;
          mDatabase = database;
      }
  
      /**
       * Returns the current generation ID of the platform key. This increments whenever a platform
       * key has to be replaced. (e.g., because the user has removed and then re-added their lock
       * screen). Returns -1 if no key has been generated yet.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       *
       * @hide
       */
      public int getGenerationId(int userId) {
          return mDatabase.getPlatformKeyGenerationId(userId);
      }
  
      /**
       * Returns {@code true} if the platform key is available. A platform key won't be available if
       * the user has not set up a lock screen.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       *
       * @hide
       */
      public boolean isAvailable(int userId) {
          return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(userId);
      }
  
      /**
       * Removes the platform key from Android KeyStore.
       * It is triggered when user disables lock screen.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @param generationId Generation id.
       *
       * @hide
       */
      public void invalidatePlatformKey(int userId, int generationId) {
          if (generationId != -1) {
              try {
                  mKeyStore.deleteEntry(getEncryptAlias(userId, generationId));
                  mKeyStore.deleteEntry(getDecryptAlias(userId, generationId));
              } catch (KeyStoreException e) {
                  // Ignore failed attempt to delete key.
              }
          }
      }
  
      /**
       * Generates a new key and increments the generation ID. Should be invoked if the platform key
       * is corrupted and needs to be rotated.
       * Updates status of old keys to {@code RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE}.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
       * @throws KeyStoreException if there is an error in AndroidKeyStore.
       * @throws InsecureUserException if the user does not have a lock screen set.
       * @throws IOException if there was an issue with local database update.
       * @throws RemoteException if there was an issue communicating with {@link IGateKeeperService}.
       *
       * @hide
       */
      @VisibleForTesting
      void regenerate(int userId)
              throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException, IOException,
                      RemoteException {
          if (!isAvailable(userId)) {
              throw new InsecureUserException(String.format(
                      Locale.US, "%d does not have a lock screen set.", userId));
          }
  
          int generationId = getGenerationId(userId);
          int nextId;
          if (generationId == -1) {
              nextId = 1;
          } else {
              invalidatePlatformKey(userId, generationId);
              nextId = generationId + 1;
          }
          generateAndLoadKey(userId, nextId);
      }
  
      /**
       * Returns the platform key used for encryption.
       * Tries to regenerate key one time if it is permanently invalid.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @throws KeyStoreException if there was an AndroidKeyStore error.
       * @throws UnrecoverableKeyException if the key could not be recovered.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
       * @throws InsecureUserException if the user does not have a lock screen set.
       * @throws IOException if there was an issue with local database update.
       * @throws RemoteException if there was an issue communicating with {@link IGateKeeperService}.
       *
       * @hide
       */
      public PlatformEncryptionKey getEncryptKey(int userId)
              throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException,
                      InsecureUserException, IOException, RemoteException {
          init(userId);
          try {
              // Try to see if the decryption key is still accessible before using the encryption key.
              // The auth-bound decryption will be unrecoverable if the screen lock is disabled.
              getDecryptKeyInternal(userId);
              return getEncryptKeyInternal(userId);
          } catch (UnrecoverableKeyException e) {
              Log.i(TAG, String.format(Locale.US,
                      "Regenerating permanently invalid Platform key for user %d.",
                      userId));
              regenerate(userId);
              return getEncryptKeyInternal(userId);
          }
      }
  
      /**
       * Returns the platform key used for encryption.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @throws KeyStoreException if there was an AndroidKeyStore error.
       * @throws UnrecoverableKeyException if the key could not be recovered.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
       * @throws InsecureUserException if the user does not have a lock screen set.
       *
       * @hide
       */
      private PlatformEncryptionKey getEncryptKeyInternal(int userId) throws KeyStoreException,
             UnrecoverableKeyException, NoSuchAlgorithmException, InsecureUserException {
          int generationId = getGenerationId(userId);
          String alias = getEncryptAlias(userId, generationId);
          if (!isKeyLoaded(userId, generationId)) {
              throw new UnrecoverableKeyException("KeyStore doesn't contain key " + alias);
          }
          AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
                  alias, /*password=*/ null);
          return new PlatformEncryptionKey(generationId, key);
      }
  
      /**
       * Returns the platform key used for decryption. Only works after a recent screen unlock.
       * Tries to regenerate key one time if it is permanently invalid.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @throws KeyStoreException if there was an AndroidKeyStore error.
       * @throws UnrecoverableKeyException if the key could not be recovered.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
       * @throws InsecureUserException if the user does not have a lock screen set.
       * @throws IOException if there was an issue with local database update.
       * @throws RemoteException if there was an issue communicating with {@link IGateKeeperService}.
       *
       * @hide
       */
      public PlatformDecryptionKey getDecryptKey(int userId)
              throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException,
                      InsecureUserException, IOException, RemoteException {
          init(userId);
          try {
              PlatformDecryptionKey decryptionKey = getDecryptKeyInternal(userId);
              ensureDecryptionKeyIsValid(userId, decryptionKey);
              return decryptionKey;
          } catch (UnrecoverableKeyException e) {
              Log.i(TAG, String.format(Locale.US,
                      "Regenerating permanently invalid Platform key for user %d.",
                      userId));
              regenerate(userId);
              return getDecryptKeyInternal(userId);
          }
      }
  
      /**
       * Returns the platform key used for decryption. Only works after a recent screen unlock.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @throws KeyStoreException if there was an AndroidKeyStore error.
       * @throws UnrecoverableKeyException if the key could not be recovered.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
       * @throws InsecureUserException if the user does not have a lock screen set.
       *
       * @hide
       */
      private PlatformDecryptionKey getDecryptKeyInternal(int userId) throws KeyStoreException,
             UnrecoverableKeyException, NoSuchAlgorithmException, InsecureUserException {
          int generationId = getGenerationId(userId);
          String alias = getDecryptAlias(userId, generationId);
          if (!isKeyLoaded(userId, generationId)) {
              throw new UnrecoverableKeyException("KeyStore doesn't contain key " + alias);
          }
          AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
                  alias, /*password=*/ null);
          return new PlatformDecryptionKey(generationId, key);
      }
  
      /**
       * Tries to use the decryption key to make sure it is not permanently invalidated. The exception
       * {@code KeyPermanentlyInvalidatedException} is thrown only when the key is in use.
       *
       * <p>Note that we ignore all other InvalidKeyException exceptions, because such an exception
       * may be thrown for auth-bound keys if there's no recent unlock event.
       */
      private void ensureDecryptionKeyIsValid(int userId, PlatformDecryptionKey decryptionKey)
              throws UnrecoverableKeyException {
          try {
              Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM).init(Cipher.UNWRAP_MODE,
                      decryptionKey.getKey(),
                      new GCMParameterSpec(GCM_TAG_LENGTH_BITS, GCM_INSECURE_NONCE_BYTES));
          } catch (KeyPermanentlyInvalidatedException e) {
              Log.e(TAG, String.format(Locale.US, "The platform key for user %d became invalid.",
                      userId));
              throw new UnrecoverableKeyException(e.getMessage());
          } catch (NoSuchAlgorithmException | InvalidKeyException
                  | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
              // Ignore all other exceptions
          }
      }
  
      /**
       * Initializes the class. If there is no current platform key, and the user has a lock screen
       * set, will create the platform key and set the generation ID.
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @throws KeyStoreException if there was an error in AndroidKeyStore.
       * @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
       * @throws IOException if there was an issue with local database update.
       * @throws RemoteException if there was an issue communicating with {@link IGateKeeperService}.
       *
       * @hide
       */
      void init(int userId)
              throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException, IOException,
                      RemoteException {
          if (!isAvailable(userId)) {
              throw new InsecureUserException(String.format(
                      Locale.US, "%d does not have a lock screen set.", userId));
          }
  
          int generationId = getGenerationId(userId);
          if (isKeyLoaded(userId, generationId)) {
              Log.i(TAG, String.format(
                      Locale.US, "Platform key generation %d exists already.", generationId));
              return;
          }
          if (generationId == -1) {
              Log.i(TAG, "Generating initial platform key generation ID.");
              generationId = 1;
          } else {
              Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no "
                      + "entry was present in AndroidKeyStore. Generating fresh key.", generationId));
              // Have to generate a fresh key, so bump the generation id
              generationId++;
          }
  
          generateAndLoadKey(userId, generationId);
      }
  
      /**
       * Returns the alias of the encryption key with the specific {@code generationId} in the
       * AndroidKeyStore.
       *
       * <p>These IDs look as follows:
       * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/encrypt}
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @param generationId The generation ID.
       * @return The alias.
       */
      private String getEncryptAlias(int userId, int generationId) {
          return KEY_ALIAS_PREFIX + userId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX;
      }
  
      /**
       * Returns the alias of the decryption key with the specific {@code generationId} in the
       * AndroidKeyStore.
       *
       * <p>These IDs look as follows:
       * {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/decrypt}
       *
       * @param userId The ID of the user to whose lock screen the platform key must be bound.
       * @param generationId The generation ID.
       * @return The alias.
       */
      private String getDecryptAlias(int userId, int generationId) {
          return KEY_ALIAS_PREFIX + userId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX;
      }
  
      /**
       * Sets the current generation ID to {@code generationId}.
       * @throws IOException if there was an issue with local database update.
       */
      private void setGenerationId(int userId, int generationId) throws IOException {
          mDatabase.setPlatformKeyGenerationId(userId, generationId);
      }
  
      /**
       * Returns {@code true} if a key has been loaded with the given {@code generationId} into
       * AndroidKeyStore.
       *
       * @throws KeyStoreException if there was an error checking AndroidKeyStore.
       */
      private boolean isKeyLoaded(int userId, int generationId) throws KeyStoreException {
          return mKeyStore.containsAlias(getEncryptAlias(userId, generationId))
                  && mKeyStore.containsAlias(getDecryptAlias(userId, generationId));
      }
  
      @VisibleForTesting
      IGateKeeperService getGateKeeperService() {
          return GateKeeper.getService();
      }
  
      /**
       * Generates a new 256-bit AES key, and loads it into AndroidKeyStore with the given
       * {@code generationId} determining its aliases.
       *
       * @throws NoSuchAlgorithmException if AES is unavailable. This should never happen, as it is
       *     available since API version 1.
       * @throws KeyStoreException if there was an issue loading the keys into AndroidKeyStore.
       * @throws IOException if there was an issue with local database update.
       * @throws RemoteException if there was an issue communicating with {@link IGateKeeperService}.
       */
      private void generateAndLoadKey(int userId, int generationId)
              throws NoSuchAlgorithmException, KeyStoreException, IOException, RemoteException {
          String encryptAlias = getEncryptAlias(userId, generationId);
          String decryptAlias = getDecryptAlias(userId, generationId);
          // SecretKey implementation doesn't provide reliable way to destroy the secret
          // so it may live in memory for some time.
          SecretKey secretKey = generateAesKey();
  
          KeyProtection.Builder decryptionKeyProtection =
                  new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
                      .setUserAuthenticationRequired(true)
                      .setUserAuthenticationValidityDurationSeconds(
                              USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS)
                      .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                      .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
          if (userId != UserHandle.USER_SYSTEM) {
              // Bind decryption key to secondary profile lock screen secret.
              long secureUserId = getGateKeeperService().getSecureUserId(userId);
              // TODO(b/124095438): Propagate this failure instead of silently failing.
              if (secureUserId == GateKeeper.INVALID_SECURE_USER_ID) {
                  Log.e(TAG, "No SID available for user " + userId);
                  return;
              }
              decryptionKeyProtection
                      .setBoundToSpecificSecureUserId(secureUserId)
                      // Ignore caller uid which always belongs to the primary profile.
                      .setCriticalToDeviceEncryption(true);
          }
          // Store decryption key first since it is more likely to fail.
          mKeyStore.setEntry(
                  decryptAlias,
                  new KeyStore.SecretKeyEntry(secretKey),
                  decryptionKeyProtection.build());
          mKeyStore.setEntry(
                  encryptAlias,
                  new KeyStore.SecretKeyEntry(secretKey),
                  new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
                      .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                      .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                      .build());
  
          setGenerationId(userId, generationId);
      }
  
      /**
       * Generates a new 256-bit AES key, in software.
       *
       * @return The software-generated AES key.
       * @throws NoSuchAlgorithmException if AES key generation is not available. This should never
       *     happen, as AES has been supported since API level 1.
       */
      private static SecretKey generateAesKey() throws NoSuchAlgorithmException {
          KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
          keyGenerator.init(KEY_SIZE_BITS);
          return keyGenerator.generateKey();
      }
  
      /**
       * Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked
       * {@link KeyStore#load(KeyStore.LoadStoreParameter)}.
       *
       * @throws KeyStoreException if there was a problem getting or initializing the key store.
       */
      private static KeyStore getAndLoadAndroidKeyStore() throws KeyStoreException {
          KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
          try {
              keyStore.load(/*param=*/ null);
          } catch (CertificateException | IOException | NoSuchAlgorithmException e) {
              // Should never happen.
              throw new KeyStoreException("Unable to load keystore.", e);
          }
          return keyStore;
      }
  
  }
  