Home | History | Annotate | Download | only in keystore
      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          */
     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