Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c13f012c authored by Rob Barnes's avatar Rob Barnes Committed by Gerrit Code Review
Browse files

Merge "Make KeyStoreCryptoOperationChunkedStreamer lazy."

parents 12a638d9 73ea6195
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -176,7 +176,7 @@ abstract class AndroidKeyStoreAuthenticatedAESCipherSpi extends AndroidKeyStoreC
                KeyStore keyStore, IBinder operationToken) {
            KeyStoreCryptoOperationStreamer streamer = new KeyStoreCryptoOperationChunkedStreamer(
                    new KeyStoreCryptoOperationChunkedStreamer.MainDataStream(
                            keyStore, operationToken));
                            keyStore, operationToken), 0);
            if (isEncrypting()) {
                return streamer;
            } else {
@@ -191,7 +191,7 @@ abstract class AndroidKeyStoreAuthenticatedAESCipherSpi extends AndroidKeyStoreC
        protected final KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer(
                KeyStore keyStore, IBinder operationToken) {
            return new KeyStoreCryptoOperationChunkedStreamer(
                    new AdditionalAuthenticationDataStream(keyStore, operationToken));
                    new AdditionalAuthenticationDataStream(keyStore, operationToken), 0);
        }

        @Override
+1 −1
Original line number Diff line number Diff line
@@ -299,7 +299,7 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor
            KeyStore keyStore, IBinder operationToken) {
        return new KeyStoreCryptoOperationChunkedStreamer(
                new KeyStoreCryptoOperationChunkedStreamer.MainDataStream(
                        keyStore, operationToken));
                        keyStore, operationToken), 0);
    }

    /**
+28 −0
Original line number Diff line number Diff line
@@ -55,6 +55,34 @@ public abstract class ArrayUtils {
        }
    }

    /**
     * Copies a subset of the source array to the destination array.
     * Length will be limited to the bounds of source and destination arrays.
     * The length actually copied is returned, which will be <= length argument.
     * @param src is the source array
     * @param srcOffset is the offset in the source array.
     * @param dst is the destination array.
     * @param dstOffset is the offset in the destination array.
     * @param length is the length to be copied from source to destination array.
     * @return The length actually copied from source array.
     */
    public static int copy(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        if (dst == null || src == null) {
            return 0;
        }
        if (length > dst.length - dstOffset) {
            length = dst.length - dstOffset;
        }
        if (length > src.length - srcOffset) {
            length = src.length - srcOffset;
        }
        if (length <= 0) {
            return 0;
        }
        System.arraycopy(src, srcOffset, dst, dstOffset, length);
        return length;
    }

    public static byte[] subarray(byte[] arr, int offset, int len) {
        if (len == 0) {
            return EmptyArray.BYTE;
+84 −201
Original line number Diff line number Diff line
@@ -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);
    }

    KeyStoreCryptoOperationChunkedStreamer(Stream operation, int chunkSizeThreshold) {
        this(operation, chunkSizeThreshold, DEFAULT_CHUNK_SIZE_MAX);
    }

    public KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize) {
    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);
            }
            // 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 (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);
                }

            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 (mChunkLength > 0) {
                    // Partialy consumed, shift chunk contents
                    ArrayUtils.copy(mChunk, opResult.inputConsumed, mChunk, 0, mChunkLength);
                }

                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;
                }
                    // 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;

        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 no error, assume all input consumed
        mConsumedInputSizeBytes += finalChunk.length;

        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");
            output = ArrayUtils.concat(output, opResult.output);
        }

        byte[] result = (bufferedOutput != null) ? bufferedOutput.toByteArray() : EmptyArray.BYTE;
        mProducedOutputSizeBytes += result.length;
        return result;
        return output;
    }

    @Override