Loading keystore/java/android/security/AndroidKeyStore.java +13 −0 Original line number Diff line number Diff line Loading @@ -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() Loading keystore/java/android/security/AndroidKeyStoreProvider.java +4 −0 Original line number Diff line number Diff line Loading @@ -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()); } } keystore/java/android/security/KeyStoreConnectException.java 0 → 100644 +12 −0 Original line number Diff line number Diff line 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"); } } keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java 0 → 100644 +228 −0 Original line number Diff line number Diff line 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; } } keystore/java/android/security/KeyStoreHmacSpi.java 0 → 100644 +174 −0 Original line number Diff line number Diff line 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); } } } Loading
keystore/java/android/security/AndroidKeyStore.java +13 −0 Original line number Diff line number Diff line Loading @@ -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() Loading
keystore/java/android/security/AndroidKeyStoreProvider.java +4 −0 Original line number Diff line number Diff line Loading @@ -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()); } }
keystore/java/android/security/KeyStoreConnectException.java 0 → 100644 +12 −0 Original line number Diff line number Diff line 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"); } }
keystore/java/android/security/KeyStoreCryptoOperationChunkedStreamer.java 0 → 100644 +228 −0 Original line number Diff line number Diff line 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; } }
keystore/java/android/security/KeyStoreHmacSpi.java 0 → 100644 +174 −0 Original line number Diff line number Diff line 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); } } }