1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.security.keystore;
18
19import android.os.IBinder;
20import android.security.KeyStore;
21import android.security.KeyStoreException;
22import android.security.keymaster.KeymasterDefs;
23import android.security.keymaster.OperationResult;
24
25import libcore.util.EmptyArray;
26
27import java.io.ByteArrayOutputStream;
28import java.io.IOException;
29import java.security.ProviderException;
30
31/**
32 * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's
33 * {@code update} and {@code finish} operations.
34 *
35 * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's
36 * update and finish operations. Firstly, KeyStore's update operation can consume only a limited
37 * amount of data in one go because the operations are marshalled via Binder. Secondly, the update
38 * operation may consume less data than provided, in which case the caller has to buffer the
39 * remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and
40 * {@link #doFinal(byte[], int, int, byte[], byte[]) doFinal} operations which can be used to
41 * conveniently implement various JCA crypto primitives.
42 *
43 * <p>Bidirectional chunked streaming of data via a KeyStore crypto operation is abstracted away as
44 * a {@link Stream} to avoid having this class deal with operation tokens and occasional additional
45 * parameters to {@code update} and {@code final} operations.
46 *
47 * @hide
48 */
49class KeyStoreCryptoOperationChunkedStreamer implements KeyStoreCryptoOperationStreamer {
50
51    /**
52     * Bidirectional chunked data stream over a KeyStore crypto operation.
53     */
54    interface Stream {
55        /**
56         * Returns the result of the KeyStore {@code update} operation or null if keystore couldn't
57         * be reached.
58         */
59        OperationResult update(byte[] input);
60
61        /**
62         * Returns the result of the KeyStore {@code finish} operation or null if keystore couldn't
63         * be reached.
64         */
65        OperationResult finish(byte[] siganture, byte[] additionalEntropy);
66    }
67
68    // Binder buffer is about 1MB, but it's shared between all active transactions of the process.
69    // Thus, it's safer to use a much smaller upper bound.
70    private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;
71
72    private final Stream mKeyStoreStream;
73    private final int mMaxChunkSize;
74
75    private byte[] mBuffered = EmptyArray.BYTE;
76    private int mBufferedOffset;
77    private int mBufferedLength;
78    private long mConsumedInputSizeBytes;
79    private long mProducedOutputSizeBytes;
80
81    public KeyStoreCryptoOperationChunkedStreamer(Stream operation) {
82        this(operation, DEFAULT_MAX_CHUNK_SIZE);
83    }
84
85    public KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize) {
86        mKeyStoreStream = operation;
87        mMaxChunkSize = maxChunkSize;
88    }
89
90    @Override
91    public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeyStoreException {
92        if (inputLength == 0) {
93            // No input provided
94            return EmptyArray.BYTE;
95        }
96
97        ByteArrayOutputStream bufferedOutput = null;
98
99        while (inputLength > 0) {
100            byte[] chunk;
101            int inputBytesInChunk;
102            if ((mBufferedLength + inputLength) > mMaxChunkSize) {
103                // Too much input for one chunk -- extract one max-sized chunk and feed it into the
104                // update operation.
105                inputBytesInChunk = mMaxChunkSize - mBufferedLength;
106                chunk = ArrayUtils.concat(mBuffered, mBufferedOffset, mBufferedLength,
107                        input, inputOffset, inputBytesInChunk);
108            } else {
109                // All of available input fits into one chunk.
110                if ((mBufferedLength == 0) && (inputOffset == 0)
111                        && (inputLength == input.length)) {
112                    // Nothing buffered and all of input array needs to be fed into the update
113                    // operation.
114                    chunk = input;
115                    inputBytesInChunk = input.length;
116                } else {
117                    // Need to combine buffered data with input data into one array.
118                    inputBytesInChunk = inputLength;
119                    chunk = ArrayUtils.concat(mBuffered, mBufferedOffset, mBufferedLength,
120                            input, inputOffset, inputBytesInChunk);
121                }
122            }
123            // Update input array references to reflect that some of its bytes are now in mBuffered.
124            inputOffset += inputBytesInChunk;
125            inputLength -= inputBytesInChunk;
126            mConsumedInputSizeBytes += inputBytesInChunk;
127
128            OperationResult opResult = mKeyStoreStream.update(chunk);
129            if (opResult == null) {
130                throw new KeyStoreConnectException();
131            } else if (opResult.resultCode != KeyStore.NO_ERROR) {
132                throw KeyStore.getKeyStoreException(opResult.resultCode);
133            }
134
135            if (opResult.inputConsumed == chunk.length) {
136                // The whole chunk was consumed
137                mBuffered = EmptyArray.BYTE;
138                mBufferedOffset = 0;
139                mBufferedLength = 0;
140            } else if (opResult.inputConsumed <= 0) {
141                // Nothing was consumed. More input needed.
142                if (inputLength > 0) {
143                    // More input is available, but it wasn't included into the previous chunk
144                    // because the chunk reached its maximum permitted size.
145                    // Shouldn't have happened.
146                    throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
147                            "Keystore consumed nothing from max-sized chunk: " + chunk.length
148                                    + " bytes");
149                }
150                mBuffered = chunk;
151                mBufferedOffset = 0;
152                mBufferedLength = chunk.length;
153            } else if (opResult.inputConsumed < chunk.length) {
154                // The chunk was consumed only partially -- buffer the rest of the chunk
155                mBuffered = chunk;
156                mBufferedOffset = opResult.inputConsumed;
157                mBufferedLength = chunk.length - opResult.inputConsumed;
158            } else {
159                throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
160                        "Keystore consumed more input than provided. Provided: " + chunk.length
161                                + ", consumed: " + opResult.inputConsumed);
162            }
163
164            if ((opResult.output != null) && (opResult.output.length > 0)) {
165                if (inputLength > 0) {
166                    // More output might be produced in this loop -- buffer the current output
167                    if (bufferedOutput == null) {
168                        bufferedOutput = new ByteArrayOutputStream();
169                        try {
170                            bufferedOutput.write(opResult.output);
171                        } catch (IOException e) {
172                            throw new ProviderException("Failed to buffer output", e);
173                        }
174                    }
175                } else {
176                    // No more output will be produced in this loop
177                    byte[] result;
178                    if (bufferedOutput == null) {
179                        // No previously buffered output
180                        result = opResult.output;
181                    } else {
182                        // There was some previously buffered output
183                        try {
184                            bufferedOutput.write(opResult.output);
185                        } catch (IOException e) {
186                            throw new ProviderException("Failed to buffer output", e);
187                        }
188                        result = bufferedOutput.toByteArray();
189                    }
190                    mProducedOutputSizeBytes += result.length;
191                    return result;
192                }
193            }
194        }
195
196        byte[] result;
197        if (bufferedOutput == null) {
198            // No output produced
199            result = EmptyArray.BYTE;
200        } else {
201            result = bufferedOutput.toByteArray();
202        }
203        mProducedOutputSizeBytes += result.length;
204        return result;
205    }
206
207    @Override
208    public byte[] doFinal(byte[] input, int inputOffset, int inputLength,
209            byte[] signature, byte[] additionalEntropy) throws KeyStoreException {
210        if (inputLength == 0) {
211            // No input provided -- simplify the rest of the code
212            input = EmptyArray.BYTE;
213            inputOffset = 0;
214        }
215
216        // Flush all buffered input and provided input into keystore/keymaster.
217        byte[] output = update(input, inputOffset, inputLength);
218        output = ArrayUtils.concat(output, flush());
219
220        OperationResult opResult = mKeyStoreStream.finish(signature, additionalEntropy);
221        if (opResult == null) {
222            throw new KeyStoreConnectException();
223        } else if (opResult.resultCode != KeyStore.NO_ERROR) {
224            throw KeyStore.getKeyStoreException(opResult.resultCode);
225        }
226        mProducedOutputSizeBytes += opResult.output.length;
227
228        return ArrayUtils.concat(output, opResult.output);
229    }
230
231    public byte[] flush() throws KeyStoreException {
232        if (mBufferedLength <= 0) {
233            return EmptyArray.BYTE;
234        }
235
236        // Keep invoking the update operation with remaining buffered data until either all of the
237        // buffered data is consumed or until update fails to consume anything.
238        ByteArrayOutputStream bufferedOutput = null;
239        while (mBufferedLength > 0) {
240            byte[] chunk = ArrayUtils.subarray(mBuffered, mBufferedOffset, mBufferedLength);
241            OperationResult opResult = mKeyStoreStream.update(chunk);
242            if (opResult == null) {
243                throw new KeyStoreConnectException();
244            } else if (opResult.resultCode != KeyStore.NO_ERROR) {
245                throw KeyStore.getKeyStoreException(opResult.resultCode);
246            }
247
248            if (opResult.inputConsumed <= 0) {
249                // Nothing was consumed. Break out of the loop to avoid an infinite loop.
250                break;
251            }
252
253            if (opResult.inputConsumed >= chunk.length) {
254                // All of the input was consumed
255                mBuffered = EmptyArray.BYTE;
256                mBufferedOffset = 0;
257                mBufferedLength = 0;
258            } else {
259                // Some of the input was not consumed
260                mBuffered = chunk;
261                mBufferedOffset = opResult.inputConsumed;
262                mBufferedLength = chunk.length - opResult.inputConsumed;
263            }
264
265            if (opResult.inputConsumed > chunk.length) {
266                throw new KeyStoreException(KeymasterDefs.KM_ERROR_UNKNOWN_ERROR,
267                        "Keystore consumed more input than provided. Provided: "
268                                + chunk.length + ", consumed: " + opResult.inputConsumed);
269            }
270
271            if ((opResult.output != null) && (opResult.output.length > 0)) {
272                // Some output was produced by this update operation
273                if (bufferedOutput == null) {
274                    // No output buffered yet.
275                    if (mBufferedLength == 0) {
276                        // No more output will be produced by this flush operation
277                        mProducedOutputSizeBytes += opResult.output.length;
278                        return opResult.output;
279                    } else {
280                        // More output might be produced by this flush operation -- buffer output.
281                        bufferedOutput = new ByteArrayOutputStream();
282                    }
283                }
284                // Buffer the output from this update operation
285                try {
286                    bufferedOutput.write(opResult.output);
287                } catch (IOException e) {
288                    throw new ProviderException("Failed to buffer output", e);
289                }
290            }
291        }
292
293        if (mBufferedLength > 0) {
294            throw new KeyStoreException(KeymasterDefs.KM_ERROR_INVALID_INPUT_LENGTH,
295                    "Keystore failed to consume last "
296                            + ((mBufferedLength != 1) ? (mBufferedLength + " bytes") : "byte")
297                            + " of input");
298        }
299
300        byte[] result = (bufferedOutput != null) ? bufferedOutput.toByteArray() : EmptyArray.BYTE;
301        mProducedOutputSizeBytes += result.length;
302        return result;
303    }
304
305    @Override
306    public long getConsumedInputSizeBytes() {
307        return mConsumedInputSizeBytes;
308    }
309
310    @Override
311    public long getProducedOutputSizeBytes() {
312        return mProducedOutputSizeBytes;
313    }
314
315    /**
316     * Main data stream via a KeyStore streaming operation.
317     *
318     * <p>For example, for an encryption operation, this is the stream through which plaintext is
319     * provided and ciphertext is obtained.
320     */
321    public static class MainDataStream implements Stream {
322
323        private final KeyStore mKeyStore;
324        private final IBinder mOperationToken;
325
326        public MainDataStream(KeyStore keyStore, IBinder operationToken) {
327            mKeyStore = keyStore;
328            mOperationToken = operationToken;
329        }
330
331        @Override
332        public OperationResult update(byte[] input) {
333            return mKeyStore.update(mOperationToken, null, input);
334        }
335
336        @Override
337        public OperationResult finish(byte[] signature, byte[] additionalEntropy) {
338            return mKeyStore.finish(mOperationToken, null, signature, additionalEntropy);
339        }
340    }
341}
342