/* * 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.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 android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.Stream; import libcore.util.EmptyArray; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.ProviderException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidParameterSpecException; import java.util.Arrays; import javax.crypto.CipherSpi; import javax.crypto.spec.GCMParameterSpec; /** * Base class for Android Keystore authenticated AES {@link CipherSpi} implementations. * * @hide */ abstract class AndroidKeyStoreAuthenticatedAESCipherSpi extends AndroidKeyStoreCipherSpiBase { abstract static class GCM extends AndroidKeyStoreAuthenticatedAESCipherSpi { static final int MIN_SUPPORTED_TAG_LENGTH_BITS = 96; private static final int MAX_SUPPORTED_TAG_LENGTH_BITS = 128; private static final int DEFAULT_TAG_LENGTH_BITS = 128; private static final int IV_LENGTH_BYTES = 12; private int mTagLengthBits = DEFAULT_TAG_LENGTH_BITS; GCM(int keymasterPadding) { super(KeymasterDefs.KM_MODE_GCM, keymasterPadding); } @Override protected final void resetAll() { mTagLengthBits = DEFAULT_TAG_LENGTH_BITS; super.resetAll(); } @Override protected final void resetWhilePreservingInitState() { super.resetWhilePreservingInitState(); } @Override protected final void initAlgorithmSpecificParameters() throws InvalidKeyException { if (!isEncrypting()) { throw new InvalidKeyException("IV required when decrypting" + ". Use IvParameterSpec or AlgorithmParameters to provide it."); } } @Override protected final void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) throws InvalidAlgorithmParameterException { // IV is used if (params == null) { if (!isEncrypting()) { // IV must be provided by the caller throw new InvalidAlgorithmParameterException( "GCMParameterSpec must be provided when decrypting"); } return; } if (!(params instanceof GCMParameterSpec)) { throw new InvalidAlgorithmParameterException("Only GCMParameterSpec supported"); } GCMParameterSpec spec = (GCMParameterSpec) params; byte[] iv = spec.getIV(); if (iv == null) { throw new InvalidAlgorithmParameterException("Null IV in GCMParameterSpec"); } else if (iv.length != IV_LENGTH_BYTES) { throw new InvalidAlgorithmParameterException("Unsupported IV length: " + iv.length + " bytes. Only " + IV_LENGTH_BYTES + " bytes long IV supported"); } int tagLengthBits = spec.getTLen(); if ((tagLengthBits < MIN_SUPPORTED_TAG_LENGTH_BITS) || (tagLengthBits > MAX_SUPPORTED_TAG_LENGTH_BITS) || ((tagLengthBits % 8) != 0)) { throw new InvalidAlgorithmParameterException( "Unsupported tag length: " + tagLengthBits + " bits" + ". Supported lengths: 96, 104, 112, 120, 128"); } setIv(iv); mTagLengthBits = tagLengthBits; } @Override protected final void initAlgorithmSpecificParameters(AlgorithmParameters params) throws InvalidAlgorithmParameterException { if (params == null) { if (!isEncrypting()) { // IV must be provided by the caller throw new InvalidAlgorithmParameterException("IV required when decrypting" + ". Use GCMParameterSpec or GCM AlgorithmParameters to provide it."); } return; } if (!"GCM".equalsIgnoreCase(params.getAlgorithm())) { throw new InvalidAlgorithmParameterException( "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm() + ". Supported: GCM"); } GCMParameterSpec spec; try { spec = params.getParameterSpec(GCMParameterSpec.class); } catch (InvalidParameterSpecException e) { if (!isEncrypting()) { // IV must be provided by the caller throw new InvalidAlgorithmParameterException("IV and tag length required when" + " decrypting, but not found in parameters: " + params, e); } setIv(null); return; } initAlgorithmSpecificParameters(spec); } @Nullable @Override protected final AlgorithmParameters engineGetParameters() { byte[] iv = getIv(); if ((iv != null) && (iv.length > 0)) { try { AlgorithmParameters params = AlgorithmParameters.getInstance("GCM"); params.init(new GCMParameterSpec(mTagLengthBits, iv)); return params; } catch (NoSuchAlgorithmException e) { throw new ProviderException( "Failed to obtain GCM AlgorithmParameters", e); } catch (InvalidParameterSpecException e) { throw new ProviderException( "Failed to initialize GCM AlgorithmParameters", e); } } return null; } @NonNull @Override protected KeyStoreCryptoOperationStreamer createMainDataStreamer( KeyStore keyStore, IBinder operationToken) { KeyStoreCryptoOperationStreamer streamer = new KeyStoreCryptoOperationChunkedStreamer( new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( keyStore, operationToken)); if (isEncrypting()) { return streamer; } else { // When decrypting, to avoid leaking unauthenticated plaintext, do not return any // plaintext before ciphertext is authenticated by KeyStore.finish. return new BufferAllOutputUntilDoFinalStreamer(streamer); } } @NonNull @Override protected final KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer( KeyStore keyStore, IBinder operationToken) { return new KeyStoreCryptoOperationChunkedStreamer( new AdditionalAuthenticationDataStream(keyStore, operationToken)); } @Override protected final int getAdditionalEntropyAmountForBegin() { if ((getIv() == null) && (isEncrypting())) { // IV will need to be generated return IV_LENGTH_BYTES; } return 0; } @Override protected final int getAdditionalEntropyAmountForFinish() { return 0; } @Override protected final void addAlgorithmSpecificParametersToBegin( @NonNull KeymasterArguments keymasterArgs) { super.addAlgorithmSpecificParametersToBegin(keymasterArgs); keymasterArgs.addUnsignedInt(KeymasterDefs.KM_TAG_MAC_LENGTH, mTagLengthBits); } protected final int getTagLengthBits() { return mTagLengthBits; } public static final class NoPadding extends GCM { public NoPadding() { super(KeymasterDefs.KM_PAD_NONE); } @Override protected final int engineGetOutputSize(int inputLen) { int tagLengthBytes = (getTagLengthBits() + 7) / 8; long result; if (isEncrypting()) { result = getConsumedInputSizeBytes() - getProducedOutputSizeBytes() + inputLen + tagLengthBytes; } else { result = getConsumedInputSizeBytes() - getProducedOutputSizeBytes() + inputLen - tagLengthBytes; } if (result < 0) { return 0; } else if (result > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } return (int) result; } } } private static final int BLOCK_SIZE_BYTES = 16; private final int mKeymasterBlockMode; private final int mKeymasterPadding; private byte[] mIv; /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */ private boolean mIvHasBeenUsed; AndroidKeyStoreAuthenticatedAESCipherSpi( int keymasterBlockMode, int keymasterPadding) { mKeymasterBlockMode = keymasterBlockMode; mKeymasterPadding = keymasterPadding; } @Override protected void resetAll() { mIv = null; mIvHasBeenUsed = false; super.resetAll(); } @Override protected final void initKey(int opmode, Key key) throws InvalidKeyException { if (!(key instanceof AndroidKeyStoreSecretKey)) { throw new InvalidKeyException( "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); } if (!KeyProperties.KEY_ALGORITHM_AES.equalsIgnoreCase(key.getAlgorithm())) { throw new InvalidKeyException( "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " + KeyProperties.KEY_ALGORITHM_AES + " supported"); } setKey((AndroidKeyStoreSecretKey) key); } @Override protected void addAlgorithmSpecificParametersToBegin( @NonNull KeymasterArguments keymasterArgs) { if ((isEncrypting()) && (mIvHasBeenUsed)) { // IV is being reused for encryption: this violates security best practices. throw new IllegalStateException( "IV has already been used. Reusing IV in encryption mode violates security best" + " practices."); } keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_AES); keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); if (mIv != null) { keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv); } } @Override protected final void loadAlgorithmSpecificParametersFromBeginResult( @NonNull KeymasterArguments keymasterArgs) { mIvHasBeenUsed = true; // NOTE: Keymaster doesn't always return an IV, even if it's used. byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null); if ((returnedIv != null) && (returnedIv.length == 0)) { returnedIv = null; } if (mIv == null) { mIv = returnedIv; } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { throw new ProviderException("IV in use differs from provided IV"); } } @Override protected final int engineGetBlockSize() { return BLOCK_SIZE_BYTES; } @Override protected final byte[] engineGetIV() { return ArrayUtils.cloneIfNotEmpty(mIv); } protected void setIv(byte[] iv) { mIv = iv; } protected byte[] getIv() { return mIv; } /** * {@link KeyStoreCryptoOperationStreamer} which buffers all output until {@code doFinal} from * which it returns all output in one go, provided {@code doFinal} succeeds. */ private static class BufferAllOutputUntilDoFinalStreamer implements KeyStoreCryptoOperationStreamer { private final KeyStoreCryptoOperationStreamer mDelegate; private ByteArrayOutputStream mBufferedOutput = new ByteArrayOutputStream(); private long mProducedOutputSizeBytes; private BufferAllOutputUntilDoFinalStreamer(KeyStoreCryptoOperationStreamer delegate) { mDelegate = delegate; } @Override public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException { byte[] output = mDelegate.update(input, inputOffset, inputLength); if (output != null) { try { mBufferedOutput.write(output); } catch (IOException e) { throw new ProviderException("Failed to buffer output", e); } } return EmptyArray.BYTE; } @Override public byte[] doFinal(byte[] input, int inputOffset, int inputLength, byte[] signature, byte[] additionalEntropy) throws KeyStoreException { byte[] output = mDelegate.doFinal(input, inputOffset, inputLength, signature, additionalEntropy); if (output != null) { try { mBufferedOutput.write(output); } catch (IOException e) { throw new ProviderException("Failed to buffer output", e); } } byte[] result = mBufferedOutput.toByteArray(); mBufferedOutput.reset(); mProducedOutputSizeBytes += result.length; return result; } @Override public long getConsumedInputSizeBytes() { return mDelegate.getConsumedInputSizeBytes(); } @Override public long getProducedOutputSizeBytes() { return mProducedOutputSizeBytes; } } /** * Additional Authentication Data (AAD) stream via a KeyStore streaming operation. This stream * sends AAD into the KeyStore. */ private static class AdditionalAuthenticationDataStream implements Stream { private final KeyStore mKeyStore; private final IBinder mOperationToken; private AdditionalAuthenticationDataStream(KeyStore keyStore, IBinder operationToken) { mKeyStore = keyStore; mOperationToken = operationToken; } @Override public OperationResult update(byte[] input) { KeymasterArguments keymasterArgs = new KeymasterArguments(); keymasterArgs.addBytes(KeymasterDefs.KM_TAG_ASSOCIATED_DATA, input); // KeyStore does not reflect AAD in inputConsumed, but users of Stream rely on this // field. We fix this discrepancy here. KeyStore.update contract is that all of AAD // has been consumed if the method succeeds. OperationResult result = mKeyStore.update(mOperationToken, keymasterArgs, null); if (result.resultCode == KeyStore.NO_ERROR) { result = new OperationResult( result.resultCode, result.token, result.operationHandle, input.length, // inputConsumed result.output, result.outParams); } return result; } @Override public OperationResult finish(byte[] signature, byte[] additionalEntropy) { if ((additionalEntropy != null) && (additionalEntropy.length > 0)) { throw new ProviderException("AAD stream does not support additional entropy"); } return new OperationResult( KeyStore.NO_ERROR, mOperationToken, 0, // operation handle -- nobody cares about this being returned from finish 0, // inputConsumed EmptyArray.BYTE, // output new KeymasterArguments() // additional params returned by finish ); } } }