diff options
Diffstat (limited to 'keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java')
-rw-r--r-- | keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java b/keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java new file mode 100644 index 0000000..f412743 --- /dev/null +++ b/keystore/java/android/security/keystore/AndroidKeyStoreAuthenticatedAESCipherSpi.java @@ -0,0 +1,442 @@ +/* + * 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 { + private 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; + } + + 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.addInt(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.addInt(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_AES); + keymasterArgs.addInt(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode); + keymasterArgs.addInt(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding); + if (mIv != null) { + keymasterArgs.addBlob(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.getBlob(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[] additionalEntropy) throws KeyStoreException { + byte[] output = mDelegate.doFinal(input, inputOffset, inputLength, 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.addBlob(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[] 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 + ); + } + } +}
\ No newline at end of file |