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.annotation.Nullable;
  import android.security.keystore.recovery.RecoveryController;
  import android.util.Log;
  import android.util.Pair;
  
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.KeyStoreException;
  import java.security.NoSuchAlgorithmException;
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.Map;
  
  import javax.crypto.Cipher;
  import javax.crypto.IllegalBlockSizeException;
  import javax.crypto.NoSuchPaddingException;
  import javax.crypto.SecretKey;
  import javax.crypto.spec.GCMParameterSpec;
  
  /**
   * A {@link javax.crypto.SecretKey} wrapped with AES/GCM/NoPadding.
   *
   * @hide
   */
  public class WrappedKey {
      private static final String TAG = "WrappedKey";
  
      private static final String KEY_WRAP_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
      private static final String APPLICATION_KEY_ALGORITHM = "AES";
      private static final int GCM_TAG_LENGTH_BITS = 128;
  
      private final int mPlatformKeyGenerationId;
      private final int mRecoveryStatus;
      private final byte[] mNonce;
      private final byte[] mKeyMaterial;
      private final byte[] mKeyMetadata;
  
      /**
       * Returns a wrapped form of {@code key}, using {@code wrappingKey} to encrypt the key material.
       *
       * @throws InvalidKeyException if {@code wrappingKey} cannot be used to encrypt {@code key}, or
       *     if {@code key} does not expose its key material. See
       *     {@link android.security.keystore.AndroidKeyStoreKey} for an example of a key that does
       *     not expose its key material.
       */
      public static WrappedKey fromSecretKey(PlatformEncryptionKey wrappingKey, SecretKey key,
              @Nullable byte[] metadata)
              throws InvalidKeyException, KeyStoreException {
          if (key.getEncoded() == null) {
              throw new InvalidKeyException(
                      "key does not expose encoded material. It cannot be wrapped.");
          }
  
          Cipher cipher;
          try {
              cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
          } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
              throw new RuntimeException(
                      "Android does not support AES/GCM/NoPadding. This should never happen.");
          }
  
          cipher.init(Cipher.WRAP_MODE, wrappingKey.getKey());
          byte[] encryptedKeyMaterial;
          try {
              encryptedKeyMaterial = cipher.wrap(key);
          } catch (IllegalBlockSizeException e) {
              Throwable cause = e.getCause();
              if (cause instanceof KeyStoreException) {
                  // If AndroidKeyStore encounters any error here, it throws IllegalBlockSizeException
                  // with KeyStoreException as the cause. This is due to there being no better option
                  // here, as the Cipher#wrap only checked throws InvalidKeyException or
                  // IllegalBlockSizeException. If this is the case, we want to propagate it to the
                  // caller, so rethrow the cause.
                  throw (KeyStoreException) cause;
              } else {
                  throw new RuntimeException(
                          "IllegalBlockSizeException should not be thrown by AES/GCM/NoPadding mode.",
                          e);
              }
          }
  
          return new WrappedKey(
                  /*nonce=*/ cipher.getIV(),
                  /*keyMaterial=*/ encryptedKeyMaterial,
                  /*keyMetadata=*/ metadata,
                  /*platformKeyGenerationId=*/ wrappingKey.getGenerationId(),
                  RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
      }
  
      /**
       * A new instance with default recovery status.
       *
       * @param nonce The nonce with which the key material was encrypted.
       * @param keyMaterial The encrypted bytes of the key material.
       * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
       *
       * @see RecoveryController#RECOVERY_STATUS_SYNC_IN_PROGRESS
       * @hide
       */
      public WrappedKey(byte[] nonce, byte[] keyMaterial, @Nullable byte[] keyMetadata,
              int platformKeyGenerationId) {
          this(nonce, keyMaterial, keyMetadata, platformKeyGenerationId,
                  RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
      }
  
      /**
       * A new instance.
       *
       * @param nonce The nonce with which the key material was encrypted.
       * @param keyMaterial The encrypted bytes of the key material.
       * @param keyMetadata The metadata that will be authenticated (but unencrypted) together with
       *     the key material when the key is uploaded to cloud.
       * @param platformKeyGenerationId The generation ID of the key used to wrap this key.
       * @param recoveryStatus recovery status of the key.
       *
       * @hide
       */
      public WrappedKey(byte[] nonce, byte[] keyMaterial, @Nullable byte[] keyMetadata,
              int platformKeyGenerationId, int recoveryStatus) {
          mNonce = nonce;
          mKeyMaterial = keyMaterial;
          mKeyMetadata = keyMetadata;
          mPlatformKeyGenerationId = platformKeyGenerationId;
          mRecoveryStatus = recoveryStatus;
      }
  
      /**
       * Returns the nonce with which the key material was encrypted.
       *
       * @hide
       */
      public byte[] getNonce() {
          return mNonce;
      }
  
      /**
       * Returns the encrypted key material.
       *
       * @hide
       */
      public byte[] getKeyMaterial() {
          return mKeyMaterial;
      }
  
      /**
       * Returns the key metadata.
       *
       * @hide
       */
      public @Nullable byte[] getKeyMetadata() {
          return mKeyMetadata;
      }
  
      /**
       * Returns the generation ID of the platform key, with which this key was wrapped.
       *
       * @hide
       */
      public int getPlatformKeyGenerationId() {
          return mPlatformKeyGenerationId;
      }
  
      /**
       * Returns recovery status of the key.
       *
       * @hide
       */
      public int getRecoveryStatus() {
          return mRecoveryStatus;
      }
  
      /**
       * Unwraps the {@code wrappedKeys} with the {@code platformKey}.
       *
       * @return The unwrapped keys, indexed by alias.
       * @throws NoSuchAlgorithmException if AES/GCM/NoPadding Cipher or AES key type is unavailable.
       * @throws BadPlatformKeyException if the {@code platformKey} has a different generation ID to
       *     any of the {@code wrappedKeys}.
       *
       * @hide
       */
      public static Map<String, Pair<SecretKey, byte[]>> unwrapKeys(
              PlatformDecryptionKey platformKey,
              Map<String, WrappedKey> wrappedKeys)
              throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
              InvalidKeyException, InvalidAlgorithmParameterException {
          HashMap<String, Pair<SecretKey, byte[]>> unwrappedKeys = new HashMap<>();
          Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
          int platformKeyGenerationId = platformKey.getGenerationId();
  
          for (String alias : wrappedKeys.keySet()) {
              WrappedKey wrappedKey = wrappedKeys.get(alias);
              if (wrappedKey.getPlatformKeyGenerationId() != platformKeyGenerationId) {
                  throw new BadPlatformKeyException(String.format(
                          Locale.US,
                          "WrappedKey with alias '%s' was wrapped with platform key %d, not "
                                  + "platform key %d",
                          alias,
                          wrappedKey.getPlatformKeyGenerationId(),
                          platformKey.getGenerationId()));
              }
  
              cipher.init(
                      Cipher.UNWRAP_MODE,
                      platformKey.getKey(),
                      new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
              SecretKey key;
              try {
                  key = (SecretKey) cipher.unwrap(
                          wrappedKey.getKeyMaterial(), APPLICATION_KEY_ALGORITHM, Cipher.SECRET_KEY);
              } catch (InvalidKeyException | NoSuchAlgorithmException e) {
                  Log.e(TAG,
                          String.format(
                                  Locale.US,
                                  "Error unwrapping recoverable key with alias '%s'",
                                  alias),
                          e);
                  continue;
              }
              unwrappedKeys.put(alias, Pair.create(key, wrappedKey.getKeyMetadata()));
          }
  
          return unwrappedKeys;
      }
  }
  