| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.security.keystore; |
| |
| import android.os.IBinder; |
| import android.security.KeyStore; |
| import android.security.KeyStoreException; |
| import android.security.keymaster.KeymasterDefs; |
| 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 |
| * 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, byte[], byte[]) doFinal} operations which can be used to |
| * conveniently implement various JCA crypto primitives. |
| * |
| * <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 |
| */ |
| class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationStreamer { |
| |
| /** |
| * Bidirectional chunked data stream over a KeyStore crypto operation. |
| */ |
| 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 {@code finish} operation or null if keystore couldn't |
| * be reached. |
| */ |
| OperationResult finish(byte[] siganture, byte[] additionalEntropy); |
| } |
| |
| // 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 final Stream mKeyStoreStream; |
| private final int mMaxChunkSize; |
| |
| private byte[] mBuffered = EmptyArray.BYTE; |
| private int mBufferedOffset; |
| private int mBufferedLength; |
| private long mConsumedInputSizeBytes; |
| private long mProducedOutputSizeBytes; |
| |
| public KeyStoreCryptoOperationChunkedStreamer(Stream operation) { |
| this(operation, DEFAULT_MAX_CHUNK_SIZE); |
| } |
| |
| public KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize) { |
| mKeyStoreStream = operation; |
| mMaxChunkSize = maxChunkSize; |
| } |
| |
| @Override |
| public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException { |
| if (inputLength == 0) { |
| // No input provided |
| return EmptyArray.BYTE; |
| } |
| |
| 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. |
| 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); |
| } |
| } |
| // 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. |
| throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR, |
| "Keystore consumed nothing 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 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)) { |
| 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 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; |
| } |
| } |
| } |
| |
| byte[] result; |
| if (bufferedOutput == null) { |
| // No output produced |
| result = EmptyArray.BYTE; |
| } else { |
| result = bufferedOutput.toByteArray(); |
| } |
| mProducedOutputSizeBytes += result.length; |
| return result; |
| } |
| |
| @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()); |
| |
| OperationResult opResult = mKeyStoreStream.finish(signature, additionalEntropy); |
| if (opResult == null) { |
| throw new KeyStoreConnectException(); |
| } else if (opResult.resultCode != KeyStore.NO_ERROR) { |
| throw KeyStore.getKeyStoreException(opResult.resultCode); |
| } |
| mProducedOutputSizeBytes += opResult.output.length; |
| |
| return ArrayUtils.concat(output, opResult.output); |
| } |
| |
| public byte[] flush() throws KeyStoreException { |
| if (mBufferedLength <= 0) { |
| return EmptyArray.BYTE; |
| } |
| |
| // 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; |
| } |
| |
| @Override |
| public long getConsumedInputSizeBytes() { |
| return mConsumedInputSizeBytes; |
| } |
| |
| @Override |
| public long getProducedOutputSizeBytes() { |
| return mProducedOutputSizeBytes; |
| } |
| |
| /** |
| * 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(byte[] signature, byte[] additionalEntropy) { |
| return mKeyStore.finish(mOperationToken, null, signature, additionalEntropy); |
| } |
| } |
| } |