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