|
|
|
|
@@ -24,19 +24,20 @@ import android.security.keymaster.OperationResult;
|
|
|
|
|
|
|
|
|
|
import libcore.util.EmptyArray;
|
|
|
|
|
|
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.security.ProviderException;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
* <p>The helper abstracts away issues that need to be solved in most code that uses KeyStore's
|
|
|
|
|
* 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
|
|
|
|
|
* remainder for next time. Thirdly, when the input is smaller than a threshold, skipping update
|
|
|
|
|
* and passing input data directly to final improves performance. This threshold is configurable;
|
|
|
|
|
* using a threshold <= 1 causes the helper act eagerly, which may be required for some types of
|
|
|
|
|
* operations (e.g. ciphers).
|
|
|
|
|
*
|
|
|
|
|
* <p>The helper exposes {@link #update(byte[], int, int) update} and
|
|
|
|
|
* {@link #doFinal(byte[], int, int, byte[], byte[]) doFinal} operations which can be used to
|
|
|
|
|
* conveniently implement various JCA crypto primitives.
|
|
|
|
|
*
|
|
|
|
|
@@ -67,240 +68,122 @@ class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationS
|
|
|
|
|
|
|
|
|
|
// 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 int DEFAULT_CHUNK_SIZE_MAX = 64 * 1024;
|
|
|
|
|
// The chunk buffer will be sent to update until its size under this threshold.
|
|
|
|
|
// This threshold should be <= the max input allowed for finish.
|
|
|
|
|
// Setting this threshold <= 1 will effectivley disable buffering between updates.
|
|
|
|
|
private static final int DEFAULT_CHUNK_SIZE_THRESHOLD = 2 * 1024;
|
|
|
|
|
|
|
|
|
|
private final Stream mKeyStoreStream;
|
|
|
|
|
private final int mMaxChunkSize;
|
|
|
|
|
|
|
|
|
|
private byte[] mBuffered = EmptyArray.BYTE;
|
|
|
|
|
private int mBufferedOffset;
|
|
|
|
|
private int mBufferedLength;
|
|
|
|
|
private final int mChunkSizeMax;
|
|
|
|
|
private final int mChunkSizeThreshold;
|
|
|
|
|
private final byte[] mChunk;
|
|
|
|
|
private int mChunkLength = 0;
|
|
|
|
|
private long mConsumedInputSizeBytes;
|
|
|
|
|
private long mProducedOutputSizeBytes;
|
|
|
|
|
|
|
|
|
|
public KeyStoreCryptoOperationChunkedStreamer(Stream operation) {
|
|
|
|
|
this(operation, DEFAULT_MAX_CHUNK_SIZE);
|
|
|
|
|
KeyStoreCryptoOperationChunkedStreamer(Stream operation) {
|
|
|
|
|
this(operation, DEFAULT_CHUNK_SIZE_THRESHOLD, DEFAULT_CHUNK_SIZE_MAX);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize) {
|
|
|
|
|
KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold) {
|
|
|
|
|
this(operation, chunkSizeThreshold, DEFAULT_CHUNK_SIZE_MAX);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold,
|
|
|
|
|
int chunkSizeMax) {
|
|
|
|
|
mKeyStoreStream = operation;
|
|
|
|
|
mMaxChunkSize = maxChunkSize;
|
|
|
|
|
mChunkSizeMax = chunkSizeMax;
|
|
|
|
|
if (chunkSizeThreshold <= 0) {
|
|
|
|
|
mChunkSizeThreshold = 1;
|
|
|
|
|
} else if (chunkSizeThreshold > chunkSizeMax) {
|
|
|
|
|
mChunkSizeThreshold = chunkSizeMax;
|
|
|
|
|
} else {
|
|
|
|
|
mChunkSizeThreshold = chunkSizeThreshold;
|
|
|
|
|
}
|
|
|
|
|
mChunk = new byte[mChunkSizeMax];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException {
|
|
|
|
|
if (inputLength == 0) {
|
|
|
|
|
if (inputLength == 0 || input == null) {
|
|
|
|
|
// No input provided
|
|
|
|
|
return EmptyArray.BYTE;
|
|
|
|
|
}
|
|
|
|
|
if (inputLength < 0 || inputOffset < 0 || (inputOffset + inputLength) > input.length) {
|
|
|
|
|
throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
|
|
|
|
|
"Input offset and length out of bounds of input array");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ByteArrayOutputStream bufferedOutput = null;
|
|
|
|
|
byte[] output = EmptyArray.BYTE;
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
inputBytesInChunk = mMaxChunkSize - mBufferedLength;
|
|
|
|
|
chunk = ArrayUtils.concat(mBuffered, mBufferedOffset, mBufferedLength,
|
|
|
|
|
input, inputOffset, 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.
|
|
|
|
|
inputBytesInChunk = inputLength;
|
|
|
|
|
chunk = ArrayUtils.concat(mBuffered, mBufferedOffset, mBufferedLength,
|
|
|
|
|
input, inputOffset, inputBytesInChunk);
|
|
|
|
|
while (inputLength > 0 || mChunkLength >= mChunkSizeThreshold) {
|
|
|
|
|
int inputConsumed = ArrayUtils.copy(input, inputOffset, mChunk, mChunkLength,
|
|
|
|
|
inputLength);
|
|
|
|
|
inputLength -= inputConsumed;
|
|
|
|
|
inputOffset += inputConsumed;
|
|
|
|
|
mChunkLength += inputConsumed;
|
|
|
|
|
mConsumedInputSizeBytes += inputConsumed;
|
|
|
|
|
|
|
|
|
|
if (mChunkLength > mChunkSizeMax) {
|
|
|
|
|
throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH,
|
|
|
|
|
"Chunk size exceeded max chunk size. Max: " + mChunkSizeMax
|
|
|
|
|
+ " Actual: " + mChunkLength);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mChunkLength >= mChunkSizeThreshold) {
|
|
|
|
|
OperationResult opResult = mKeyStoreStream.update(
|
|
|
|
|
ArrayUtils.subarray(mChunk, 0, mChunkLength));
|
|
|
|
|
|
|
|
|
|
if (opResult == null) {
|
|
|
|
|
throw new KeyStoreConnectException();
|
|
|
|
|
} else if (opResult.resultCode != KeyStore.NO_ERROR) {
|
|
|
|
|
throw KeyStore.getKeyStoreException(opResult.resultCode);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Update input array references to reflect that some of its bytes are now in mBuffered.
|
|
|
|
|
inputOffset += inputBytesInChunk;
|
|
|
|
|
inputLength -= inputBytesInChunk;
|
|
|
|
|
mConsumedInputSizeBytes += inputBytesInChunk;
|
|
|
|
|
|
|
|
|
|
OperationResult opResult = mKeyStoreStream.update(chunk);
|
|
|
|
|
if (opResult == null) {
|
|
|
|
|
throw new KeyStoreConnectException();
|
|
|
|
|
} else if (opResult.resultCode != KeyStore.NO_ERROR) {
|
|
|
|
|
throw KeyStore.getKeyStoreException(opResult.resultCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opResult.inputConsumed == chunk.length) {
|
|
|
|
|
// The whole chunk was consumed
|
|
|
|
|
mBuffered = EmptyArray.BYTE;
|
|
|
|
|
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.
|
|
|
|
|
if (opResult.inputConsumed <= 0) {
|
|
|
|
|
throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH,
|
|
|
|
|
"Keystore consumed 0 of " + mChunkLength + " bytes provided.");
|
|
|
|
|
} else if (opResult.inputConsumed > mChunkLength) {
|
|
|
|
|
throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
|
|
|
|
|
"Keystore consumed nothing from max-sized chunk: " + chunk.length
|
|
|
|
|
+ " bytes");
|
|
|
|
|
"Keystore consumed more input than provided. Provided: "
|
|
|
|
|
+ mChunkLength + ", consumed: " + opResult.inputConsumed);
|
|
|
|
|
}
|
|
|
|
|
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 KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
|
|
|
|
|
"Keystore consumed more input than provided. Provided: " + chunk.length
|
|
|
|
|
+ ", consumed: " + opResult.inputConsumed);
|
|
|
|
|
}
|
|
|
|
|
mChunkLength -= opResult.inputConsumed;
|
|
|
|
|
|
|
|
|
|
if ((opResult.output != null) && (opResult.output.length > 0)) {
|
|
|
|
|
if (inputLength + mBufferedLength > 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 ProviderException("Failed to buffer output", e);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No more output will be produced in this loop
|
|
|
|
|
byte[] result;
|
|
|
|
|
if (bufferedOutput == null) {
|
|
|
|
|
// No previously buffered output
|
|
|
|
|
result = opResult.output;
|
|
|
|
|
} else {
|
|
|
|
|
// There was some previously buffered output
|
|
|
|
|
try {
|
|
|
|
|
bufferedOutput.write(opResult.output);
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new ProviderException("Failed to buffer output", e);
|
|
|
|
|
}
|
|
|
|
|
result = bufferedOutput.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
mProducedOutputSizeBytes += result.length;
|
|
|
|
|
return result;
|
|
|
|
|
if (mChunkLength > 0) {
|
|
|
|
|
// Partialy consumed, shift chunk contents
|
|
|
|
|
ArrayUtils.copy(mChunk, opResult.inputConsumed, mChunk, 0, mChunkLength);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((opResult.output != null) && (opResult.output.length > 0)) {
|
|
|
|
|
// Output was produced
|
|
|
|
|
mProducedOutputSizeBytes += opResult.output.length;
|
|
|
|
|
output = ArrayUtils.concat(output, opResult.output);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byte[] result;
|
|
|
|
|
if (bufferedOutput == null) {
|
|
|
|
|
// No output produced
|
|
|
|
|
result = EmptyArray.BYTE;
|
|
|
|
|
} else {
|
|
|
|
|
result = bufferedOutput.toByteArray();
|
|
|
|
|
}
|
|
|
|
|
mProducedOutputSizeBytes += result.length;
|
|
|
|
|
return result;
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public byte[] doFinal(byte[] input, int inputOffset, int inputLength,
|
|
|
|
|
byte[] signature, byte[] additionalEntropy) throws KeyStoreException {
|
|
|
|
|
if (inputLength == 0) {
|
|
|
|
|
// No input provided -- simplify the rest of the code
|
|
|
|
|
input = EmptyArray.BYTE;
|
|
|
|
|
inputOffset = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flush all buffered input and provided input into keystore/keymaster.
|
|
|
|
|
byte[] output = update(input, inputOffset, inputLength);
|
|
|
|
|
output = ArrayUtils.concat(output, flush());
|
|
|
|
|
byte[] finalChunk = ArrayUtils.subarray(mChunk, 0, mChunkLength);
|
|
|
|
|
OperationResult opResult = mKeyStoreStream.finish(finalChunk, signature, additionalEntropy);
|
|
|
|
|
|
|
|
|
|
OperationResult opResult = mKeyStoreStream.finish(EmptyArray.BYTE, signature,
|
|
|
|
|
additionalEntropy);
|
|
|
|
|
if (opResult == null) {
|
|
|
|
|
throw new KeyStoreConnectException();
|
|
|
|
|
} else if (opResult.resultCode != KeyStore.NO_ERROR) {
|
|
|
|
|
throw KeyStore.getKeyStoreException(opResult.resultCode);
|
|
|
|
|
}
|
|
|
|
|
mProducedOutputSizeBytes += opResult.output.length;
|
|
|
|
|
// If no error, assume all input consumed
|
|
|
|
|
mConsumedInputSizeBytes += finalChunk.length;
|
|
|
|
|
|
|
|
|
|
return ArrayUtils.concat(output, opResult.output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[] flush() throws KeyStoreException {
|
|
|
|
|
if (mBufferedLength <= 0) {
|
|
|
|
|
return EmptyArray.BYTE;
|
|
|
|
|
if ((opResult.output != null) && (opResult.output.length > 0)) {
|
|
|
|
|
mProducedOutputSizeBytes += opResult.output.length;
|
|
|
|
|
output = ArrayUtils.concat(output, opResult.output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep invoking the update operation with remaining buffered data until either all of the
|
|
|
|
|
// buffered data is consumed or until update fails to consume anything.
|
|
|
|
|
ByteArrayOutputStream bufferedOutput = null;
|
|
|
|
|
while (mBufferedLength > 0) {
|
|
|
|
|
byte[] chunk = ArrayUtils.subarray(mBuffered, mBufferedOffset, mBufferedLength);
|
|
|
|
|
OperationResult opResult = mKeyStoreStream.update(chunk);
|
|
|
|
|
if (opResult == null) {
|
|
|
|
|
throw new KeyStoreConnectException();
|
|
|
|
|
} else if (opResult.resultCode != KeyStore.NO_ERROR) {
|
|
|
|
|
throw KeyStore.getKeyStoreException(opResult.resultCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opResult.inputConsumed <= 0) {
|
|
|
|
|
// Nothing was consumed. Break out of the loop to avoid an infinite loop.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opResult.inputConsumed >= chunk.length) {
|
|
|
|
|
// All of the input was consumed
|
|
|
|
|
mBuffered = EmptyArray.BYTE;
|
|
|
|
|
mBufferedOffset = 0;
|
|
|
|
|
mBufferedLength = 0;
|
|
|
|
|
} else {
|
|
|
|
|
// Some of the input was not consumed
|
|
|
|
|
mBuffered = chunk;
|
|
|
|
|
mBufferedOffset = opResult.inputConsumed;
|
|
|
|
|
mBufferedLength = chunk.length - opResult.inputConsumed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opResult.inputConsumed > chunk.length) {
|
|
|
|
|
throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
|
|
|
|
|
"Keystore consumed more input than provided. Provided: "
|
|
|
|
|
+ chunk.length + ", consumed: " + opResult.inputConsumed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((opResult.output != null) && (opResult.output.length > 0)) {
|
|
|
|
|
// Some output was produced by this update operation
|
|
|
|
|
if (bufferedOutput == null) {
|
|
|
|
|
// No output buffered yet.
|
|
|
|
|
if (mBufferedLength == 0) {
|
|
|
|
|
// No more output will be produced by this flush operation
|
|
|
|
|
mProducedOutputSizeBytes += opResult.output.length;
|
|
|
|
|
return opResult.output;
|
|
|
|
|
} else {
|
|
|
|
|
// More output might be produced by this flush operation -- buffer output.
|
|
|
|
|
bufferedOutput = new ByteArrayOutputStream();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Buffer the output from this update operation
|
|
|
|
|
try {
|
|
|
|
|
bufferedOutput.write(opResult.output);
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
throw new ProviderException("Failed to buffer output", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mBufferedLength > 0) {
|
|
|
|
|
throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH,
|
|
|
|
|
"Keystore failed to consume last "
|
|
|
|
|
+ ((mBufferedLength != 1) ? (mBufferedLength + " bytes") : "byte")
|
|
|
|
|
+ " of input");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byte[] result = (bufferedOutput != null) ? bufferedOutput.toByteArray() : EmptyArray.BYTE;
|
|
|
|
|
mProducedOutputSizeBytes += result.length;
|
|
|
|
|
return result;
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|