CipherFactory.java revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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
5package org.chromium.content.browser.crypto;
6
7import android.os.AsyncTask;
8import android.os.Bundle;
9import android.util.Log;
10
11import java.io.IOException;
12import java.security.GeneralSecurityException;
13import java.security.Key;
14import java.security.SecureRandom;
15import java.util.Arrays;
16import java.util.concurrent.Callable;
17import java.util.concurrent.ExecutionException;
18import java.util.concurrent.FutureTask;
19
20import javax.annotation.concurrent.ThreadSafe;
21import javax.crypto.Cipher;
22import javax.crypto.KeyGenerator;
23import javax.crypto.spec.IvParameterSpec;
24import 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
47public 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}