Merge "Add unauthenticated AES ciphers backed by AndroidKeyStore."

This commit is contained in:
Alex Klyubin
2015-03-31 21:00:16 +00:00
committed by Gerrit Code Review
7 changed files with 765 additions and 118 deletions

View File

@ -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(

View File

@ -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());
}
}

View File

@ -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");
}
}
}
}

View File

@ -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 {
/**
* Returns the result of the KeyStore update operation or null if keystore couldn't be
* reached.
* Bidirectional chunked data stream over a KeyStore crypto operation.
*/
public interface Stream {
/**
* 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;
}
// All of available input fits into one chunk.
// Flush all buffered input and provided input into keystore/keymaster.
byte[] output = update(input, inputOffset, inputLength);
output = concat(output, flush());
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);
}
mBuffered = EMPTY_BYTE_ARRAY;
mBufferedLength = 0;
mBufferedOffset = 0;
OperationResult opResult = mKeyStoreOperation.finish(finalChunk);
OperationResult opResult = mKeyStoreStream.finish();
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);
return concat(output, opResult.output);
}
// 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;
/**
* 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 = mKeyStoreStream.update(chunk);
if (opResult == null) {
throw new KeyStoreConnectException();
} else if (opResult.resultCode != KeyStore.NO_ERROR) {
throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode);
}
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 (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);
}
return (result != null) ? result : EMPTY_BYTE_ARRAY;
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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)