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 
17 package android.security.keystore;
18 
19 import android.os.IBinder;
20 import android.security.KeyStore;
21 import android.security.KeyStoreException;
22 import android.security.keymaster.KeymasterDefs;
23 import android.security.keymaster.OperationResult;
24 
25 import libcore.util.EmptyArray;
26 
27 import java.io.ByteArrayOutputStream;
28 import java.io.IOException;
29 import 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  */
49 class 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          */
update(byte[] input)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          */
finish(byte[] siganture, byte[] additionalEntropy)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 
KeyStoreCryptoOperationChunkedStreamer(Stream operation)81     public KeyStoreCryptoOperationChunkedStreamer(Stream operation) {
82         this(operation, DEFAULT_MAX_CHUNK_SIZE);
83     }
84 
KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize)85     public KeyStoreCryptoOperationChunkedStreamer(Stream operation, int maxChunkSize) {
86         mKeyStoreStream = operation;
87         mMaxChunkSize = maxChunkSize;
88     }
89 
90     @Override
update(byte[] input, int inputOffset, int inputLength)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
doFinal(byte[] input, int inputOffset, int inputLength, byte[] signature, byte[] additionalEntropy)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 
flush()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
getConsumedInputSizeBytes()306     public long getConsumedInputSizeBytes() {
307         return mConsumedInputSizeBytes;
308     }
309 
310     @Override
getProducedOutputSizeBytes()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 
MainDataStream(KeyStore keyStore, IBinder operationToken)326         public MainDataStream(KeyStore keyStore, IBinder operationToken) {
327             mKeyStore = keyStore;
328             mOperationToken = operationToken;
329         }
330 
331         @Override
update(byte[] input)332         public OperationResult update(byte[] input) {
333             return mKeyStore.update(mOperationToken, null, input);
334         }
335 
336         @Override
finish(byte[] signature, byte[] additionalEntropy)337         public OperationResult finish(byte[] signature, byte[] additionalEntropy) {
338             return mKeyStore.finish(mOperationToken, null, signature, additionalEntropy);
339         }
340     }
341 }
342