diff options
author | Alex Klyubin <klyubin@google.com> | 2015-03-30 21:01:39 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2015-03-30 21:01:39 +0000 |
commit | 8a78286915a8f71eb09b5ae29a3bd8fb977180e6 (patch) | |
tree | 07dcf960329d6680697a96aa9094ab0174387687 /keystore | |
parent | dc0078b7947e0b035dce6c61108a48a276b11034 (diff) | |
parent | 7ca65f09013e807b6df61b2ba3e650a09ceff432 (diff) | |
download | frameworks_base-8a78286915a8f71eb09b5ae29a3bd8fb977180e6.zip frameworks_base-8a78286915a8f71eb09b5ae29a3bd8fb977180e6.tar.gz frameworks_base-8a78286915a8f71eb09b5ae29a3bd8fb977180e6.tar.bz2 |
am 7ca65f09: am b000d129: am 6a6f0c7d: Merge "Add HmacSHA256 backed by AndroidKeyStore."
* commit '7ca65f09013e807b6df61b2ba3e650a09ceff432':
Add HmacSHA256 backed by AndroidKeyStore.
Diffstat (limited to 'keystore')
9 files changed, 472 insertions, 4 deletions
diff --git a/keystore/java/android/security/AndroidKeyStore.java b/keystore/java/android/security/AndroidKeyStore.java index f3eb317..1d16ca1 100644 --- a/keystore/java/android/security/AndroidKeyStore.java +++ b/keystore/java/android/security/AndroidKeyStore.java @@ -494,6 +494,19 @@ public class AndroidKeyStore extends KeyStoreSpi { 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) { + // TODO: Remove MAC length constraint once Keymaster API no longer requires it. + // TODO: Switch to bits instead of bytes, once this is fixed in Keymaster + args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes); + } + } @KeyStoreKeyConstraints.PurposeEnum int purposes = (params.getPurposes() != null) ? params.getPurposes() diff --git a/keystore/java/android/security/AndroidKeyStoreProvider.java b/keystore/java/android/security/AndroidKeyStoreProvider.java index 598bcd8..7313c48 100644 --- a/keystore/java/android/security/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/AndroidKeyStoreProvider.java @@ -39,5 +39,9 @@ public class AndroidKeyStoreProvider extends Provider { // javax.crypto.KeyGenerator put("KeyGenerator.AES", KeyStoreKeyGeneratorSpi.AES.class.getName()); put("KeyGenerator.HmacSHA256", KeyStoreKeyGeneratorSpi.HmacSHA256.class.getName()); + + // javax.crypto.Mac + put("Mac.HmacSHA256", KeyStoreHmacSpi.HmacSHA256.class.getName()); + put("Mac.HmacSHA256 SupportedKeyClasses", KeyStoreSecretKey.class.getName()); } } diff --git a/keystore/java/android/security/KeyStoreConnectException.java b/keystore/java/android/security/KeyStoreConnectException.java new file mode 100644 index 0000000..4c465a4 --- /dev/null +++ b/keystore/java/android/security/KeyStoreConnectException.java @@ -0,0 +1,12 @@ +package android.security; + +/** + * Indicates a communications error with keystore service. + * + * @hide + */ +public class KeyStoreConnectException extends CryptoOperationException { + public KeyStoreConnectException() { + super("Failed to communicate with keystore service"); + } +} diff --git a/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java b/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java new file mode 100644 index 0000000..a37ddce --- /dev/null +++ b/keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java @@ -0,0 +1,228 @@ +package android.security; + +import android.security.keymaster.OperationResult; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's + * {@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 + * {@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. + * + * @hide + */ +public class KeyStoreCryptoOperationChunkedStreamer { + public interface KeyStoreOperation { + /** + * Returns the result of the KeyStore 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. + */ + OperationResult finish(byte[] input); + } + + // Binder buffer is about 1MB, but it's shared between all active transactions of the process. + // Thus, it's safer to use a much smaller upper bound. + 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 int mMaxChunkSize; + + private byte[] mBuffered = EMPTY_BYTE_ARRAY; + private int mBufferedOffset; + private int mBufferedLength; + + public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation) { + this(operation, DEFAULT_MAX_CHUNK_SIZE); + } + + public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation, int maxChunkSize) { + mKeyStoreOperation = operation; + mMaxChunkSize = maxChunkSize; + } + + public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeymasterException { + if (inputLength == 0) { + // No input provided + return EMPTY_BYTE_ARRAY; + } + + ByteArrayOutputStream bufferedOutput = null; + + while (inputLength > 0) { + byte[] chunk; + int inputBytesInChunk; + 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); + } else { + // All of available input fits into one chunk. + if ((mBufferedLength == 0) && (inputOffset == 0) + && (inputLength == input.length)) { + // Nothing buffered and all of input array needs to be fed into the update + // operation. + chunk = input; + 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); + } + } + // Update input array references to reflect that some of its bytes are now in mBuffered. + inputOffset += inputBytesInChunk; + inputLength -= inputBytesInChunk; + + OperationResult opResult = mKeyStoreOperation.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) { + // The whole chunk was consumed + mBuffered = EMPTY_BYTE_ARRAY; + mBufferedOffset = 0; + mBufferedLength = 0; + } else if (opResult.inputConsumed == 0) { + // Nothing was consumed. More input needed. + if (inputLength > 0) { + // More input is available, but it wasn't included into the previous chunk + // because the chunk reached its maximum permitted size. + // Shouldn't have happened. + throw new CryptoOperationException("Nothing consumed from max-sized chunk: " + + chunk.length + " bytes"); + } + mBuffered = chunk; + mBufferedOffset = 0; + mBufferedLength = chunk.length; + } else if (opResult.inputConsumed < chunk.length) { + // The chunk was consumed only partially -- buffer the rest of the chunk + mBuffered = chunk; + mBufferedOffset = opResult.inputConsumed; + mBufferedLength = chunk.length - opResult.inputConsumed; + } else { + throw new CryptoOperationException("Consumed more than provided: " + + opResult.inputConsumed + ", provided: " + chunk.length); + } + + if ((opResult.output != null) && (opResult.output.length > 0)) { + if (inputLength > 0) { + // More output might be produced in this loop -- buffer the current output + if (bufferedOutput == null) { + bufferedOutput = new ByteArrayOutputStream(); + try { + bufferedOutput.write(opResult.output); + } catch (IOException e) { + throw new CryptoOperationException("Failed to buffer output", e); + } + } + } else { + // No more output will be produced in this loop + if (bufferedOutput == null) { + // No previously buffered output + return opResult.output; + } else { + // There was some previously buffered output + try { + bufferedOutput.write(opResult.output); + } catch (IOException e) { + throw new CryptoOperationException("Failed to buffer output", e); + } + return bufferedOutput.toByteArray(); + } + } + } + } + + if (bufferedOutput == null) { + // No output produced + return EMPTY_BYTE_ARRAY; + } else { + return bufferedOutput.toByteArray(); + } + } + + public byte[] doFinal(byte[] input, int inputOffset, int inputLength) + throws KeymasterException { + if (inputLength == 0) { + // No input provided -- simplify the rest of the code + input = EMPTY_BYTE_ARRAY; + 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. + + 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); + 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 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; + } 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; + } +} diff --git a/keystore/java/android/security/KeyStoreHmacSpi.java b/keystore/java/android/security/KeyStoreHmacSpi.java new file mode 100644 index 0000000..3080d7b --- /dev/null +++ b/keystore/java/android/security/KeyStoreHmacSpi.java @@ -0,0 +1,174 @@ +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.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.MacSpi; + +/** + * {@link MacSpi} which provides HMAC implementations backed by Android KeyStore. + * + * @hide + */ +public abstract class KeyStoreHmacSpi extends MacSpi { + + public static class HmacSHA256 extends KeyStoreHmacSpi { + public HmacSHA256() { + super(KeyStoreKeyConstraints.Digest.SHA256, 256 / 8); + } + } + + private final KeyStore mKeyStore = KeyStore.getInstance(); + private final @KeyStoreKeyConstraints.DigestEnum int mDigest; + private final int mMacSizeBytes; + + private String mKeyAliasInKeyStore; + + // The fields below are reset by the engineReset operation. + private KeyStoreCryptoOperationChunkedStreamer mChunkedStreamer; + private IBinder mOperationToken; + + protected KeyStoreHmacSpi(@KeyStoreKeyConstraints.DigestEnum int digest, int macSizeBytes) { + mDigest = digest; + mMacSizeBytes = macSizeBytes; + } + + @Override + protected int engineGetMacLength() { + return mMacSizeBytes; + } + + @Override + protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException, + InvalidAlgorithmParameterException { + if (key == null) { + throw new InvalidKeyException("key == null"); + } else if (!(key instanceof KeyStoreSecretKey)) { + throw new InvalidKeyException( + "Only Android KeyStore secret keys supported. Key: " + key); + } + + if (params != null) { + throw new InvalidAlgorithmParameterException( + "Unsupported algorithm parameters: " + params); + } + + mKeyAliasInKeyStore = ((KeyStoreSecretKey) key).getAlias(); + engineReset(); + } + + @Override + protected void engineReset() { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mOperationToken = null; + mKeyStore.abort(operationToken); + } + mChunkedStreamer = null; + + KeymasterArguments keymasterArgs = new KeymasterArguments(); + keymasterArgs.addInt(KeymasterDefs.KM_TAG_DIGEST, mDigest); + + OperationResult opResult = mKeyStore.begin(mKeyAliasInKeyStore, + KeymasterDefs.KM_PURPOSE_SIGN, + true, + keymasterArgs, + null, + new KeymasterArguments()); + 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)); + } + mOperationToken = opResult.token; + if (mOperationToken == null) { + throw new CryptoOperationException("Keystore returned null operation token"); + } + mChunkedStreamer = new KeyStoreCryptoOperationChunkedStreamer( + new KeyStoreStreamingConsumer(mKeyStore, mOperationToken)); + } + + @Override + protected void engineUpdate(byte input) { + engineUpdate(new byte[] {input}, 0, 1); + } + + @Override + protected void engineUpdate(byte[] input, int offset, int len) { + if (mChunkedStreamer == null) { + throw new IllegalStateException("Not initialized"); + } + + byte[] output; + try { + output = mChunkedStreamer.update(input, offset, len); + } catch (KeymasterException e) { + throw new CryptoOperationException("Keystore operation failed", e); + } + if ((output != null) && (output.length != 0)) { + throw new CryptoOperationException("Update operation unexpectedly produced output"); + } + } + + @Override + protected byte[] engineDoFinal() { + if (mChunkedStreamer == null) { + throw new IllegalStateException("Not initialized"); + } + + byte[] result; + try { + result = mChunkedStreamer.doFinal(null, 0, 0); + } catch (KeymasterException e) { + throw new CryptoOperationException("Keystore operation failed", e); + } + + engineReset(); + return result; + } + + @Override + public void finalize() throws Throwable { + try { + IBinder operationToken = mOperationToken; + if (operationToken != null) { + mOperationToken = null; + mKeyStore.abort(operationToken); + } + } finally { + 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 47bb1cc..b5e2436 100644 --- a/keystore/java/android/security/KeyStoreKeyConstraints.java +++ b/keystore/java/android/security/KeyStoreKeyConstraints.java @@ -401,6 +401,20 @@ public abstract class KeyStoreKeyConstraints { throw new IllegalArgumentException("Unknown digest: " + digest); } } + + /** + * @hide + */ + public static Integer getOutputSizeBytes(@DigestEnum int digest) { + switch (digest) { + case NONE: + return null; + case SHA256: + return 256 / 8; + default: + throw new IllegalArgumentException("Unknown digest: " + digest); + } + } } @Retention(RetentionPolicy.SOURCE) diff --git a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java index 86950dd..f1f9436 100644 --- a/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java +++ b/keystore/java/android/security/KeyStoreKeyGeneratorSpi.java @@ -28,7 +28,8 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { public HmacSHA256() { super(KeyStoreKeyConstraints.Algorithm.HMAC, KeyStoreKeyConstraints.Digest.SHA256, - 256); + KeyStoreKeyConstraints.Digest.getOutputSizeBytes( + KeyStoreKeyConstraints.Digest.SHA256) * 8); } } @@ -76,6 +77,19 @@ public abstract class KeyStoreKeyGeneratorSpi extends KeyGeneratorSpi { 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) { + // TODO: Remove MAC length constraint once Keymaster API no longer requires it. + // TODO: Switch to bits instead of bytes, once this is fixed in Keymaster + args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes); + } + } int keySizeBits = (spec.getKeySize() != null) ? spec.getKeySize() : mDefaultKeySizeBits; args.addInt(KeymasterDefs.KM_TAG_KEY_SIZE, keySizeBits); @KeyStoreKeyConstraints.PurposeEnum int purposes = (spec.getPurposes() != null) diff --git a/keystore/java/android/security/KeymasterException.java b/keystore/java/android/security/KeymasterException.java index 4ff7115..bc8198f 100644 --- a/keystore/java/android/security/KeymasterException.java +++ b/keystore/java/android/security/KeymasterException.java @@ -7,7 +7,14 @@ package android.security; */ public class KeymasterException extends Exception { - public KeymasterException(String message) { + private final int mErrorCode; + + public KeymasterException(int errorCode, String message) { super(message); + mErrorCode = errorCode; + } + + public int getErrorCode() { + return mErrorCode; } } diff --git a/keystore/java/android/security/KeymasterUtils.java b/keystore/java/android/security/KeymasterUtils.java index e6e88c7..4f17586 100644 --- a/keystore/java/android/security/KeymasterUtils.java +++ b/keystore/java/android/security/KeymasterUtils.java @@ -13,9 +13,11 @@ public abstract class KeymasterUtils { case KeymasterDefs.KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT: // The name of this parameter significantly differs between Keymaster and framework // APIs. Use the framework wording to make life easier for developers. - return new KeymasterException("Invalid user authentication validity duration"); + return new KeymasterException(keymasterErrorCode, + "Invalid user authentication validity duration"); default: - return new KeymasterException(KeymasterDefs.getErrorMessage(keymasterErrorCode)); + return new KeymasterException(keymasterErrorCode, + KeymasterDefs.getErrorMessage(keymasterErrorCode)); } } } |