Home | History | Annotate | Download | only in crypto
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.content.browser.crypto;
      6 
      7 import android.os.AsyncTask;
      8 import android.os.Bundle;
      9 import android.util.Log;
     10 
     11 import java.io.IOException;
     12 import java.security.GeneralSecurityException;
     13 import java.security.Key;
     14 import java.security.SecureRandom;
     15 import java.util.Arrays;
     16 import java.util.concurrent.Callable;
     17 import java.util.concurrent.ExecutionException;
     18 import java.util.concurrent.FutureTask;
     19 
     20 import javax.annotation.concurrent.ThreadSafe;
     21 import javax.crypto.Cipher;
     22 import javax.crypto.KeyGenerator;
     23 import javax.crypto.spec.IvParameterSpec;
     24 import javax.crypto.spec.SecretKeySpec;
     25 
     26 /**
     27  * Generates {@link Cipher} instances for encrypting session data that is temporarily stored.
     28  *
     29  * When an Activity is sent to the background, Android gives it the opportunity to save state to
     30  * restore a user's session when the Activity is restarted. In addition to saving state to disk,
     31  * Android has a mechanism for saving instance state through {@link Bundle}s, which help
     32  * differentiate between users pausing and ending a session:
     33  * - If the Activity is killed in the background (e.g. to free up resources for other Activities),
     34  *   Android gives a {@link Bundle} to the Activity when the user restarts the Activity. The
     35  *   {@link Bundle} is expected to be small and fast to generate, and is managed by Android.
     36  * - If the Activity was explicitly killed (e.g. the user swiped away the task from Recent Tasks),
     37  *   Android does not restore the {@link Bundle} when the user restarts the Activity.
     38  *
     39  * To securely save temporary session data to disk:
     40  * - Encrypt data with a {@link Cipher} from {@link CipherFactory#getCipher(int)} before storing it.
     41  * - Store {@link Cipher} parameters in the Bundle via {@link CipherFactory#saveToBundle(Bundle)}.
     42  *
     43  * Explicitly ending the session destroys the {@link Bundle}, making the previous session's data
     44  * unreadable.
     45  */
     46 @ThreadSafe
     47 public class CipherFactory {
     48     private static final String TAG = "CipherFactory";
     49     static final int NUM_BYTES = 16;
     50 
     51     static final String BUNDLE_IV = "org.chromium.content.browser.crypto.CipherFactory.IV";
     52     static final String BUNDLE_KEY = "org.chromium.content.browser.crypto.CipherFactory.KEY";
     53 
     54     /** Holds intermediate data for the computation. */
     55     private static class CipherData {
     56         public final Key key;
     57         public final byte[] iv;
     58 
     59         public CipherData(Key key, byte[] iv) {
     60             this.key = key;
     61             this.iv = iv;
     62         }
     63     }
     64 
     65     /** Singleton holder for the class. */
     66     private static class LazyHolder {
     67         private static CipherFactory sInstance = new CipherFactory();
     68     }
     69 
     70     /**
     71      * Synchronization primitive to prevent thrashing the cipher parameters between threads
     72      * attempting to restore previous parameters and generate new ones.
     73      */
     74     private final Object mDataLock = new Object();
     75 
     76     /** Used to generate data needed for the Cipher on a background thread. */
     77     private FutureTask<CipherData> mDataGenerator;
     78 
     79     /** Holds data for cipher generation. */
     80     private CipherData mData;
     81 
     82     /** Generates random data for the Ciphers. May be swapped out for tests. */
     83     private ByteArrayGenerator mRandomNumberProvider;
     84 
     85     /** @return The Singleton instance. Creates it if it doesn't exist. */
     86     public static CipherFactory getInstance() {
     87         return LazyHolder.sInstance;
     88     }
     89 
     90     /**
     91      * Creates a secure Cipher for encrypting data.
     92      * This function blocks until data needed to generate a Cipher has been created by the
     93      * background thread.
     94      * @param opmode One of Cipher.{ENCRYPT,DECRYPT}_MODE.
     95      * @return A Cipher, or null if it is not possible to instantiate one.
     96      */
     97     public Cipher getCipher(int opmode) {
     98         CipherData data = getCipherData(true);
     99 
    100         if (data != null) {
    101             try {
    102                 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    103                 cipher.init(opmode, data.key, new IvParameterSpec(data.iv));
    104                 return cipher;
    105             } catch (GeneralSecurityException e) {
    106                 // Can't do anything here.
    107             }
    108         }
    109 
    110         Log.e(TAG, "Error in creating cipher instance.");
    111         return null;
    112     }
    113 
    114     /**
    115      * Returns data required for generating the Cipher.
    116      * @param generateIfNeeded Generates data on the background thread, blocking until it is done.
    117      * @return Data to use for the Cipher, null if it couldn't be generated.
    118      */
    119     CipherData getCipherData(boolean generateIfNeeded) {
    120         if (mData == null && generateIfNeeded) {
    121             // Ideally, this task should have been started way before this.
    122             triggerKeyGeneration();
    123 
    124             // Grab the data from the task.
    125             CipherData data;
    126             try {
    127                 data = mDataGenerator.get();
    128             } catch (InterruptedException e) {
    129                 throw new RuntimeException(e);
    130             } catch (ExecutionException e) {
    131                 throw new RuntimeException(e);
    132             }
    133 
    134             // Only the first thread is allowed to save the data.
    135             synchronized (mDataLock) {
    136                 if (mData == null) mData = data;
    137             }
    138         }
    139         return mData;
    140     }
    141 
    142     /**
    143      * Creates a Callable that generates the data required to create a Cipher. This is done on a
    144      * background thread to prevent blocking on the I/O required for
    145      * {@link ByteArrayGenerator#getBytes(int)}.
    146      * @return Callable that generates the Cipher data.
    147      */
    148     private Callable<CipherData> createGeneratorCallable() {
    149         return new Callable<CipherData>() {
    150             @Override
    151             public CipherData call() {
    152                 // Poll random data to generate initialization parameters for the Cipher.
    153                 byte[] seed, iv;
    154                 try {
    155                     seed = mRandomNumberProvider.getBytes(NUM_BYTES);
    156                     iv = mRandomNumberProvider.getBytes(NUM_BYTES);
    157                 } catch (IOException e) {
    158                     Log.e(TAG, "Couldn't get generator data.");
    159                     return null;
    160                 } catch (GeneralSecurityException e) {
    161                     Log.e(TAG, "Couldn't get generator data.");
    162                     return null;
    163                 }
    164 
    165                 try {
    166                     // Old versions of SecureRandom do not seed themselves as securely as possible.
    167                     // This workaround should suffice until the fixed version is deployed to all
    168                     // users. The seed comes from RandomNumberProvider.getBytes(), which reads
    169                     // from /dev/urandom, which is as good as the platform can get.
    170                     //
    171                     // TODO(palmer): Consider getting rid of this once the updated platform has
    172                     // shipped to everyone. Alternately, leave this in as a defense against other
    173                     // bugs in SecureRandom.
    174                     SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
    175                     random.setSeed(seed);
    176 
    177                     KeyGenerator generator = KeyGenerator.getInstance("AES");
    178                     generator.init(128, random);
    179                     return new CipherData(generator.generateKey(), iv);
    180                 } catch (GeneralSecurityException e) {
    181                     Log.e(TAG, "Couldn't get generator instances.");
    182                     return null;
    183                 }
    184             }
    185         };
    186     }
    187 
    188     /**
    189      * Generates the encryption key and IV on a background thread (if necessary).
    190      * Should be explicitly called when the Activity determines that it will need a Cipher rather
    191      * than immediately calling {@link CipherFactory#getCipher(int)}.
    192      */
    193     public void triggerKeyGeneration() {
    194         if (mData != null) return;
    195 
    196         synchronized (mDataLock) {
    197             if (mDataGenerator == null) {
    198                 mDataGenerator = new FutureTask<CipherData>(createGeneratorCallable());
    199                 AsyncTask.THREAD_POOL_EXECUTOR.execute(mDataGenerator);
    200             }
    201         }
    202     }
    203 
    204     /**
    205      * Saves the encryption data in a bundle. Expected to be called when an Activity saves its state
    206      * before being sent to the background.
    207      *
    208      * The IV *could* go into the first block of the payload. However, since the staleness of the
    209      * data is determined by whether or not it's able to be decrypted, the IV should not be read
    210      * from it.
    211      *
    212      * @param outState The data bundle to store data into.
    213      */
    214     public void saveToBundle(Bundle outState) {
    215         CipherData data = getCipherData(false);
    216         if (data == null) return;
    217 
    218         byte[] wrappedKey = data.key.getEncoded();
    219         if (wrappedKey != null && data.iv != null) {
    220             outState.putByteArray(BUNDLE_KEY, wrappedKey);
    221             outState.putByteArray(BUNDLE_IV, data.iv);
    222         }
    223     }
    224 
    225     /**
    226      * Restores the encryption key from the given Bundle. Expected to be called when an Activity is
    227      * being restored after being killed in the background. If the Activity was explicitly killed by
    228      * the user, Android gives no Bundle (and therefore no key).
    229      *
    230      * @param savedInstanceState Bundle containing the Activity's previous state. Null if the user
    231      *                           explicitly killed the Activity.
    232      * @return                   True if the data was restored successfully from the Bundle, or if
    233      *                           the CipherData in use matches the Bundle contents.
    234      *
    235      */
    236     public boolean restoreFromBundle(Bundle savedInstanceState) {
    237         if (savedInstanceState == null) return false;
    238 
    239         byte[] wrappedKey = savedInstanceState.getByteArray(BUNDLE_KEY);
    240         byte[] iv = savedInstanceState.getByteArray(BUNDLE_IV);
    241         if (wrappedKey == null || iv == null) return false;
    242 
    243         try {
    244             Key bundledKey = new SecretKeySpec(wrappedKey, "AES");
    245             synchronized (mDataLock) {
    246                 if (mData == null) {
    247                     mData = new CipherData(bundledKey, iv);
    248                     return true;
    249                 } else if (mData.key.equals(bundledKey) && Arrays.equals(mData.iv, iv)) {
    250                     return true;
    251                 } else {
    252                     Log.e(TAG, "Attempted to restore different cipher data.");
    253                 }
    254             }
    255         } catch (IllegalArgumentException e) {
    256             Log.e(TAG, "Error in restoring the key from the bundle.");
    257         }
    258 
    259         return false;
    260     }
    261 
    262     /**
    263      * Overrides the random number generated that is normally used by the class.
    264      * @param mockProvider Should be used to provide non-random data.
    265      */
    266     void setRandomNumberProviderForTests(ByteArrayGenerator mockProvider) {
    267         mRandomNumberProvider = mockProvider;
    268     }
    269 
    270     private CipherFactory() {
    271         mRandomNumberProvider = new ByteArrayGenerator();
    272     }
    273 }