summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authordfalcantara@chromium.org <dfalcantara@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-04-04 21:06:13 +0000
committerdfalcantara@chromium.org <dfalcantara@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-04-04 21:06:13 +0000
commit4560459de08cf7b123c6d711af3def9a3ce39856 (patch)
tree2489663fd75c842a53883347a26fac203b6fcccf
parent4e91b6c0ee7ef5e9e16fe5c9f4d937372cec101a (diff)
downloadchromium_src-4560459de08cf7b123c6d711af3def9a3ce39856.zip
chromium_src-4560459de08cf7b123c6d711af3def9a3ce39856.tar.gz
chromium_src-4560459de08cf7b123c6d711af3def9a3ce39856.tar.bz2
Merge 261299 "Upstream Incognito key management"
> Upstream Incognito key management > > * Yanks out the Incognito key management from ChromeTab in the downstream repo. > * Cleans up the code so that intermediary results are not stored if the final > key couldn't be generated with them. > * Makes the class more thread-safe to allow multiple Activities to try restoring > their copies of the Incognito key simultaneously: > ** If a key doesn't exist yet, the bundle key is saved. > ** If a key already exists and the bundle key matches, we return success. > ** Otherwise, the key is out of date -- another Activity generated a > new Incognito key. > > Goes with https://chrome-internal-review.googlesource.com/#/c/158482/ > > Tested this out manually on the Samsung Note 3 for the following scenarios (I have absolutely no idea how to test this with our current infrastructure): > > SCENARIO 1 > 1) Opened two simultaneous Main Activities side-by-side with different incognito tabs -- they use the same Incognito key. > 2) Opened a WebappActivity as its own window. > 3) Opened Recents and swiped away the WebappActivity, killing Chrome's process. > 4) Re-opened one of the Main Activities from Recents, bringing both Activities back side-by-side with their incognito tabs intact. > > SCENARIO 2 > 1) Opened two simultaneously Main Activities side-by-side with different incognito tabs -- they use the same Incognito key. > 2) Opened Recents and swiped away one of the Main Activities, killing Chrome's process. > 3) Re-opened the remaining Main Activity from Recents, bringing it back with its incognito tabs intact. > 4) Opened a new Main instance by starting Chrome from scratch. Gets started without any incognito tabs because there's no state and the incognito key was thus lost. > > SCENARIO 3 > 1) Opened two simultaneously Main Activities side-by-side with different incognito tabs -- they use the same Incognito key. > 2) Opened Recents and swiped away one of the Main Activities, killing Chrome's process. > 3) Opened a new Main instance by starting Chrome from scratch. Gets started without any incognito tabs because there's no state and the incognito key was thus lost. > 4) Re-opened the other Main Activity from Recents, bringing it back with its incognito tabs intact because no new Incognito key was generated yet. > > SCENARIO 4 > 1) Opened two simultaneously Main Activities side-by-side with different incognito tabs -- they use the same Incognito key. > 2) Opened Recents and swiped away one of the Main Activities, killing Chrome's process. > 3) Opened a new Main instance by starting Chrome from scratch. Gets started without any incognito tabs because there's no state and the incognito key was thus lost. > 4) Opened a new Incognito tab in this new Main instance, forcing a new Incognito key to be generated. > 5) Re-opened the other Main Activity from Recents, losing its incognito tabs because the incognito key is now different. > > Added functional unit tests for this class, but an instrumentation test for Chrome on Android proper seems out of bounds. > > BUG=346737 > TEST=CipherFactoryTest > > Review URL: https://codereview.chromium.org/214273004 TBR=dfalcantara@chromium.org Review URL: https://codereview.chromium.org/226723002 git-svn-id: svn://svn.chromium.org/chrome/branches/1916/src@261869 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--content/public/android/java/src/org/chromium/content/browser/crypto/ByteArrayGenerator.java36
-rw-r--r--content/public/android/java/src/org/chromium/content/browser/crypto/CipherFactory.java269
-rw-r--r--content/public/android/java/src/org/chromium/content/browser/crypto/OWNERS3
-rw-r--r--content/public/android/javatests/src/org/chromium/content/browser/crypto/CipherFactoryTest.java229
-rw-r--r--content/public/android/javatests/src/org/chromium/content/browser/crypto/OWNERS3
5 files changed, 540 insertions, 0 deletions
diff --git a/content/public/android/java/src/org/chromium/content/browser/crypto/ByteArrayGenerator.java b/content/public/android/java/src/org/chromium/content/browser/crypto/ByteArrayGenerator.java
new file mode 100644
index 0000000..a7879c7
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/crypto/ByteArrayGenerator.java
@@ -0,0 +1,36 @@
+// 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.content.browser.crypto;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+/**
+ * Generates byte arrays for use in crypto algorithms. Defaults to pulling random data
+ * from /dev/urandom, but can be overwritten for other generation methods.
+ */
+public class ByteArrayGenerator {
+ /**
+ * Polls random data to generate the array.
+ * @param numBytes Length of the array to generate.
+ * @return byte[] containing randomly generated data.
+ */
+ public byte[] getBytes(int numBytes) throws IOException, GeneralSecurityException {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream("/dev/urandom");
+ byte[] bytes = new byte[numBytes];
+ if (bytes.length != fis.read(bytes)) {
+ throw new GeneralSecurityException("Not enough random data available");
+ }
+ return bytes;
+ } finally {
+ if (fis != null) {
+ fis.close();
+ }
+ }
+ }
+}
diff --git a/content/public/android/java/src/org/chromium/content/browser/crypto/CipherFactory.java b/content/public/android/java/src/org/chromium/content/browser/crypto/CipherFactory.java
new file mode 100644
index 0000000..b6ff368
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/crypto/CipherFactory.java
@@ -0,0 +1,269 @@
+// 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.content.browser.crypto;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+
+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";
+
+ /** 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;
+
+ /** @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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+ 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>() {
+ @Override
+ public CipherData call() {
+ // Poll random data to generate initialization parameters for the Cipher.
+ byte[] seed, iv;
+ try {
+ seed = mRandomNumberProvider.getBytes(NUM_BYTES);
+ iv = mRandomNumberProvider.getBytes(NUM_BYTES);
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't get generator data.");
+ return null;
+ }
+
+ try {
+ // Old versions of SecureRandom do not seed themselves as securely as possible.
+ // This workaround should suffice until the fixed version is deployed to all
+ // users. The seed comes from RandomNumberProvider.getBytes(), which reads
+ // from /dev/urandom, which is as good as the platform can get.
+ //
+ // TODO(palmer): Consider getting rid of this once the updated platform has
+ // shipped to everyone. Alternately, leave this in as a defense against other
+ // bugs in SecureRandom.
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ random.setSeed(seed);
+
+ KeyGenerator generator = KeyGenerator.getInstance("AES");
+ generator.init(128, random);
+ return new CipherData(generator.generateKey(), iv);
+ } 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;
+ }
+
+ private CipherFactory() {
+ mRandomNumberProvider = new ByteArrayGenerator();
+ }
+} \ No newline at end of file
diff --git a/content/public/android/java/src/org/chromium/content/browser/crypto/OWNERS b/content/public/android/java/src/org/chromium/content/browser/crypto/OWNERS
new file mode 100644
index 0000000..fce4c0f
--- /dev/null
+++ b/content/public/android/java/src/org/chromium/content/browser/crypto/OWNERS
@@ -0,0 +1,3 @@
+dfalcantara@chromium.org
+palmer@chromium.org
+
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/crypto/CipherFactoryTest.java b/content/public/android/javatests/src/org/chromium/content/browser/crypto/CipherFactoryTest.java
new file mode 100644
index 0000000..7f7cfc7
--- /dev/null
+++ b/content/public/android/javatests/src/org/chromium/content/browser/crypto/CipherFactoryTest.java
@@ -0,0 +1,229 @@
+// 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.content.browser.crypto;
+
+import android.os.Bundle;
+import android.test.InstrumentationTestCase;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+
+/**
+ * Functional tests for the {@link CipherFactory}. Confirms that saving and restoring data works, as
+ * well as that {@link Cipher} instances properly encrypt and decrypt data.
+ *
+ * Tests that confirm that the class is thread-safe would require putting potentially flaky hooks
+ * throughout the class to simulate artificial blockages.
+ */
+public class CipherFactoryTest extends InstrumentationTestCase {
+ private static final byte[] INPUT_DATA = {1, 16, 84};
+
+ /** Generates non-random byte[] for testing. */
+ private static class DeterministicParameterGenerator extends ByteArrayGenerator {
+ @Override
+ public byte[] getBytes(int numBytes) throws IOException, GeneralSecurityException {
+ return getBytes(numBytes, (byte) 0);
+ }
+
+ /**
+ * Generates a linearly-increasing byte[] sequence that wraps around after 0xFF.
+ * @param numBytes Length of the byte[] to create.
+ * @param startByte Byte to start at.
+ * @return The completed byte[].
+ */
+ public byte[] getBytes(int numBytes, byte startByte) {
+ byte[] bytes = new byte[numBytes];
+ for (int i = 0; i < numBytes; ++i) {
+ bytes[i] = (byte) (startByte + i);
+ }
+ return bytes;
+ }
+ }
+ private DeterministicParameterGenerator mNumberProvider;
+
+ /**
+ * Overrides the {@link ByteArrayGenerator} used by the {@link CipherFactory} to ensure
+ * deterministic results.
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mNumberProvider = new DeterministicParameterGenerator();
+ CipherFactory.getInstance().setRandomNumberProviderForTests(mNumberProvider);
+ }
+
+ /**
+ * {@link Cipher} instances initialized using the same parameters work in exactly the same way.
+ */
+ public void testCipherUse() throws Exception {
+ // Check encryption.
+ Cipher aEncrypt = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+ Cipher bEncrypt = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+ byte[] output = sameOutputDifferentCiphers(INPUT_DATA, aEncrypt, bEncrypt);
+
+ // Check decryption.
+ Cipher aDecrypt = CipherFactory.getInstance().getCipher(Cipher.DECRYPT_MODE);
+ Cipher bDecrypt = CipherFactory.getInstance().getCipher(Cipher.DECRYPT_MODE);
+ byte[] decrypted = sameOutputDifferentCiphers(output, aDecrypt, bDecrypt);
+ assertTrue(Arrays.equals(decrypted, INPUT_DATA));
+ }
+
+ /**
+ * Restoring a {@link Bundle} containing the same parameters already in use by the
+ * {@link CipherFactory} should keep the same keys.
+ */
+ public void testSameBundleRestoration() throws Exception {
+ // Create two bundles with the same saved state.
+ Bundle aBundle = new Bundle();
+ Bundle bBundle = new Bundle();
+
+ byte[] sameIv = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 100);
+ aBundle.putByteArray(CipherFactory.BUNDLE_IV, sameIv);
+ bBundle.putByteArray(CipherFactory.BUNDLE_IV, sameIv);
+
+ byte[] sameKey = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 200);
+ aBundle.putByteArray(CipherFactory.BUNDLE_KEY, sameKey);
+ bBundle.putByteArray(CipherFactory.BUNDLE_KEY, sameKey);
+
+ // Restore using the first bundle, then the second. Both should succeed.
+ assertTrue(CipherFactory.getInstance().restoreFromBundle(aBundle));
+ Cipher aCipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+ assertTrue(CipherFactory.getInstance().restoreFromBundle(bBundle));
+ Cipher bCipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+
+ // Make sure the CipherFactory instances are using the same key.
+ sameOutputDifferentCiphers(INPUT_DATA, aCipher, bCipher);
+ }
+
+ /**
+ * Restoring a {@link Bundle} containing a different set of parameters from those already in use
+ * by the {@link CipherFactory} should fail. Any Ciphers created after the failed restoration
+ * attempt should use the already-existing keys.
+ */
+ public void testDifferentBundleRestoration() throws Exception {
+ // Restore one set of parameters.
+ Bundle aBundle = new Bundle();
+ byte[] aIv = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 50);
+ byte[] aKey = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 100);
+ aBundle.putByteArray(CipherFactory.BUNDLE_IV, aIv);
+ aBundle.putByteArray(CipherFactory.BUNDLE_KEY, aKey);
+ assertTrue(CipherFactory.getInstance().restoreFromBundle(aBundle));
+ Cipher aCipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+
+ // Restore using a different set of parameters.
+ Bundle bBundle = new Bundle();
+ byte[] bIv = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 150);
+ byte[] bKey = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 200);
+ bBundle.putByteArray(CipherFactory.BUNDLE_IV, bIv);
+ bBundle.putByteArray(CipherFactory.BUNDLE_KEY, bKey);
+ assertFalse(CipherFactory.getInstance().restoreFromBundle(bBundle));
+ Cipher bCipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+
+ // Make sure they're using the same (original) key by encrypting the same data.
+ sameOutputDifferentCiphers(INPUT_DATA, aCipher, bCipher);
+ }
+
+ /**
+ * Restoration from a {@link Bundle} missing data should fail.
+ */
+ public void testIncompleteBundleRestoration() throws Exception {
+ // Make sure we handle the null case.
+ assertFalse(CipherFactory.getInstance().restoreFromBundle(null));
+
+ // Try restoring without the key.
+ Bundle aBundle = new Bundle();
+ byte[] iv = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 50);
+ aBundle.putByteArray(CipherFactory.BUNDLE_IV, iv);
+ assertFalse(CipherFactory.getInstance().restoreFromBundle(aBundle));
+
+ // Try restoring without the initialization vector.
+ Bundle bBundle = new Bundle();
+ byte[] key = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 100);
+ bBundle.putByteArray(CipherFactory.BUNDLE_KEY, key);
+ assertFalse(CipherFactory.getInstance().restoreFromBundle(bBundle));
+ }
+
+ /**
+ * Parameters should only be saved when they're needed by the {@link CipherFactory}. Restoring
+ * parameters from a {@link Bundle} before this point should result in {@link Cipher}s using the
+ * restored parameters instead of any generated ones.
+ */
+ public void testRestorationSucceedsBeforeCipherCreated() throws Exception {
+ byte[] iv = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 50);
+ byte[] key = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 100);
+ Bundle bundle = new Bundle();
+ bundle.putByteArray(CipherFactory.BUNDLE_IV, iv);
+ bundle.putByteArray(CipherFactory.BUNDLE_KEY, key);
+
+ // The keys should be initialized only after restoration.
+ assertNull(CipherFactory.getInstance().getCipherData(false));
+ assertTrue(CipherFactory.getInstance().restoreFromBundle(bundle));
+ assertNotNull(CipherFactory.getInstance().getCipherData(false));
+ }
+
+ /**
+ * If the {@link CipherFactory} has already generated parameters, restorations of different data
+ * should fail. All {@link Cipher}s should use the generated parameters.
+ */
+ public void testRestorationDiscardsAfterOtherCipherAlreadyCreated() throws Exception {
+ byte[] iv = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 50);
+ byte[] key = mNumberProvider.getBytes(CipherFactory.NUM_BYTES, (byte) 100);
+ Bundle bundle = new Bundle();
+ bundle.putByteArray(CipherFactory.BUNDLE_IV, iv);
+ bundle.putByteArray(CipherFactory.BUNDLE_KEY, key);
+
+ // The keys should be initialized after creating the cipher, so the keys shouldn't match.
+ Cipher aCipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+ assertFalse(CipherFactory.getInstance().restoreFromBundle(bundle));
+ Cipher bCipher = CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+
+ // B's cipher should use the keys generated for A.
+ sameOutputDifferentCiphers(INPUT_DATA, aCipher, bCipher);
+ }
+
+ /**
+ * Data saved out to the {@link Bundle} should match what is held by the {@link CipherFactory}.
+ */
+ public void testSavingToBundle() throws Exception {
+ // Nothing should get saved out before Cipher data exists.
+ Bundle initialBundle = new Bundle();
+ CipherFactory.getInstance().saveToBundle(initialBundle);
+ assertFalse(initialBundle.containsKey(CipherFactory.BUNDLE_IV));
+ assertFalse(initialBundle.containsKey(CipherFactory.BUNDLE_KEY));
+
+ // Check that Cipher data gets saved if it exists.
+ CipherFactory.getInstance().getCipher(Cipher.ENCRYPT_MODE);
+ Bundle afterBundle = new Bundle();
+ CipherFactory.getInstance().saveToBundle(afterBundle);
+ assertTrue(afterBundle.containsKey(CipherFactory.BUNDLE_IV));
+ assertTrue(afterBundle.containsKey(CipherFactory.BUNDLE_KEY));
+
+ // Confirm the saved keys match by restoring it.
+ assertTrue(CipherFactory.getInstance().restoreFromBundle(afterBundle));
+ }
+
+ /**
+ * Confirm that the two {@link Cipher}s are functionally equivalent.
+ * @return The input after it has been operated on (e.g. decrypted or encrypted).
+ */
+ private byte[] sameOutputDifferentCiphers(byte[] input, Cipher aCipher, Cipher bCipher)
+ throws Exception {
+ assertNotNull(aCipher);
+ assertNotNull(bCipher);
+ assertNotSame(aCipher, bCipher);
+
+ byte[] aOutput = aCipher.doFinal(input);
+ byte[] bOutput = bCipher.doFinal(input);
+
+ assertNotNull(aOutput);
+ assertNotNull(bOutput);
+ assertTrue(Arrays.equals(aOutput, bOutput));
+
+ return aOutput;
+ }
+}
diff --git a/content/public/android/javatests/src/org/chromium/content/browser/crypto/OWNERS b/content/public/android/javatests/src/org/chromium/content/browser/crypto/OWNERS
new file mode 100644
index 0000000..fce4c0f
--- /dev/null
+++ b/content/public/android/javatests/src/org/chromium/content/browser/crypto/OWNERS
@@ -0,0 +1,3 @@
+dfalcantara@chromium.org
+palmer@chromium.org
+