blob: ee062520db081b4a368719f461920da1ea08965d [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.crypto;
import android.annotation.SuppressLint;
import android.os.Bundle;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.SecureRandomInitializer;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import javax.annotation.concurrent.ThreadSafe;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Generates {@link Cipher} instances for encrypting session data that is temporarily stored.
*
* When an Activity is sent to the background, Android gives it the opportunity to save state to
* restore a user's session when the Activity is restarted. In addition to saving state to disk,
* Android has a mechanism for saving instance state through {@link Bundle}s, which help
* differentiate between users pausing and ending a session:
* - If the Activity is killed in the background (e.g. to free up resources for other Activities),
* Android gives a {@link Bundle} to the Activity when the user restarts the Activity. The
* {@link Bundle} is expected to be small and fast to generate, and is managed by Android.
* - If the Activity was explicitly killed (e.g. the user swiped away the task from Recent Tasks),
* Android does not restore the {@link Bundle} when the user restarts the Activity.
*
* To securely save temporary session data to disk:
* - Encrypt data with a {@link Cipher} from {@link CipherFactory#getCipher(int)} before storing it.
* - Store {@link Cipher} parameters in the Bundle via {@link CipherFactory#saveToBundle(Bundle)}.
*
* Explicitly ending the session destroys the {@link Bundle}, making the previous session's data
* unreadable.
*/
@ThreadSafe
public class CipherFactory {
private static final String TAG = "CipherFactory";
static final int NUM_BYTES = 16;
static final String BUNDLE_IV = "org.chromium.content.browser.crypto.CipherFactory.IV";
static final String BUNDLE_KEY = "org.chromium.content.browser.crypto.CipherFactory.KEY";
/**
* An observer for whether cipher data has been created.
*/
public interface CipherDataObserver {
/**
* Called asynchronously after new cipher key data has been generated.
*/
void onCipherDataGenerated();
}
/** Holds intermediate data for the computation. */
private static class CipherData {
public final Key key;
public final byte[] iv;
public CipherData(Key key, byte[] iv) {
this.key = key;
this.iv = iv;
}
}
/** Singleton holder for the class. */
private static class LazyHolder {
private static CipherFactory sInstance = new CipherFactory();
}
/**
* Synchronization primitive to prevent thrashing the cipher parameters between threads
* attempting to restore previous parameters and generate new ones.
*/
private final Object mDataLock = new Object();
/** Used to generate data needed for the Cipher on a background thread. */
private FutureTask<CipherData> mDataGenerator;
/** Holds data for cipher generation. */
private CipherData mData;
/** Generates random data for the Ciphers. May be swapped out for tests. */
private ByteArrayGenerator mRandomNumberProvider;
/** A list of observers for this class. */
private final ObserverList<CipherDataObserver> mObservers;
/** @return The Singleton instance. Creates it if it doesn't exist. */
public static CipherFactory getInstance() {
return LazyHolder.sInstance;
}
/**
* Creates a secure Cipher for encrypting data.
* This function blocks until data needed to generate a Cipher has been created by the
* background thread.
* @param opmode One of Cipher.{ENCRYPT,DECRYPT}_MODE.
* @return A Cipher, or null if it is not possible to instantiate one.
*/
public Cipher getCipher(int opmode) {
CipherData data = getCipherData(true);
if (data != null) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(opmode, data.key, new IvParameterSpec(data.iv));
return cipher;
} catch (GeneralSecurityException e) {
// Can't do anything here.
}
}
Log.e(TAG, "Error in creating cipher instance.");
return null;
}
/**
* @return Whether a cipher has been generated.
*/
public boolean hasCipher() {
synchronized (mDataLock) {
return mData != null;
}
}
/**
* Returns data required for generating the Cipher.
* @param generateIfNeeded Generates data on the background thread, blocking until it is done.
* @return Data to use for the Cipher, null if it couldn't be generated.
*/
CipherData getCipherData(boolean generateIfNeeded) {
if (mData == null && generateIfNeeded) {
// Ideally, this task should have been started way before this.
triggerKeyGeneration();
// Grab the data from the task.
CipherData data;
try {
data = mDataGenerator.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
// Only the first thread is allowed to save the data.
synchronized (mDataLock) {
if (mData == null) {
mData = data;
// Posting an asynchronous task to notify the observers.
PostTask.postTask(UiThreadTaskTraits.DEFAULT, new Runnable() {
@Override
public void run() {
notifyCipherDataGenerated();
}
});
}
}
}
return mData;
}
/**
* Creates a Callable that generates the data required to create a Cipher. This is done on a
* background thread to prevent blocking on the I/O required for
* {@link ByteArrayGenerator#getBytes(int)}.
* @return Callable that generates the Cipher data.
*/
private Callable<CipherData> createGeneratorCallable() {
return new Callable<CipherData>() {
// SecureRandomInitializer addresses the bug in SecureRandom that "TrulyRandom"
// warns about, so this lint warning can safely be suppressed.
@SuppressLint("TrulyRandom")
@Override
public CipherData call() {
// Poll random data to generate initialization parameters for the Cipher.
byte[] iv;
try {
iv = mRandomNumberProvider.getBytes(NUM_BYTES);
} catch (IOException e) {
Log.e(TAG, "Couldn't get generator data.");
return null;
} catch (GeneralSecurityException e) {
Log.e(TAG, "Couldn't get generator data.");
return null;
}
try {
SecureRandom random = new SecureRandom();
SecureRandomInitializer.initialize(random);
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128, random);
return new CipherData(generator.generateKey(), iv);
} catch (IOException e) {
Log.e(TAG, "Couldn't get generator data.");
return null;
} catch (GeneralSecurityException e) {
Log.e(TAG, "Couldn't get generator instances.");
return null;
}
}
};
}
/**
* Generates the encryption key and IV on a background thread (if necessary).
* Should be explicitly called when the Activity determines that it will need a Cipher rather
* than immediately calling {@link CipherFactory#getCipher(int)}.
*/
public void triggerKeyGeneration() {
if (mData != null) return;
synchronized (mDataLock) {
if (mDataGenerator == null) {
mDataGenerator = new FutureTask<CipherData>(createGeneratorCallable());
AsyncTask.THREAD_POOL_EXECUTOR.execute(mDataGenerator);
}
}
}
/**
* Saves the encryption data in a bundle. Expected to be called when an Activity saves its state
* before being sent to the background.
*
* The IV *could* go into the first block of the payload. However, since the staleness of the
* data is determined by whether or not it's able to be decrypted, the IV should not be read
* from it.
*
* @param outState The data bundle to store data into.
*/
public void saveToBundle(Bundle outState) {
CipherData data = getCipherData(false);
if (data == null) return;
byte[] wrappedKey = data.key.getEncoded();
if (wrappedKey != null && data.iv != null) {
outState.putByteArray(BUNDLE_KEY, wrappedKey);
outState.putByteArray(BUNDLE_IV, data.iv);
}
}
/**
* Restores the encryption key from the given Bundle. Expected to be called when an Activity is
* being restored after being killed in the background. If the Activity was explicitly killed by
* the user, Android gives no Bundle (and therefore no key).
*
* @param savedInstanceState Bundle containing the Activity's previous state. Null if the user
* explicitly killed the Activity.
* @return True if the data was restored successfully from the Bundle, or if
* the CipherData in use matches the Bundle contents.
*
*/
public boolean restoreFromBundle(Bundle savedInstanceState) {
if (savedInstanceState == null) return false;
byte[] wrappedKey = savedInstanceState.getByteArray(BUNDLE_KEY);
byte[] iv = savedInstanceState.getByteArray(BUNDLE_IV);
if (wrappedKey == null || iv == null) return false;
try {
Key bundledKey = new SecretKeySpec(wrappedKey, "AES");
synchronized (mDataLock) {
if (mData == null) {
mData = new CipherData(bundledKey, iv);
return true;
} else if (mData.key.equals(bundledKey) && Arrays.equals(mData.iv, iv)) {
return true;
} else {
Log.e(TAG, "Attempted to restore different cipher data.");
}
}
} catch (IllegalArgumentException e) {
Log.e(TAG, "Error in restoring the key from the bundle.");
}
return false;
}
/**
* Overrides the random number generated that is normally used by the class.
* @param mockProvider Should be used to provide non-random data.
*/
void setRandomNumberProviderForTests(ByteArrayGenerator mockProvider) {
mRandomNumberProvider = mockProvider;
}
/**
* Adds an observer for cipher data creation.
* @param observer The observer to add.
*/
public void addCipherDataObserver(CipherDataObserver observer) {
mObservers.addObserver(observer);
}
/**
* Removes a cipher data observer for cipher data creation.
* @param observer The observer to remove.
*/
public void removeCipherDataObserver(CipherDataObserver observer) {
mObservers.removeObserver(observer);
}
private void notifyCipherDataGenerated() {
for (CipherDataObserver observer : mObservers) {
observer.onCipherDataGenerated();
}
}
private CipherFactory() {
mRandomNumberProvider = new ByteArrayGenerator();
mObservers = new ObserverList<CipherDataObserver>();
}
}