content: /*
   * Copyright (C) 2015 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 android.security.keystore;
  
  import android.annotation.CallSuper;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.os.IBinder;
  import android.security.KeyStore;
  import android.security.KeyStoreException;
  import android.security.keymaster.KeymasterArguments;
  import android.security.keymaster.KeymasterDefs;
  import android.security.keymaster.OperationResult;
  
  import libcore.util.EmptyArray;
  
  import java.nio.BufferOverflowException;
  import java.nio.ByteBuffer;
  import java.security.AlgorithmParameters;
  import java.security.GeneralSecurityException;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.InvalidParameterException;
  import java.security.Key;
  import java.security.KeyFactory;
  import java.security.NoSuchAlgorithmException;
  import java.security.PrivateKey;
  import java.security.ProviderException;
  import java.security.PublicKey;
  import java.security.SecureRandom;
  import java.security.spec.AlgorithmParameterSpec;
  import java.security.spec.InvalidKeySpecException;
  import java.security.spec.PKCS8EncodedKeySpec;
  import java.security.spec.X509EncodedKeySpec;
  
  import javax.crypto.AEADBadTagException;
  import javax.crypto.BadPaddingException;
  import javax.crypto.Cipher;
  import javax.crypto.CipherSpi;
  import javax.crypto.IllegalBlockSizeException;
  import javax.crypto.NoSuchPaddingException;
  import javax.crypto.SecretKey;
  import javax.crypto.SecretKeyFactory;
  import javax.crypto.ShortBufferException;
  import javax.crypto.spec.SecretKeySpec;
  
  /**
   * Base class for {@link CipherSpi} implementations of Android KeyStore backed ciphers.
   *
   * @hide
   */
  abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStoreCryptoOperation {
      private final KeyStore mKeyStore;
  
      // Fields below are populated by Cipher.init and KeyStore.begin and should be preserved after
      // doFinal finishes.
      private boolean mEncrypting;
      private int mKeymasterPurposeOverride = -1;
      private AndroidKeyStoreKey mKey;
      private SecureRandom mRng;
  
      /**
       * Token referencing this operation inside keystore service. It is initialized by
       * {@code engineInit} and is invalidated when {@code engineDoFinal} succeeds and on some error
       * conditions in between.
       */
      private IBinder mOperationToken;
      private long mOperationHandle;
      private KeyStoreCryptoOperationStreamer mMainDataStreamer;
      private KeyStoreCryptoOperationStreamer mAdditionalAuthenticationDataStreamer;
      private boolean mAdditionalAuthenticationDataStreamerClosed;
  
      /**
       * Encountered exception which could not be immediately thrown because it was encountered inside
       * a method that does not throw checked exception. This exception will be thrown from
       * {@code engineDoFinal}. Once such an exception is encountered, {@code engineUpdate} and
       * {@code engineDoFinal} start ignoring input data.
       */
      private Exception mCachedException;
  
      AndroidKeyStoreCipherSpiBase() {
          mKeyStore = KeyStore.getInstance();
      }
  
      @Override
      protected final void engineInit(int opmode, Key key, SecureRandom random)
              throws InvalidKeyException {
          resetAll();
  
          boolean success = false;
          try {
              init(opmode, key, random);
              initAlgorithmSpecificParameters();
              try {
                  ensureKeystoreOperationInitialized();
              } catch (InvalidAlgorithmParameterException e) {
                  throw new InvalidKeyException(e);
              }
              success = true;
          } finally {
              if (!success) {
                  resetAll();
              }
          }
      }
  
      @Override
      protected final void engineInit(int opmode, Key key, AlgorithmParameters params,
              SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException {
          resetAll();
  
          boolean success = false;
          try {
              init(opmode, key, random);
              initAlgorithmSpecificParameters(params);
              ensureKeystoreOperationInitialized();
              success = true;
          } finally {
              if (!success) {
                  resetAll();
              }
          }
      }
  
      @Override
      protected final void engineInit(int opmode, Key key, AlgorithmParameterSpec params,
              SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException {
          resetAll();
  
          boolean success = false;
          try {
              init(opmode, key, random);
              initAlgorithmSpecificParameters(params);
              ensureKeystoreOperationInitialized();
              success = true;
          } finally {
              if (!success) {
                  resetAll();
              }
          }
      }
  
      private void init(int opmode, Key key, SecureRandom random) throws InvalidKeyException {
          switch (opmode) {
              case Cipher.ENCRYPT_MODE:
              case Cipher.WRAP_MODE:
                  mEncrypting = true;
                  break;
              case Cipher.DECRYPT_MODE:
              case Cipher.UNWRAP_MODE:
                  mEncrypting = false;
                  break;
              default:
                  throw new InvalidParameterException("Unsupported opmode: " + opmode);
          }
          initKey(opmode, key);
          if (mKey == null) {
              throw new ProviderException("initKey did not initialize the key");
          }
          mRng = random;
      }
  
      /**
       * Resets this cipher to its pristine pre-init state. This must be equivalent to obtaining a new
       * cipher instance.
       *
       * <p>Subclasses storing additional state should override this method, reset the additional
       * state, and then chain to superclass.
       */
      @CallSuper
      protected void resetAll() {
          IBinder operationToken = mOperationToken;
          if (operationToken != null) {
              mKeyStore.abort(operationToken);
          }
          mEncrypting = false;
          mKeymasterPurposeOverride = -1;
          mKey = null;
          mRng = null;
          mOperationToken = null;
          mOperationHandle = 0;
          mMainDataStreamer = null;
          mAdditionalAuthenticationDataStreamer = null;
          mAdditionalAuthenticationDataStreamerClosed = false;
          mCachedException = null;
      }
  
      /**
       * Resets this cipher while preserving the initialized state. This must be equivalent to
       * rolling back the cipher's state to just after the most recent {@code engineInit} completed
       * successfully.
       *
       * <p>Subclasses storing additional post-init state should override this method, reset the
       * additional state, and then chain to superclass.
       */
      @CallSuper
      protected void resetWhilePreservingInitState() {
          IBinder operationToken = mOperationToken;
          if (operationToken != null) {
              mKeyStore.abort(operationToken);
          }
          mOperationToken = null;
          mOperationHandle = 0;
          mMainDataStreamer = null;
          mAdditionalAuthenticationDataStreamer = null;
          mAdditionalAuthenticationDataStreamerClosed = false;
          mCachedException = null;
      }
  
      private void ensureKeystoreOperationInitialized() throws InvalidKeyException,
              InvalidAlgorithmParameterException {
          if (mMainDataStreamer != null) {
              return;
          }
          if (mCachedException != null) {
              return;
          }
          if (mKey == null) {
              throw new IllegalStateException("Not initialized");
          }
  
          KeymasterArguments keymasterInputArgs = new KeymasterArguments();
          addAlgorithmSpecificParametersToBegin(keymasterInputArgs);
          byte[] additionalEntropy = KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng(
                  mRng, getAdditionalEntropyAmountForBegin());
  
          int purpose;
          if (mKeymasterPurposeOverride != -1) {
              purpose = mKeymasterPurposeOverride;
          } else {
              purpose = mEncrypting
                      ? KeymasterDefs.KM_PURPOSE_ENCRYPT : KeymasterDefs.KM_PURPOSE_DECRYPT;
          }
          OperationResult opResult = mKeyStore.begin(
                  mKey.getAlias(),
                  purpose,
                  true, // permit aborting this operation if keystore runs out of resources
                  keymasterInputArgs,
                  additionalEntropy,
                  mKey.getUid());
          if (opResult == null) {
              throw new KeyStoreConnectException();
          }
  
          // Store operation token and handle regardless of the error code returned by KeyStore to
          // ensure that the operation gets aborted immediately if the code below throws an exception.
          mOperationToken = opResult.token;
          mOperationHandle = opResult.operationHandle;
  
          // If necessary, throw an exception due to KeyStore operation having failed.
          GeneralSecurityException e = KeyStoreCryptoOperationUtils.getExceptionForCipherInit(
                  mKeyStore, mKey, opResult.resultCode);
          if (e != null) {
              if (e instanceof InvalidKeyException) {
                  throw (InvalidKeyException) e;
              } else if (e instanceof InvalidAlgorithmParameterException) {
                  throw (InvalidAlgorithmParameterException) e;
              } else {
                  throw new ProviderException("Unexpected exception type", e);
              }
          }
  
          if (mOperationToken == null) {
              throw new ProviderException("Keystore returned null operation token");
          }
          if (mOperationHandle == 0) {
              throw new ProviderException("Keystore returned invalid operation handle");
          }
  
          loadAlgorithmSpecificParametersFromBeginResult(opResult.outParams);
          mMainDataStreamer = createMainDataStreamer(mKeyStore, opResult.token);
          mAdditionalAuthenticationDataStreamer =
                  createAdditionalAuthenticationDataStreamer(mKeyStore, opResult.token);
          mAdditionalAuthenticationDataStreamerClosed = false;
      }
  
      /**
       * Creates a streamer which sends plaintext/ciphertext into the provided KeyStore and receives
       * the corresponding ciphertext/plaintext from the KeyStore.
       *
       * <p>This implementation returns a working streamer.
       */
      @NonNull
      protected KeyStoreCryptoOperationStreamer createMainDataStreamer(
              KeyStore keyStore, IBinder operationToken) {
          return new KeyStoreCryptoOperationChunkedStreamer(
                  new KeyStoreCryptoOperationChunkedStreamer.MainDataStream(
                          keyStore, operationToken));
      }
  
      /**
       * Creates a streamer which sends Additional Authentication Data (AAD) into the KeyStore.
       *
       * <p>This implementation returns {@code null}.
       *
       * @return stream or {@code null} if AAD is not supported by this cipher.
       */
      @Nullable
      protected KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer(
              @SuppressWarnings("unused") KeyStore keyStore,
              @SuppressWarnings("unused") IBinder operationToken) {
          return null;
      }
  
      @Override
      protected final byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
          if (mCachedException != null) {
              return null;
          }
          try {
              ensureKeystoreOperationInitialized();
          } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
              mCachedException = e;
              return null;
          }
  
          if (inputLen == 0) {
              return null;
          }
  
          byte[] output;
          try {
              flushAAD();
              output = mMainDataStreamer.update(input, inputOffset, inputLen);
          } catch (KeyStoreException e) {
              mCachedException = e;
              return null;
          }
  
          if (output.length == 0) {
              return null;
          }
  
          return output;
      }
  
      private void flushAAD() throws KeyStoreException {
          if ((mAdditionalAuthenticationDataStreamer != null)
                  && (!mAdditionalAuthenticationDataStreamerClosed)) {
              byte[] output;
              try {
                  output = mAdditionalAuthenticationDataStreamer.doFinal(
                          EmptyArray.BYTE, 0, 0,
                          null, // no signature
                          null // no additional entropy needed flushing AAD
                          );
              } finally {
                  mAdditionalAuthenticationDataStreamerClosed = true;
              }
              if ((output != null) && (output.length > 0)) {
                  throw new ProviderException(
                          "AAD update unexpectedly returned data: " + output.length + " bytes");
              }
          }
      }
  
      @Override
      protected final int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output,
              int outputOffset) throws ShortBufferException {
          byte[] outputCopy = engineUpdate(input, inputOffset, inputLen);
          if (outputCopy == null) {
              return 0;
          }
          int outputAvailable = output.length - outputOffset;
          if (outputCopy.length > outputAvailable) {
              throw new ShortBufferException("Output buffer too short. Produced: "
                      + outputCopy.length + ", available: " + outputAvailable);
          }
          System.arraycopy(outputCopy, 0, output, outputOffset, outputCopy.length);
          return outputCopy.length;
      }
  
      @Override
      protected final int engineUpdate(ByteBuffer input, ByteBuffer output)
              throws ShortBufferException {
          if (input == null) {
              throw new NullPointerException("input == null");
          }
          if (output == null) {
              throw new NullPointerException("output == null");
          }
  
          int inputSize = input.remaining();
          byte[] outputArray;
          if (input.hasArray()) {
              outputArray =
                      engineUpdate(
                              input.array(), input.arrayOffset() + input.position(), inputSize);
              input.position(input.position() + inputSize);
          } else {
              byte[] inputArray = new byte[inputSize];
              input.get(inputArray);
              outputArray = engineUpdate(inputArray, 0, inputSize);
          }
  
          int outputSize = (outputArray != null) ? outputArray.length : 0;
          if (outputSize > 0) {
              int outputBufferAvailable = output.remaining();
              try {
                  output.put(outputArray);
              } catch (BufferOverflowException e) {
                  throw new ShortBufferException(
                          "Output buffer too small. Produced: " + outputSize + ", available: "
                                  + outputBufferAvailable);
              }
          }
          return outputSize;
      }
  
      @Override
      protected final void engineUpdateAAD(byte[] input, int inputOffset, int inputLen) {
          if (mCachedException != null) {
              return;
          }
  
          try {
              ensureKeystoreOperationInitialized();
          } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
              mCachedException = e;
              return;
          }
  
          if (mAdditionalAuthenticationDataStreamerClosed) {
              throw new IllegalStateException(
                      "AAD can only be provided before Cipher.update is invoked");
          }
  
          if (mAdditionalAuthenticationDataStreamer == null) {
              throw new IllegalStateException("This cipher does not support AAD");
          }
  
          byte[] output;
          try {
              output = mAdditionalAuthenticationDataStreamer.update(input, inputOffset, inputLen);
          } catch (KeyStoreException e) {
              mCachedException = e;
              return;
          }
  
          if ((output != null) && (output.length > 0)) {
              throw new ProviderException("AAD update unexpectedly produced output: "
                      + output.length + " bytes");
          }
      }
  
      @Override
      protected final void engineUpdateAAD(ByteBuffer src) {
          if (src == null) {
              throw new IllegalArgumentException("src == null");
          }
          if (!src.hasRemaining()) {
              return;
          }
  
          byte[] input;
          int inputOffset;
          int inputLen;
          if (src.hasArray()) {
              input = src.array();
              inputOffset = src.arrayOffset() + src.position();
              inputLen = src.remaining();
              src.position(src.limit());
          } else {
              input = new byte[src.remaining()];
              inputOffset = 0;
              inputLen = input.length;
              src.get(input);
          }
          engineUpdateAAD(input, inputOffset, inputLen);
      }
  
      @Override
      protected final byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
              throws IllegalBlockSizeException, BadPaddingException {
          if (mCachedException != null) {
              throw (IllegalBlockSizeException)
                      new IllegalBlockSizeException().initCause(mCachedException);
          }
  
          try {
              ensureKeystoreOperationInitialized();
          } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
              throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e);
          }
  
          byte[] output;
          try {
              flushAAD();
              byte[] additionalEntropy =
                      KeyStoreCryptoOperationUtils.getRandomBytesToMixIntoKeystoreRng(
                              mRng, getAdditionalEntropyAmountForFinish());
              output = mMainDataStreamer.doFinal(
                      input, inputOffset, inputLen,
                      null, // no signature involved
                      additionalEntropy);
          } catch (KeyStoreException e) {
              switch (e.getErrorCode()) {
                  case KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH:
                      throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e);
                  case KeymasterDefs.KM_ERROR_INVALID_ARGUMENT:
                      throw (BadPaddingException) new BadPaddingException().initCause(e);
                  case KeymasterDefs.KM_ERROR_VERIFICATION_FAILED:
                      throw (AEADBadTagException) new AEADBadTagException().initCause(e);
                  default:
                      throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e);
              }
          }
  
          resetWhilePreservingInitState();
          return output;
      }
  
      @Override
      protected final int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output,
              int outputOffset) throws ShortBufferException, IllegalBlockSizeException,
              BadPaddingException {
          byte[] outputCopy = engineDoFinal(input, inputOffset, inputLen);
          if (outputCopy == null) {
              return 0;
          }
          int outputAvailable = output.length - outputOffset;
          if (outputCopy.length > outputAvailable) {
              throw new ShortBufferException("Output buffer too short. Produced: "
                      + outputCopy.length + ", available: " + outputAvailable);
          }
          System.arraycopy(outputCopy, 0, output, outputOffset, outputCopy.length);
          return outputCopy.length;
      }
  
      @Override
      protected final int engineDoFinal(ByteBuffer input, ByteBuffer output)
              throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
          if (input == null) {
              throw new NullPointerException("input == null");
          }
          if (output == null) {
              throw new NullPointerException("output == null");
          }
  
          int inputSize = input.remaining();
          byte[] outputArray;
          if (input.hasArray()) {
              outputArray =
                      engineDoFinal(
                              input.array(), input.arrayOffset() + input.position(), inputSize);
              input.position(input.position() + inputSize);
          } else {
              byte[] inputArray = new byte[inputSize];
              input.get(inputArray);
              outputArray = engineDoFinal(inputArray, 0, inputSize);
          }
  
          int outputSize = (outputArray != null) ? outputArray.length : 0;
          if (outputSize > 0) {
              int outputBufferAvailable = output.remaining();
              try {
                  output.put(outputArray);
              } catch (BufferOverflowException e) {
                  throw new ShortBufferException(
                          "Output buffer too small. Produced: " + outputSize + ", available: "
                                  + outputBufferAvailable);
              }
          }
          return outputSize;
      }
  
      @Override
      protected final byte[] engineWrap(Key key)
              throws IllegalBlockSizeException, InvalidKeyException {
          if (mKey == null) {
              throw new IllegalStateException("Not initilized");
          }
  
          if (!isEncrypting()) {
              throw new IllegalStateException(
                      "Cipher must be initialized in Cipher.WRAP_MODE to wrap keys");
          }
  
          if (key == null) {
              throw new NullPointerException("key == null");
          }
          byte[] encoded = null;
          if (key instanceof SecretKey) {
              if ("RAW".equalsIgnoreCase(key.getFormat())) {
                  encoded = key.getEncoded();
              }
              if (encoded == null) {
                  try {
                      SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(key.getAlgorithm());
                      SecretKeySpec spec =
                              (SecretKeySpec) keyFactory.getKeySpec(
                                      (SecretKey) key, SecretKeySpec.class);
                      encoded = spec.getEncoded();
                  } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                      throw new InvalidKeyException(
                              "Failed to wrap key because it does not export its key material",
                              e);
                  }
              }
          } else if (key instanceof PrivateKey) {
              if ("PKCS8".equalsIgnoreCase(key.getFormat())) {
                  encoded = key.getEncoded();
              }
              if (encoded == null) {
                  try {
                      KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm());
                      PKCS8EncodedKeySpec spec =
                              keyFactory.getKeySpec(key, PKCS8EncodedKeySpec.class);
                      encoded = spec.getEncoded();
                  } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                      throw new InvalidKeyException(
                              "Failed to wrap key because it does not export its key material",
                              e);
                  }
              }
          } else if (key instanceof PublicKey) {
              if ("X.509".equalsIgnoreCase(key.getFormat())) {
                  encoded = key.getEncoded();
              }
              if (encoded == null) {
                  try {
                      KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm());
                      X509EncodedKeySpec spec =
                              keyFactory.getKeySpec(key, X509EncodedKeySpec.class);
                      encoded = spec.getEncoded();
                  } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                      throw new InvalidKeyException(
                              "Failed to wrap key because it does not export its key material",
                              e);
                  }
              }
          } else {
              throw new InvalidKeyException("Unsupported key type: " + key.getClass().getName());
          }
  
          if (encoded == null) {
              throw new InvalidKeyException(
                      "Failed to wrap key because it does not export its key material");
          }
  
          try {
              return engineDoFinal(encoded, 0, encoded.length);
          } catch (BadPaddingException e) {
              throw (IllegalBlockSizeException) new IllegalBlockSizeException().initCause(e);
          }
      }
  
      @Override
      protected final Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgorithm,
              int wrappedKeyType) throws InvalidKeyException, NoSuchAlgorithmException {
          if (mKey == null) {
              throw new IllegalStateException("Not initilized");
          }
  
          if (isEncrypting()) {
              throw new IllegalStateException(
                      "Cipher must be initialized in Cipher.WRAP_MODE to wrap keys");
          }
  
          if (wrappedKey == null) {
              throw new NullPointerException("wrappedKey == null");
          }
  
          byte[] encoded;
          try {
              encoded = engineDoFinal(wrappedKey, 0, wrappedKey.length);
          } catch (IllegalBlockSizeException | BadPaddingException e) {
              throw new InvalidKeyException("Failed to unwrap key", e);
          }
  
          switch (wrappedKeyType) {
              case Cipher.SECRET_KEY:
              {
                  return new SecretKeySpec(encoded, wrappedKeyAlgorithm);
                  // break;
              }
              case Cipher.PRIVATE_KEY:
              {
                  KeyFactory keyFactory = KeyFactory.getInstance(wrappedKeyAlgorithm);
                  try {
                      return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encoded));
                  } catch (InvalidKeySpecException e) {
                      throw new InvalidKeyException(
                              "Failed to create private key from its PKCS#8 encoded form", e);
                  }
                  // break;
              }
              case Cipher.PUBLIC_KEY:
              {
                  KeyFactory keyFactory = KeyFactory.getInstance(wrappedKeyAlgorithm);
                  try {
                      return keyFactory.generatePublic(new X509EncodedKeySpec(encoded));
                  } catch (InvalidKeySpecException e) {
                      throw new InvalidKeyException(
                              "Failed to create public key from its X.509 encoded form", e);
                  }
                  // break;
              }
              default:
                  throw new InvalidParameterException(
                          "Unsupported wrappedKeyType: " + wrappedKeyType);
          }
      }
  
      @Override
      protected final void engineSetMode(String mode) throws NoSuchAlgorithmException {
          // This should never be invoked because all algorithms registered with the AndroidKeyStore
          // provide explicitly specify block mode.
          throw new UnsupportedOperationException();
      }
  
      @Override
      protected final void engineSetPadding(String arg0) throws NoSuchPaddingException {
          // This should never be invoked because all algorithms registered with the AndroidKeyStore
          // provide explicitly specify padding mode.
          throw new UnsupportedOperationException();
      }
  
      @Override
      protected final int engineGetKeySize(Key key) throws InvalidKeyException {
          throw new UnsupportedOperationException();
      }
  
      @CallSuper
      @Override
      public void finalize() throws Throwable {
          try {
              IBinder operationToken = mOperationToken;
              if (operationToken != null) {
                  mKeyStore.abort(operationToken);
              }
          } finally {
              super.finalize();
          }
      }
  
      @Override
      public final long getOperationHandle() {
          return mOperationHandle;
      }
  
      protected final void setKey(@NonNull AndroidKeyStoreKey key) {
          mKey = key;
      }
  
      /**
       * Overrides the default purpose/type of the crypto operation.
       */
      protected final void setKeymasterPurposeOverride(int keymasterPurpose) {
          mKeymasterPurposeOverride = keymasterPurpose;
      }
  
      protected final int getKeymasterPurposeOverride() {
          return mKeymasterPurposeOverride;
      }
  
      /**
       * Returns {@code true} if this cipher is initialized for encryption, {@code false} if this
       * cipher is initialized for decryption.
       */
      protected final boolean isEncrypting() {
          return mEncrypting;
      }
  
      @NonNull
      protected final KeyStore getKeyStore() {
          return mKeyStore;
      }
  
      protected final long getConsumedInputSizeBytes() {
          if (mMainDataStreamer == null) {
              throw new IllegalStateException("Not initialized");
          }
          return mMainDataStreamer.getConsumedInputSizeBytes();
      }
  
      protected final long getProducedOutputSizeBytes() {
          if (mMainDataStreamer == null) {
              throw new IllegalStateException("Not initialized");
          }
          return mMainDataStreamer.getProducedOutputSizeBytes();
      }
  
      static String opmodeToString(int opmode) {
          switch (opmode) {
              case Cipher.ENCRYPT_MODE:
                  return "ENCRYPT_MODE";
              case Cipher.DECRYPT_MODE:
                  return "DECRYPT_MODE";
              case Cipher.WRAP_MODE:
                  return "WRAP_MODE";
              case Cipher.UNWRAP_MODE:
                  return "UNWRAP_MODE";
              default:
                  return String.valueOf(opmode);
          }
      }
  
      // The methods below need to be implemented by subclasses.
  
      /**
       * Initializes this cipher with the provided key.
       *
       * @throws InvalidKeyException if the {@code key} is not suitable for this cipher in the
       *         specified {@code opmode}.
       *
       * @see #setKey(AndroidKeyStoreKey)
       */
      protected abstract void initKey(int opmode, @Nullable Key key) throws InvalidKeyException;
  
      /**
       * Returns algorithm-specific parameters used by this cipher or {@code null} if no
       * algorithm-specific parameters are used.
       */
      @Nullable
      @Override
      protected abstract AlgorithmParameters engineGetParameters();
  
      /**
       * Invoked by {@code engineInit} to initialize algorithm-specific parameters when no additional
       * initialization parameters were provided.
       *
       * @throws InvalidKeyException if this cipher cannot be configured based purely on the provided
       *         key and needs additional parameters to be provided to {@code Cipher.init}.
       */
      protected abstract void initAlgorithmSpecificParameters() throws InvalidKeyException;
  
      /**
       * Invoked by {@code engineInit} to initialize algorithm-specific parameters when additional
       * parameters were provided.
       *
       * @param params additional algorithm parameters or {@code null} if not specified.
       *
       * @throws InvalidAlgorithmParameterException if there is insufficient information to configure
       *         this cipher or if the provided parameters are not suitable for this cipher.
       */
      protected abstract void initAlgorithmSpecificParameters(
              @Nullable AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException;
  
      /**
       * Invoked by {@code engineInit} to initialize algorithm-specific parameters when additional
       * parameters were provided.
       *
       * @param params additional algorithm parameters or {@code null} if not specified.
       *
       * @throws InvalidAlgorithmParameterException if there is insufficient information to configure
       *         this cipher or if the provided parameters are not suitable for this cipher.
       */
      protected abstract void initAlgorithmSpecificParameters(@Nullable AlgorithmParameters params)
              throws InvalidAlgorithmParameterException;
  
      /**
       * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's
       * {@code begin} operation. This amount of entropy is typically what's consumed to generate
       * random parameters, such as IV.
       *
       * <p>For decryption, the return value should be {@code 0} because decryption should not be
       * consuming any entropy. For encryption, the value combined with
       * {@link #getAdditionalEntropyAmountForFinish()} should match (or exceed) the amount of Shannon
       * entropy of the ciphertext produced by this cipher assuming the key, the plaintext, and all
       * explicitly provided parameters to {@code Cipher.init} are known. For example, for AES CBC
       * encryption with an explicitly provided IV the return value should be {@code 0}, whereas for
       * the case where IV is generated by the KeyStore's {@code begin} operation it should be
       * {@code 16}.
       */
      protected abstract int getAdditionalEntropyAmountForBegin();
  
      /**
       * Returns the amount of additional entropy (in bytes) to be provided to the KeyStore's
       * {@code finish} operation. This amount of entropy is typically what's consumed by encryption
       * padding scheme.
       *
       * <p>For decryption, the return value should be {@code 0} because decryption should not be
       * consuming any entropy. For encryption, the value combined with
       * {@link #getAdditionalEntropyAmountForBegin()} should match (or exceed) the amount of Shannon
       * entropy of the ciphertext produced by this cipher assuming the key, the plaintext, and all
       * explicitly provided parameters to {@code Cipher.init} are known. For example, for RSA with
       * OAEP the return value should be the size of the OAEP hash output. For RSA with PKCS#1 padding
       * the return value should be the size of the padding string or could be raised (for simplicity)
       * to the size of the modulus.
       */
      protected abstract int getAdditionalEntropyAmountForFinish();
  
      /**
       * Invoked to add algorithm-specific parameters for the KeyStore's {@code begin} operation.
       *
       * @param keymasterArgs keystore/keymaster arguments to be populated with algorithm-specific
       *        parameters.
       */
      protected abstract void addAlgorithmSpecificParametersToBegin(
              @NonNull KeymasterArguments keymasterArgs);
  
      /**
       * Invoked to obtain algorithm-specific parameters from the result of the KeyStore's
       * {@code begin} operation.
       *
       * <p>Some parameters, such as IV, are not required to be provided to {@code Cipher.init}. Such
       * parameters, if not provided, must be generated by KeyStore and returned to the user of
       * {@code Cipher} and potentially reused after {@code doFinal}.
       *
       * @param keymasterArgs keystore/keymaster arguments returned by KeyStore {@code begin}
       *        operation.
       */
      protected abstract void loadAlgorithmSpecificParametersFromBeginResult(
              @NonNull KeymasterArguments keymasterArgs);
  }
  