diff options
7 files changed, 758 insertions, 111 deletions
diff --git a/keystore/java/android/security/AndroidKeyStore.java b/keystore/java/android/security/AndroidKeyStore.java index 1d16ca1..846d1f1 100644 --- a/keystore/java/android/security/AndroidKeyStore.java +++ b/keystore/java/android/security/AndroidKeyStore.java @@ -457,7 +457,7 @@ public class AndroidKeyStore extends KeyStoreSpi { String keyAlgorithmString = key.getAlgorithm(); @KeyStoreKeyConstraints.AlgorithmEnum int keyAlgorithm; - @KeyStoreKeyConstraints.AlgorithmEnum Integer digest; + @KeyStoreKeyConstraints.DigestEnum Integer digest; try { keyAlgorithm = KeyStoreKeyConstraints.Algorithm.fromJCASecretKeyAlgorithm(keyAlgorithmString); @@ -493,12 +493,6 @@ public class AndroidKeyStore extends KeyStoreSpi { if (digest != null) { args.addInt(KeymasterDefs.KM_TAG_DIGEST, KeyStoreKeyConstraints.Digest.toKeymaster(digest)); - } - if (keyAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) { - if (digest == null) { - throw new IllegalStateException("Digest algorithm must be specified for key" - + " algorithm " + keyAlgorithmString); - } Integer digestOutputSizeBytes = KeyStoreKeyConstraints.Digest.getOutputSizeBytes(digest); if (digestOutputSizeBytes != null) { @@ -507,6 +501,12 @@ public class AndroidKeyStore extends KeyStoreSpi { args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes); } } + if (keyAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) { + if (digest == null) { + throw new IllegalStateException("Digest algorithm must be specified for key" + + " algorithm " + keyAlgorithmString); + } + } @KeyStoreKeyConstraints.PurposeEnum int purposes = (params.getPurposes() != null) ? params.getPurposes() @@ -560,6 +560,12 @@ public class AndroidKeyStore extends KeyStoreSpi { // TODO: Remove this once keymaster does not require us to specify the size of imported key. args.addInt(KeymasterDefs.KM_TAG_KEY_SIZE, keyMaterial.length * 8); + if (((purposes & KeyStoreKeyConstraints.Purpose.ENCRYPT) != 0) + || ((purposes & KeyStoreKeyConstraints.Purpose.DECRYPT) != 0)) { + // Permit caller-specified IV. This is needed for the Cipher abstraction. + args.addBoolean(KeymasterDefs.KM_TAG_CALLER_NONCE); + } + Credentials.deleteAllTypesForAlias(mKeyStore, entryAlias); String keyAliasInKeystore = Credentials.USER_SECRET_KEY + entryAlias; int errorCode = mKeyStore.importKey( diff --git a/keystore/java/android/security/AndroidKeyStoreProvider.java b/keystore/java/android/security/AndroidKeyStoreProvider.java index 7313c48..6cf9b7a 100644 --- a/keystore/java/android/security/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/AndroidKeyStoreProvider.java @@ -41,7 +41,30 @@ public class AndroidKeyStoreProvider extends Provider { put("KeyGenerator.HmacSHA256", KeyStoreKeyGeneratorSpi.HmacSHA256.class.getName()); // javax.crypto.Mac - put("Mac.HmacSHA256", KeyStoreHmacSpi.HmacSHA256.class.getName()); - put("Mac.HmacSHA256 SupportedKeyClasses", KeyStoreSecretKey.class.getName()); + putMacImpl("HmacSHA256", KeyStoreHmacSpi.HmacSHA256.class.getName()); + + // javax.crypto.Cipher + putSymmetricCipherImpl("AES/ECB/NoPadding", + KeyStoreCipherSpi.AES.ECB.NoPadding.class.getName()); + putSymmetricCipherImpl("AES/ECB/PKCS7Padding", + KeyStoreCipherSpi.AES.ECB.PKCS7Padding.class.getName()); + + putSymmetricCipherImpl("AES/CBC/NoPadding", + KeyStoreCipherSpi.AES.CBC.NoPadding.class.getName()); + putSymmetricCipherImpl("AES/CBC/PKCS7Padding", + KeyStoreCipherSpi.AES.CBC.PKCS7Padding.class.getName()); + + putSymmetricCipherImpl("AES/CTR/NoPadding", + KeyStoreCipherSpi.AES.CTR.NoPadding.class.getName()); + } + + private void putMacImpl(String algorithm, String implClass) { + put("Mac." + algorithm, implClass); + put("Mac." + algorithm + " SupportedKeyClasses", KeyStoreSecretKey.class.getName()); + } + + private void putSymmetricCipherImpl(String transformation, String implClass) { + put("Cipher." + transformation, implClass); + put("Cipher." + transformation + " SupportedKeyClasses", KeyStoreSecretKey.class.getName()); } } diff --git a/keystore/java/android/security/KeyStoreCipherSpi.java b/keystore/java/android/security/KeyStoreCipherSpi.java new file mode 100644 index 0000000..6863236 --- /dev/null +++ b/keystore/java/android/security/KeyStoreCipherSpi.java @@ -0,0 +1,540 @@ +package android.security; + +import android.os.IBinder; +import android.security.keymaster.KeymasterArguments; +import android.security.keymaster.KeymasterDefs; +import android.security.keymaster.OperationResult; + +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; + +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.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; + +/** + * Base class for {@link CipherSpi} providing Android KeyStore backed ciphers. + * + * @hide + */ +public abstract class KeyStoreCipherSpi extends CipherSpi { + + public abstract static class AES extends KeyStoreCipherSpi { + protected AES(@KeyStoreKeyConstraints.BlockModeEnum int blockMode, + @KeyStoreKeyConstraints.PaddingEnum int padding, boolean ivUsed) { + super(KeyStoreKeyConstraints.Algorithm.AES, + blockMode, + padding, + 16, + ivUsed); + } + + public abstract static class ECB extends AES { + protected ECB(@KeyStoreKeyConstraints.PaddingEnum int padding) { + super(KeyStoreKeyConstraints.BlockMode.ECB, padding, false); + } + + public static class NoPadding extends ECB { + public NoPadding() { + super(KeyStoreKeyConstraints.Padding.NONE); + } + } + + public static class PKCS7Padding extends ECB { + public PKCS7Padding() { + super(KeyStoreKeyConstraints.Padding.PKCS7); + } + } + } + + public abstract static class CBC extends AES { + protected CBC(@KeyStoreKeyConstraints.BlockModeEnum int padding) { + super(KeyStoreKeyConstraints.BlockMode.CBC, padding, true); + } + + public static class NoPadding extends CBC { + public NoPadding() { + super(KeyStoreKeyConstraints.Padding.NONE); + } + } + + public static class PKCS7Padding extends CBC { + public PKCS7Padding() { + super(KeyStoreKeyConstraints.Padding.PKCS7); + } + } + } + + public abstract static class CTR extends AES { + protected CTR(@KeyStoreKeyConstraints.BlockModeEnum int padding) { + super(KeyStoreKeyConstraints.BlockMode.CTR, padding, true); + } + + public static class NoPadding extends CTR { + public NoPadding() { + super(KeyStoreKeyConstraints.Padding.NONE); + } + } + } + } + + private final KeyStore mKeyStore; + private final @KeyStoreKeyConstraints.AlgorithmEnum int mAlgorithm; + private final @KeyStoreKeyConstraints.BlockModeEnum int mBlockMode; + private final @KeyStoreKeyConstraints.PaddingEnum int mPadding; + private final int mBlockSizeBytes; + private final boolean mIvUsed; + + // Fields below are populated by Cipher.init and KeyStore.begin and should be preserved after + // doFinal finishes. + protected boolean mEncrypting; + private KeyStoreSecretKey mKey; + private SecureRandom mRng; + private boolean mFirstOperationInitiated; + byte[] mIv; + + // Fields below must be reset + private byte[] mAdditionalEntropyForBegin; + /** + * Token referencing this operation inside keystore service. It is initialized by + * {@code engineInit} and is invalidated when {@code engineDoFinal} succeeds and one some + * error conditions in between. + */ + private IBinder mOperationToken; + private KeyStoreCryptoOperationChunkedStreamer mMainDataStreamer; + + protected KeyStoreCipherSpi( + @KeyStoreKeyConstraints.AlgorithmEnum int algorithm, + @KeyStoreKeyConstraints.BlockModeEnum int blockMode, + @KeyStoreKeyConstraints.PaddingEnum int padding, + int blockSizeBytes, + boolean ivUsed) { + mKeyStore = KeyStore.getInstance(); + mAlgorithm = algorithm; + mBlockMode = blockMode; + mPadding = padding; + mBlockSizeBytes = blockSizeBytes; + mIvUsed = ivUsed; + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + init(opmode, key, random); + initAlgorithmSpecificParameters(); + ensureKeystoreOperationInitialized(); + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + init(opmode, key, random); + initAlgorithmSpecificParameters(params); + ensureKeystoreOperationInitialized(); + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, + SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + init(opmode, key, random); + initAlgorithmSpecificParameters(params); + ensureKeystoreOperationInitialized(); + } + + private void init(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + reset(); + if (!(key instanceof KeyStoreSecretKey)) { + throw new InvalidKeyException( + "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null")); + } + mKey = (KeyStoreSecretKey) key; + mRng = random; + mIv = null; + mFirstOperationInitiated = false; + + if ((opmode != Cipher.ENCRYPT_MODE) && (opmode != Cipher.DECRYPT_MODE)) { + throw new UnsupportedOperationException( + "Only ENCRYPT and DECRYPT modes supported. Mode: " + opmode); + } + mEncrypting = opmode == Cipher.ENCRYPT_MODE; + } + + private void reset() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mOperationToken = null; + mKeyStore.abort(operationToken); + } + mMainDataStreamer = null; + mAdditionalEntropyForBegin = null; + } + + private void ensureKeystoreOperationInitialized() { + if (mMainDataStreamer != null) { + return; + } + if (mKey == null) { + throw new IllegalStateException("Not initialized"); + } + + KeymasterArguments keymasterInputArgs = new KeymasterArguments(); + keymasterInputArgs.addInt(KeymasterDefs.KM_TAG_ALGORITHM, mAlgorithm); + keymasterInputArgs.addInt(KeymasterDefs.KM_TAG_BLOCK_MODE, mBlockMode); + keymasterInputArgs.addInt(KeymasterDefs.KM_TAG_PADDING, mPadding); + addAlgorithmSpecificParametersToBegin(keymasterInputArgs); + + KeymasterArguments keymasterOutputArgs = new KeymasterArguments(); + OperationResult opResult = mKeyStore.begin( + mKey.getAlias(), + mEncrypting ? KeymasterDefs.KM_PURPOSE_ENCRYPT : KeymasterDefs.KM_PURPOSE_DECRYPT, + true, // permit aborting this operation if keystore runs out of resources + keymasterInputArgs, + mAdditionalEntropyForBegin, + keymasterOutputArgs); + mAdditionalEntropyForBegin = null; + if (opResult == null) { + throw new KeyStoreConnectException(); + } else if (opResult.resultCode != KeyStore.NO_ERROR) { + throw new CryptoOperationException("Failed to start keystore operation", + KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode)); + } + + if (opResult.token == null) { + throw new CryptoOperationException("Keystore returned null operation token"); + } + mOperationToken = opResult.token; + loadAlgorithmSpecificParametersFromBeginResult(keymasterOutputArgs); + mFirstOperationInitiated = true; + mMainDataStreamer = new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + mKeyStore, opResult.token)); + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + ensureKeystoreOperationInitialized(); + + if (inputLen == 0) { + return null; + } + + byte[] output; + try { + output = mMainDataStreamer.update(input, inputOffset, inputLen); + } catch (KeymasterException e) { + throw new CryptoOperationException("Keystore operation failed", e); + } + + if (output.length == 0) { + return null; + } + + return output; + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, + int outputOffset) throws ShortBufferException { + ensureKeystoreOperationInitialized(); + + 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 byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + ensureKeystoreOperationInitialized(); + + byte[] output; + try { + output = mMainDataStreamer.doFinal(input, inputOffset, inputLen); + } catch (KeymasterException e) { + switch (e.getErrorCode()) { + case KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH: + throw new IllegalBlockSizeException(); + case KeymasterDefs.KM_ERROR_INVALID_ARGUMENT: + throw new BadPaddingException(); + case KeymasterDefs.KM_ERROR_VERIFICATION_FAILED: + throw new AEADBadTagException(); + default: + throw new CryptoOperationException("Keystore operation failed", e); + } + } + + reset(); + return output; + } + + @Override + protected 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 int engineGetBlockSize() { + return mBlockSizeBytes; + } + + @Override + protected byte[] engineGetIV() { + return (mIv != null) ? mIv.clone() : null; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + return inputLen + 3 * engineGetBlockSize(); + } + + @Override + protected 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 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(); + } + + // The methods below may need to be overridden by subclasses that use algorithm-specific + // parameters. + + /** + * Returns algorithm-specific parameters used by this {@code CipherSpi} instance or {@code null} + * if no algorithm-specific parameters are used. + * + * <p>This implementation only handles the IV parameter. + */ + @Override + protected AlgorithmParameters engineGetParameters() { + if (!mIvUsed) { + return null; + } + if ((mIv != null) && (mIv.length > 0)) { + try { + AlgorithmParameters params = AlgorithmParameters.getInstance("AES"); + params.init(new IvParameterSpec(mIv)); + return params; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to obtain AES AlgorithmParameters", e); + } catch (InvalidParameterSpecException e) { + throw new RuntimeException( + "Failed to initialize AES AlgorithmParameters with an IV", e); + } + } + return null; + } + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters. These parameters + * may need to be stored to be reused after {@code doFinal}. + * + * <p>The default implementation only handles the IV parameters. + * + * @param params algorithm parameters. + * + * @throws InvalidAlgorithmParameterException if some/all of the parameters cannot be + * automatically configured and thus {@code Cipher.init} needs to be invoked with + * explicitly provided parameters. + */ + protected void initAlgorithmSpecificParameters(AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (!mIvUsed) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!mEncrypting) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException( + "IvParameterSpec must be provided when decrypting"); + } + return; + } + if (!(params instanceof IvParameterSpec)) { + throw new InvalidAlgorithmParameterException("Only IvParameterSpec supported"); + } + mIv = ((IvParameterSpec) params).getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in IvParameterSpec"); + } + } + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters. These parameters + * may need to be stored to be reused after {@code doFinal}. + * + * <p>The default implementation only handles the IV parameters. + * + * @param params algorithm parameters. + * + * @throws InvalidAlgorithmParameterException if some/all of the parameters cannot be + * automatically configured and thus {@code Cipher.init} needs to be invoked with + * explicitly provided parameters. + */ + protected void initAlgorithmSpecificParameters(AlgorithmParameters params) + throws InvalidAlgorithmParameterException { + if (!mIvUsed) { + if (params != null) { + throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params); + } + return; + } + + // IV is used + if (params == null) { + if (!mEncrypting) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + return; + } + + IvParameterSpec ivSpec; + try { + ivSpec = params.getParameterSpec(IvParameterSpec.class); + } catch (InvalidParameterSpecException e) { + if (!mEncrypting) { + // IV must be provided by the caller + throw new InvalidAlgorithmParameterException("IV required when decrypting" + + ", but not found in parameters: " + params, e); + } + mIv = null; + return; + } + mIv = ivSpec.getIV(); + if (mIv == null) { + throw new InvalidAlgorithmParameterException("Null IV in AlgorithmParameters"); + } + } + + /** + * Invoked by {@code engineInit} to initialize algorithm-specific parameters. These parameters + * may need to be stored to be reused after {@code doFinal}. + * + * <p>The default implementation only handles the IV parameter. + * + * @throws InvalidKeyException if some/all of the parameters cannot be automatically configured + * and thus {@code Cipher.init} needs to be invoked with explicitly provided parameters. + */ + protected void initAlgorithmSpecificParameters() throws InvalidKeyException { + if (!mIvUsed) { + return; + } + + // IV is used + if (!mEncrypting) { + throw new InvalidKeyException("IV required when decrypting" + + ". Use IvParameterSpec or AlgorithmParameters to provide it."); + } + } + + /** + * Invoked to add algorithm-specific parameters for the KeyStore's {@code begin} operation. + * + * <p>The default implementation takes care of the IV. + * + * @param keymasterArgs keystore/keymaster arguments to be populated with algorithm-specific + * parameters. + */ + protected void addAlgorithmSpecificParametersToBegin(KeymasterArguments keymasterArgs) { + if (!mFirstOperationInitiated) { + // First begin operation -- see if we need to provide additional entropy for IV + // generation. + if (mIvUsed) { + // IV is needed + if ((mIv == null) && (mEncrypting)) { + // TODO: Switch to keymaster-generated IV code below once keymaster supports + // that. + // IV is needed but was not provided by the caller -- generate an IV. + mIv = new byte[mBlockSizeBytes]; + SecureRandom rng = (mRng != null) ? mRng : new SecureRandom(); + rng.nextBytes(mIv); +// // IV was not provided by the caller and thus will be generated by keymaster. +// // Mix in some additional entropy from the provided SecureRandom. +// if (mRng != null) { +// mAdditionalEntropyForBegin = new byte[mBlockSizeBytes]; +// mRng.nextBytes(mAdditionalEntropyForBegin); +// } + } + } + } + + if ((mIvUsed) && (mIv != null)) { + keymasterArgs.addBlob(KeymasterDefs.KM_TAG_NONCE, mIv); + } + } + + /** + * Invoked by {@code engineInit} to obtain algorithm-specific parameters from the result of the + * Keymaster's {@code begin} operation. Some of these parameters may need to be reused after + * {@code doFinal} by {@link #addAlgorithmSpecificParametersToBegin(KeymasterArguments)}. + * + * <p>The default implementation only takes care of the IV. + * + * @param keymasterArgs keystore/keymaster arguments returned by KeyStore {@code begin} + * operation. + */ + protected void loadAlgorithmSpecificParametersFromBeginResult( + KeymasterArguments keymasterArgs) { + // 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 (mIvUsed) { + if (mIv == null) { + mIv = returnedIv; + } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) { + throw new CryptoOperationException("IV in use differs from provided IV"); + } + } else { + if (returnedIv != null) { + throw new CryptoOperationException( + "IV in use despite IV not being used by this transformation"); + } + } + } +} diff --git a/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java index a37ddce..6385947 100644 --- a/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java +++ b/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java @@ -1,5 +1,6 @@ package android.security; +import android.os.IBinder; import android.security.keymaster.OperationResult; import java.io.ByteArrayOutputStream; @@ -10,32 +11,36 @@ import java.io.IOException; * {@code update} and {@code finish} operations. * * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's - * update and finish operations. Firstly, KeyStore's update and finish operations can consume only a - * limited amount of data in one go because the operations are marshalled via Binder. Secondly, the - * update operation may consume less data than provided, in which case the caller has to buffer - * the remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and + * update and finish operations. Firstly, KeyStore's update operation can consume only a limited + * amount of data in one go because the operations are marshalled via Binder. Secondly, the update + * operation may consume less data than provided, in which case the caller has to buffer the + * remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and * {@link #doFinal(byte[], int, int) doFinal} operations which can be used to conveniently implement * various JCA crypto primitives. * - * <p>KeyStore operation through which data is streamed is abstracted away as - * {@link KeyStoreOperation} to avoid having this class deal with operation tokens and occasional - * additional parameters to update and final operations. + * <p>Bidirectional chunked streaming of data via a KeyStore crypto operation is abstracted away as + * a {@link Stream} to avoid having this class deal with operation tokens and occasional additional + * parameters to {@code update} and {@code final} operations. * * @hide */ public class KeyStoreCryptoOperationChunkedStreamer { - public interface KeyStoreOperation { + + /** + * Bidirectional chunked data stream over a KeyStore crypto operation. + */ + public interface Stream { /** - * Returns the result of the KeyStore update operation or null if keystore couldn't be - * reached. + * Returns the result of the KeyStore {@code update} operation or null if keystore couldn't + * be reached. */ OperationResult update(byte[] input); /** - * Returns the result of the KeyStore finish operation or null if keystore couldn't be - * reached. + * Returns the result of the KeyStore {@code finish} operation or null if keystore couldn't + * be reached. */ - OperationResult finish(byte[] input); + OperationResult finish(); } // Binder buffer is about 1MB, but it's shared between all active transactions of the process. @@ -43,19 +48,19 @@ public class KeyStoreCryptoOperationChunkedStreamer { private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024; private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - private final KeyStoreOperation mKeyStoreOperation; + private final Stream mKeyStoreStream; private final int mMaxChunkSize; private byte[] mBuffered = EMPTY_BYTE_ARRAY; private int mBufferedOffset; private int mBufferedLength; - public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation) { + public KeyStoreCryptoOperationChunkedStreamer(Stream operation) { this(operation, DEFAULT_MAX_CHUNK_SIZE); } - public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation, int maxChunkSize) { - mKeyStoreOperation = operation; + public KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize) { + mKeyStoreStream = operation; mMaxChunkSize = maxChunkSize; } @@ -73,10 +78,9 @@ public class KeyStoreCryptoOperationChunkedStreamer { if ((mBufferedLength + inputLength) > mMaxChunkSize) { // Too much input for one chunk -- extract one max-sized chunk and feed it into the // update operation. - chunk = new byte[mMaxChunkSize]; - System.arraycopy(mBuffered, mBufferedOffset, chunk, 0, mBufferedLength); - inputBytesInChunk = chunk.length - mBufferedLength; - System.arraycopy(input, inputOffset, chunk, mBufferedLength, inputBytesInChunk); + inputBytesInChunk = mMaxChunkSize - mBufferedLength; + chunk = concat(mBuffered, mBufferedOffset, mBufferedLength, + input, inputOffset, inputBytesInChunk); } else { // All of available input fits into one chunk. if ((mBufferedLength == 0) && (inputOffset == 0) @@ -87,17 +91,16 @@ public class KeyStoreCryptoOperationChunkedStreamer { inputBytesInChunk = input.length; } else { // Need to combine buffered data with input data into one array. - chunk = new byte[mBufferedLength + inputLength]; inputBytesInChunk = inputLength; - System.arraycopy(mBuffered, mBufferedOffset, chunk, 0, mBufferedLength); - System.arraycopy(input, inputOffset, chunk, mBufferedLength, inputLength); + chunk = concat(mBuffered, mBufferedOffset, mBufferedLength, + input, inputOffset, inputBytesInChunk); } } // Update input array references to reflect that some of its bytes are now in mBuffered. inputOffset += inputBytesInChunk; inputLength -= inputBytesInChunk; - OperationResult opResult = mKeyStoreOperation.update(chunk); + OperationResult opResult = mKeyStoreStream.update(chunk); if (opResult == null) { throw new KeyStoreConnectException(); } else if (opResult.resultCode != KeyStore.NO_ERROR) { @@ -176,53 +179,115 @@ public class KeyStoreCryptoOperationChunkedStreamer { inputOffset = 0; } - byte[] updateOutput = null; - if ((mBufferedLength + inputLength) > mMaxChunkSize) { - updateOutput = update(input, inputOffset, inputLength); - inputOffset += inputLength; - inputLength = 0; + // Flush all buffered input and provided input into keystore/keymaster. + byte[] output = update(input, inputOffset, inputLength); + output = concat(output, flush()); + + OperationResult opResult = mKeyStoreStream.finish(); + if (opResult == null) { + throw new KeyStoreConnectException(); + } else if (opResult.resultCode != KeyStore.NO_ERROR) { + throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode); } - // All of available input fits into one chunk. - byte[] finalChunk; - if ((mBufferedLength == 0) && (inputOffset == 0) - && (inputLength == input.length)) { - // Nothing buffered and all of input array needs to be fed into the finish operation. - finalChunk = input; - } else { - // Need to combine buffered data with input data into one array. - finalChunk = new byte[mBufferedLength + inputLength]; - System.arraycopy(mBuffered, mBufferedOffset, finalChunk, 0, mBufferedLength); - System.arraycopy(input, inputOffset, finalChunk, mBufferedLength, inputLength); + return concat(output, opResult.output); + } + + /** + * Passes all of buffered input into the the KeyStore operation (via the {@code update} + * operation) and returns output. + */ + public byte[] flush() throws KeymasterException { + if (mBufferedLength <= 0) { + return EMPTY_BYTE_ARRAY; } + + byte[] chunk = subarray(mBuffered, mBufferedOffset, mBufferedLength); mBuffered = EMPTY_BYTE_ARRAY; mBufferedLength = 0; mBufferedOffset = 0; - OperationResult opResult = mKeyStoreOperation.finish(finalChunk); + OperationResult opResult = mKeyStoreStream.update(chunk); if (opResult == null) { throw new KeyStoreConnectException(); } else if (opResult.resultCode != KeyStore.NO_ERROR) { throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode); } - if (opResult.inputConsumed != finalChunk.length) { - throw new CryptoOperationException("Unexpected number of bytes consumed by finish: " - + opResult.inputConsumed + " instead of " + finalChunk.length); + if (opResult.inputConsumed < chunk.length) { + throw new CryptoOperationException("Keystore failed to consume all input. Provided: " + + chunk.length + ", consumed: " + opResult.inputConsumed); + } else if (opResult.inputConsumed > chunk.length) { + throw new CryptoOperationException("Keystore consumed more input than provided" + + " . Provided: " + chunk.length + ", consumed: " + opResult.inputConsumed); } - // Return the concatenation of the output of update and finish. - byte[] result; - byte[] finishOutput = opResult.output; - if ((updateOutput == null) || (updateOutput.length == 0)) { - result = finishOutput; - } else if ((finishOutput == null) || (finishOutput.length == 0)) { - result = updateOutput; + return (opResult.output != null) ? opResult.output : EMPTY_BYTE_ARRAY; + } + + private static byte[] concat(byte[] arr1, byte[] arr2) { + if ((arr1 == null) || (arr1.length == 0)) { + return arr2; + } else if ((arr2 == null) || (arr2.length == 0)) { + return arr1; } else { - result = new byte[updateOutput.length + finishOutput.length]; - System.arraycopy(updateOutput, 0, result, 0, updateOutput.length); - System.arraycopy(finishOutput, 0, result, updateOutput.length, finishOutput.length); + byte[] result = new byte[arr1.length + arr2.length]; + System.arraycopy(arr1, 0, result, 0, arr1.length); + System.arraycopy(arr2, 0, result, arr1.length, arr2.length); + return result; + } + } + + private static byte[] concat(byte[] arr1, int offset1, int len1, byte[] arr2, int offset2, + int len2) { + if (len1 == 0) { + return subarray(arr2, offset2, len2); + } else if (len2 == 0) { + return subarray(arr1, offset1, len1); + } else { + byte[] result = new byte[len1 + len2]; + System.arraycopy(arr1, offset1, result, 0, len1); + System.arraycopy(arr2, offset2, result, len1, len2); + return result; + } + } + + private static byte[] subarray(byte[] arr, int offset, int len) { + if (len == 0) { + return EMPTY_BYTE_ARRAY; + } + if ((offset == 0) && (len == arr.length)) { + return arr; + } + byte[] result = new byte[len]; + System.arraycopy(arr, offset, result, 0, len); + return result; + } + + /** + * Main data stream via a KeyStore streaming operation. + * + * <p>For example, for an encryption operation, this is the stream through which plaintext is + * provided and ciphertext is obtained. + */ + public static class MainDataStream implements Stream { + + private final KeyStore mKeyStore; + private final IBinder mOperationToken; + + public MainDataStream(KeyStore keyStore, IBinder operationToken) { + mKeyStore = keyStore; + mOperationToken = operationToken; + } + + @Override + public OperationResult update(byte[] input) { + return mKeyStore.update(mOperationToken, null, input); + } + + @Override + public OperationResult finish() { + return mKeyStore.finish(mOperationToken, null, null); } - return (result != null) ? result : EMPTY_BYTE_ARRAY; } } diff --git a/keystore/java/android/security/KeyStoreHmacSpi.java b/keystore/java/android/security/KeyStoreHmacSpi.java index 3080d7b..e3c98b8 100644 --- a/keystore/java/android/security/KeyStoreHmacSpi.java +++ b/keystore/java/android/security/KeyStoreHmacSpi.java @@ -93,7 +93,8 @@ public abstract class KeyStoreHmacSpi extends MacSpi { throw new CryptoOperationException("Keystore returned null operation token"); } mChunkedStreamer = new KeyStoreCryptoOperationChunkedStreamer( - new KeyStoreStreamingConsumer(mKeyStore, mOperationToken)); + new KeyStoreCryptoOperationChunkedStreamer.MainDataStream( + mKeyStore, mOperationToken)); } @Override @@ -147,28 +148,4 @@ public abstract class KeyStoreHmacSpi extends MacSpi { super.finalize(); } } - - /** - * KeyStore-backed consumer of {@code MacSpi}'s chunked stream. - */ - private static class KeyStoreStreamingConsumer - implements KeyStoreCryptoOperationChunkedStreamer.KeyStoreOperation { - private final KeyStore mKeyStore; - private final IBinder mOperationToken; - - private KeyStoreStreamingConsumer(KeyStore keyStore, IBinder operationToken) { - mKeyStore = keyStore; - mOperationToken = operationToken; - } - - @Override - public OperationResult update(byte[] input) { - return mKeyStore.update(mOperationToken, null, input); - } - - @Override - public OperationResult finish(byte[] input) { - return mKeyStore.finish(mOperationToken, null, input); - } - } } diff --git a/keystore/java/android/security/KeyStoreKeyConstraints.java b/keystore/java/android/security/KeyStoreKeyConstraints.java index b5e2436..58ea388 100644 --- a/keystore/java/android/security/KeyStoreKeyConstraints.java +++ b/keystore/java/android/security/KeyStoreKeyConstraints.java @@ -222,16 +222,6 @@ public abstract class KeyStoreKeyConstraints { throw new IllegalArgumentException("Unsupported key algorithm: " + algorithm); } } - - /** - * @hide - */ - public static String toJCAKeyPairAlgorithm(@AlgorithmEnum int algorithm) { - switch (algorithm) { - default: - throw new IllegalArgumentException("Unsupported key alorithm: " + algorithm); - } - } } @Retention(RetentionPolicy.SOURCE) @@ -306,6 +296,20 @@ public abstract class KeyStoreKeyConstraints { throw new IllegalArgumentException("Unknown padding: " + padding); } } + + /** + * @hide + */ + public static @PaddingEnum int fromJCAPadding(String padding) { + String paddingLower = padding.toLowerCase(Locale.US); + if ("nopadding".equals(paddingLower)) { + return NONE; + } else if ("pkcs7padding".equals(paddingLower)) { + return PKCS7; + } else { + throw new IllegalArgumentException("Unknown padding: " + padding); + } + } } @Retention(RetentionPolicy.SOURCE) @@ -418,7 +422,7 @@ public abstract class KeyStoreKeyConstraints { } @Retention(RetentionPolicy.SOURCE) - @IntDef({BlockMode.ECB}) + @IntDef({BlockMode.ECB, BlockMode.CBC, BlockMode.CTR}) public @interface BlockModeEnum {} /** @@ -427,11 +431,15 @@ public abstract class KeyStoreKeyConstraints { public static abstract class BlockMode { private BlockMode() {} - /** - * Electronic Codebook (ECB) block mode. - */ + /** Electronic Codebook (ECB) block mode. */ public static final int ECB = 0; + /** Cipher Block Chaining (CBC) block mode. */ + public static final int CBC = 1; + + /** Counter (CTR) block mode. */ + public static final int CTR = 2; + /** * @hide */ @@ -439,6 +447,10 @@ public abstract class KeyStoreKeyConstraints { switch (mode) { case ECB: return KeymasterDefs.KM_MODE_ECB; + case CBC: + return KeymasterDefs.KM_MODE_CBC; + case CTR: + return KeymasterDefs.KM_MODE_CTR; default: throw new IllegalArgumentException("Unknown block mode: " + mode); } @@ -451,6 +463,10 @@ public abstract class KeyStoreKeyConstraints { switch (mode) { case KeymasterDefs.KM_MODE_ECB: return ECB; + case KeymasterDefs.KM_MODE_CBC: + return CBC; + case KeymasterDefs.KM_MODE_CTR: + return CTR; default: throw new IllegalArgumentException("Unknown block mode: " + mode); } @@ -463,9 +479,29 @@ public abstract class KeyStoreKeyConstraints { switch (mode) { case ECB: return "ECB"; + case CBC: + return "CBC"; + case CTR: + return "CTR"; default: throw new IllegalArgumentException("Unknown block mode: " + mode); } } + + /** + * @hide + */ + public static @BlockModeEnum int fromJCAMode(String mode) { + String modeLower = mode.toLowerCase(Locale.US); + if ("ecb".equals(modeLower)) { + return ECB; + } else if ("cbc".equals(modeLower)) { + return CBC; + } else if ("ctr".equals(modeLower)) { + return CTR; + } else { + throw new IllegalArgumentException("Unknown block mode: " + mode); + } + } } } diff --git a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java index f1f9436..3e5b5c0 100644 --- a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java +++ b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java @@ -35,7 +35,7 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { private final KeyStore mKeyStore = KeyStore.getInstance(); private final @KeyStoreKeyConstraints.AlgorithmEnum int mAlgorithm; - private final @KeyStoreKeyConstraints.AlgorithmEnum Integer mDigest; + private final @KeyStoreKeyConstraints.DigestEnum Integer mDigest; private final int mDefaultKeySizeBits; private KeyGeneratorSpec mSpec; @@ -76,12 +76,6 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { if (mDigest != null) { args.addInt(KeymasterDefs.KM_TAG_DIGEST, KeyStoreKeyConstraints.Digest.toKeymaster(mDigest)); - } - if (mAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) { - if (mDigest == null) { - throw new IllegalStateException("Digest algorithm must be specified for key" - + " algorithm " + KeyStoreKeyConstraints.Algorithm.toString(mAlgorithm)); - } Integer digestOutputSizeBytes = KeyStoreKeyConstraints.Digest.getOutputSizeBytes(mDigest); if (digestOutputSizeBytes != null) { @@ -90,6 +84,12 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes); } } + if (mAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) { + if (mDigest == null) { + throw new IllegalStateException("Digest algorithm must be specified for key" + + " algorithm " + KeyStoreKeyConstraints.Algorithm.toString(mAlgorithm)); + } + } int keySizeBits = (spec.getKeySize() != null) ? spec.getKeySize() : mDefaultKeySizeBits; args.addInt(KeymasterDefs.KM_TAG_KEY_SIZE, keySizeBits); @KeyStoreKeyConstraints.PurposeEnum int purposes = (spec.getPurposes() != null) |