Bug 709408 - Part 2: Remove Cryptographer. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Wed, 15 Feb 2012 22:05:53 -0800
changeset 89869 d884651d36d617d9f7a1ec0678c6dc8f8e89b168
parent 89868 6675906944a4943f71935ba799b5e4cbd0c96ab6
child 89870 127239d2fdb56820d21b0e998729ff6074dd5851
push idunknown
push userunknown
push dateunknown
reviewersrnewman
bugs709408
milestone13.0a1
Bug 709408 - Part 2: Remove Cryptographer. r=rnewman
mobile/android/base/sync/CollectionKeys.java
mobile/android/base/sync/CryptoRecord.java
mobile/android/base/sync/crypto/CryptoInfo.java
mobile/android/base/sync/crypto/Cryptographer.java
mobile/android/base/sync/crypto/KeyBundle.java
mobile/android/base/sync/jpake/JPakeClient.java
mobile/android/sync/java-sources.mn
--- a/mobile/android/base/sync/CollectionKeys.java
+++ b/mobile/android/base/sync/CollectionKeys.java
@@ -37,22 +37,21 @@
 
 package org.mozilla.gecko.sync;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.HashMap;
 import java.util.Map.Entry;
 
-import org.mozilla.apache.commons.codec.binary.Base64;
 import org.json.JSONException;
 import org.json.simple.JSONArray;
 import org.json.simple.parser.ParseException;
+import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.crypto.CryptoException;
-import org.mozilla.gecko.sync.crypto.Cryptographer;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 
 import android.util.Log;
 
 public class CollectionKeys {
   private static final String LOG_TAG = "CollectionKeys";
   private KeyBundle                  defaultKeyBundle     = null;
   private HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>();
@@ -63,19 +62,26 @@ public class CollectionKeys {
       return ck.asCryptoRecord();
     } catch (NoCollectionKeysSetException e) {
       // Cannot occur.
       Log.e(LOG_TAG, "generateCollectionKeys returned a value with no default key. Unpossible.", e);
       throw new IllegalStateException("CollectionKeys should not have null default key.");
     }
   }
 
+  /**
+   * Randomly generate a basic CollectionKeys object.
+   * @throws CryptoException
+   */
   public static CollectionKeys generateCollectionKeys() throws CryptoException {
     CollectionKeys ck = new CollectionKeys();
-    ck.populate();
+    ck.clear();
+    ck.defaultKeyBundle = KeyBundle.withRandomKeys();
+    // TODO: eventually we would like to keep per-collection keys, just generate
+    // new ones as appropriate.
     return ck;
   }
 
   public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException {
     if (this.defaultKeyBundle == null) {
       throw new NoCollectionKeysSetException();
     }
     return this.defaultKeyBundle;
@@ -99,17 +105,17 @@ public class CollectionKeys {
    * @param array
    * @return
    * @throws JSONException
    * @throws UnsupportedEncodingException
    */
   private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException {
     String encKeyStr  = (String) array.get(0);
     String hmacKeyStr = (String) array.get(1);
-    return KeyBundle.decodeKeyStrings(encKeyStr, hmacKeyStr);
+    return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr);
   }
 
   @SuppressWarnings("unchecked")
   private static JSONArray keyBundleToArray(KeyBundle bundle) {
     // Generate JSON.
     JSONArray keysArray = new JSONArray();
     keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey())));
     keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey())));
@@ -199,20 +205,9 @@ public class CollectionKeys {
   public void setDefaultKeyBundle(KeyBundle keys) {
     this.defaultKeyBundle = keys;
   }
 
   public void clear() {
     this.defaultKeyBundle = null;
     this.collectionKeyBundles = new HashMap<String, KeyBundle>();
   }
-
-  /**
-   * Randomly generate a basic CollectionKeys object.
-   * @throws CryptoException
-   */
-  public void populate() throws CryptoException {
-    this.clear();
-    this.defaultKeyBundle = Cryptographer.generateKeys();
-    // TODO: eventually we would like to keep per-collection keys, just generate
-    // new ones as appropriate.
-  }
 }
--- a/mobile/android/base/sync/CryptoRecord.java
+++ b/mobile/android/base/sync/CryptoRecord.java
@@ -41,17 +41,16 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 
 import org.json.simple.JSONObject;
 import org.json.simple.parser.JSONParser;
 import org.json.simple.parser.ParseException;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.CryptoInfo;
-import org.mozilla.gecko.sync.crypto.Cryptographer;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.crypto.MissingCryptoInputException;
 import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
 /**
  * A Sync crypto record has:
  *
@@ -60,16 +59,18 @@ import org.mozilla.gecko.sync.repositori
  * * a payload, which is encrypted and decrypted on request.
  *
  * The payload flips between being a blob of JSON with hmac/IV/ciphertext
  * attributes and the cleartext itself.
  *
  * Until there's some benefit to the abstraction, we're simply going to
  * call this CryptoRecord.
  *
+ * CryptoRecord uses CryptoInfo to do the actual encryption and decryption.
+ *
  * @author rnewman
  *
  */
 public class CryptoRecord extends Record {
 
   // JSON related constants.
   private static final String KEY_ID         = "id";
   private static final String KEY_COLLECTION = "collection";
@@ -87,17 +88,18 @@ public class CryptoRecord extends Record
    * KeyBundle with keys for decryption. Output: byte[] clearText
    * @throws CryptoException
    * @throws UnsupportedEncodingException
    */
   private static byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle) throws CryptoException, UnsupportedEncodingException {
     byte[] ciphertext = Base64.decodeBase64(((String) payload.get(KEY_CIPHERTEXT)).getBytes("UTF-8"));
     byte[] iv         = Base64.decodeBase64(((String) payload.get(KEY_IV)).getBytes("UTF-8"));
     byte[] hmac       = Utils.hex2Byte((String) payload.get(KEY_HMAC));
-    return Cryptographer.decrypt(new CryptoInfo(ciphertext, iv, hmac, keybundle));
+
+    return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage();
   }
 
   // The encrypted JSON body object.
   // The decrypted JSON body object. Fields are copied from `body`.
 
   public ExtendedJSONObject payload;
   public KeyBundle   keyBundle;
 
@@ -216,18 +218,17 @@ public class CryptoRecord extends Record
   }
 
   public CryptoRecord encrypt() throws CryptoException, UnsupportedEncodingException {
     if (this.keyBundle == null) {
       throw new NoKeyBundleException();
     }
     String cleartext = payload.toJSONString();
     byte[] cleartextBytes = cleartext.getBytes("UTF-8");
-    CryptoInfo info = new CryptoInfo(cleartextBytes, keyBundle);
-    Cryptographer.encrypt(info);
+    CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle);
     String message = new String(Base64.encodeBase64(info.getMessage()));
     String iv      = new String(Base64.encodeBase64(info.getIV()));
     String hmac    = Utils.byte2hex(info.getHMAC());
     ExtendedJSONObject ciphertext = new ExtendedJSONObject();
     ciphertext.put(KEY_CIPHERTEXT, message);
     ciphertext.put(KEY_HMAC, hmac);
     ciphertext.put(KEY_IV, iv);
     this.payload = ciphertext;
--- a/mobile/android/base/sync/crypto/CryptoInfo.java
+++ b/mobile/android/base/sync/crypto/CryptoInfo.java
@@ -1,24 +1,69 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync.crypto;
 
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+
 /*
  * All info in these objects should be decoded (i.e. not BaseXX encoded).
  */
 public class CryptoInfo {
+  private static final String TRANSFORMATION     = "AES/CBC/PKCS5Padding";
+  private static final String KEY_ALGORITHM_SPEC = "AES";
 
   private byte[] message;
   private byte[] iv;
   private byte[] hmac;
   private KeyBundle keys;
 
+  /**
+   * Return a CryptoInfo with given plaintext encrypted using given keys.
+   */
+  public static CryptoInfo encrypt(byte[] plaintextBytes, KeyBundle keys) throws CryptoException {
+    CryptoInfo info = new CryptoInfo(plaintextBytes, keys);
+    info.encrypt();
+    return info;
+  }
+
+  /**
+   * Return a CryptoInfo with given plaintext encrypted using given keys and initial vector.
+   */
+  public static CryptoInfo encrypt(byte[] plaintextBytes, byte[] iv, KeyBundle keys) throws CryptoException {
+    CryptoInfo info = new CryptoInfo(plaintextBytes, iv, null, keys);
+    info.encrypt();
+    return info;
+  }
+
+  /**
+   * Return a CryptoInfo with given ciphertext decrypted using given keys and initial vector, verifying that given HMAC validates.
+   */
+  public static CryptoInfo decrypt(byte[] ciphertext, byte[] iv, byte[] hmac, KeyBundle keys) throws CryptoException {
+    CryptoInfo info = new CryptoInfo(ciphertext, iv, hmac, keys);
+    info.decrypt();
+    return info;
+  }
+
   /*
    * Constructor typically used when encrypting.
    */
   public CryptoInfo(byte[] message, KeyBundle keys) {
     this.setMessage(message);
     this.setKeys(keys);
   }
 
@@ -58,9 +103,139 @@ public class CryptoInfo {
 
   public KeyBundle getKeys() {
     return keys;
   }
 
   public void setKeys(KeyBundle keys) {
     this.keys = keys;
   }
+
+  /*
+   * Generate HMAC for given cipher text.
+   */
+  public static byte[] generatedHMACFor(byte[] message, KeyBundle keys) throws NoSuchAlgorithmException, InvalidKeyException {
+    Mac hmacHasher = HKDF.makeHMACHasher(keys.getHMACKey());
+    return hmacHasher.doFinal(Base64.encodeBase64(message));
+  }
+
+  /*
+   * Return true if generated HMAC is the same as the specified HMAC.
+   */
+  public boolean generatedHMACIsHMAC() throws NoSuchAlgorithmException, InvalidKeyException {
+    byte[] generatedHMAC = generatedHMACFor(getMessage(), getKeys());
+    byte[] expectedHMAC  = getHMAC();
+    return Arrays.equals(generatedHMAC, expectedHMAC);
+  }
+
+  /**
+   * Performs functionality common to both encryption and decryption.
+   *
+   * @param cipher
+   * @param inputMessage non-BaseXX-encoded message
+   * @return encrypted/decrypted message
+   * @throws CryptoException
+   */
+  private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage)
+                        throws CryptoException {
+    byte[] outputMessage = null;
+    try {
+      outputMessage = cipher.doFinal(inputMessage);
+    } catch (IllegalBlockSizeException e) {
+      throw new CryptoException(e);
+    } catch (BadPaddingException e) {
+      throw new CryptoException(e);
+    }
+    return outputMessage;
+  }
+
+  /**
+   * Encrypt a CryptoInfo in-place.
+   *
+   * @throws CryptoException
+   */
+  public void encrypt() throws CryptoException {
+
+    Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION);
+    try {
+      byte[] encryptionKey = getKeys().getEncryptionKey();
+      SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC);
+
+      // If no IV is provided, we allow the cipher to provide one.
+      if (getIV() == null || getIV().length == 0) {
+        cipher.init(Cipher.ENCRYPT_MODE, spec);
+      } else {
+        cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(getIV()));
+      }
+    } catch (GeneralSecurityException ex) {
+      throw new CryptoException(ex);
+    }
+
+    // Encrypt.
+    byte[] encryptedBytes = commonCrypto(cipher, getMessage());
+    byte[] iv = cipher.getIV();
+
+    byte[] hmac;
+    // Generate HMAC.
+    try {
+      hmac = generatedHMACFor(encryptedBytes, keys);
+    } catch (NoSuchAlgorithmException e) {
+      throw new CryptoException(e);
+    } catch (InvalidKeyException e) {
+      throw new CryptoException(e);
+    }
+
+    // Update in place.  keys is already set.
+    this.setHMAC(hmac);
+    this.setIV(iv);
+    this.setMessage(encryptedBytes);
+  }
+
+  /**
+   * Decrypt a CryptoInfo in-place.
+   *
+   * @throws CryptoException
+   */
+  public void decrypt() throws CryptoException {
+
+    // Check HMAC.
+    try {
+      if (!generatedHMACIsHMAC()) {
+        throw new HMACVerificationException();
+      }
+    } catch (NoSuchAlgorithmException e) {
+      throw new CryptoException(e);
+    } catch (InvalidKeyException e) {
+      throw new CryptoException(e);
+    }
+
+    Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION);
+    try {
+      byte[] encryptionKey = getKeys().getEncryptionKey();
+      SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC);
+      cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(getIV()));
+    } catch (GeneralSecurityException ex) {
+      throw new CryptoException(ex);
+    }
+    byte[] decryptedBytes = commonCrypto(cipher, getMessage());
+    byte[] iv = cipher.getIV();
+
+    // Update in place.  keys is already set.
+    this.setHMAC(null);
+    this.setIV(iv);
+    this.setMessage(decryptedBytes);
+  }
+
+  /**
+   * Helper to get a Cipher object.
+   *
+   * @param transformation The type of Cipher to get.
+   */
+  private static Cipher getCipher(String transformation) throws CryptoException {
+    try {
+      return Cipher.getInstance(transformation);
+    } catch (NoSuchAlgorithmException e) {
+      throw new CryptoException(e);
+    } catch (NoSuchPaddingException e) {
+      throw new CryptoException(e);
+    }
+  }
 }
deleted file mode 100644
--- a/mobile/android/base/sync/crypto/Cryptographer.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Android Sync Client.
- *
- * The Initial Developer of the Original Code is
- * the Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Jason Voll
- * Richard Newman <rnewman@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-package org.mozilla.gecko.sync.crypto;
-
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.Mac;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.mozilla.apache.commons.codec.binary.Base64;
-import org.mozilla.gecko.sync.Utils;
-
-/*
- * Implements the basic required cryptography options.
- */
-public class Cryptographer {
-
-  private static final String TRANSFORMATION     = "AES/CBC/PKCS5Padding";
-  private static final String KEY_ALGORITHM_SPEC = "AES";
-  private static final int    KEY_SIZE           = 256;
-
-  public static CryptoInfo encrypt(CryptoInfo info) throws CryptoException {
-
-    Cipher cipher = getCipher();
-    try {
-      byte[] encryptionKey = info.getKeys().getEncryptionKey();
-      SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC);
-
-      // If no IV is provided, we allow the cipher to provide one.
-      if (info.getIV() == null ||
-          info.getIV().length == 0) {
-        cipher.init(Cipher.ENCRYPT_MODE, spec);
-      } else {
-        System.out.println("IV is " + info.getIV().length);
-        cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(info.getIV()));
-      }
-    } catch (GeneralSecurityException ex) {
-      throw new CryptoException(ex);
-    }
-
-    // Encrypt.
-    byte[] encryptedBytes = commonCrypto(cipher, info.getMessage());
-    info.setMessage(encryptedBytes);
-
-    // Save IV.
-    info.setIV(cipher.getIV());
-
-    // Generate HMAC.
-    try {
-      info.setHMAC(generateHMAC(info));
-    } catch (NoSuchAlgorithmException e) {
-      throw new CryptoException(e);
-    } catch (InvalidKeyException e) {
-      throw new CryptoException(e);
-    }
-
-    return info;
-
-  }
-
-  /*
-   * Perform a decryption.
-   *
-   * @argument info info bundle for decryption
-   *
-   * @return decrypted byte[]
-   *
-   * @throws CryptoException
-   */
-  public static byte[] decrypt(CryptoInfo info) throws CryptoException {
-
-    // Check HMAC.
-    try {
-      if (!verifyHMAC(info)) {
-        throw new HMACVerificationException();
-      }
-    } catch (NoSuchAlgorithmException e) {
-      throw new CryptoException(e);
-    } catch (InvalidKeyException e) {
-      throw new CryptoException(e);
-    }
-
-    Cipher cipher = getCipher();
-    try {
-      byte[] encryptionKey = info.getKeys().getEncryptionKey();
-      SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC);
-      cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(info.getIV()));
-    } catch (GeneralSecurityException ex) {
-      ex.printStackTrace();
-      throw new CryptoException(ex);
-    }
-    return commonCrypto(cipher, info.getMessage());
-  }
-
-  /*
-   * Make 2 random 256 bit keys (encryption and HMAC).
-   */
-  public static KeyBundle generateKeys() throws CryptoException {
-    KeyGenerator keygen;
-    try {
-      keygen = KeyGenerator.getInstance(KEY_ALGORITHM_SPEC);
-    } catch (NoSuchAlgorithmException e) {
-      e.printStackTrace();
-      throw new CryptoException(e);
-    }
-
-    keygen.init(KEY_SIZE);
-    byte[] encryptionKey = keygen.generateKey().getEncoded();
-    byte[] hmacKey = keygen.generateKey().getEncoded();
-    return new KeyBundle(encryptionKey, hmacKey);
-  }
-
-  /*
-   * Performs functionality common to both the encryption and decryption
-   * operations.
-   *
-   * Input: Cipher object, non-BaseXX-encoded byte[] input Output:
-   * encrypted/decrypted byte[]
-   */
-  private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage)
-                        throws CryptoException {
-    byte[] outputMessage = null;
-    try {
-      outputMessage = cipher.doFinal(inputMessage);
-    } catch (IllegalBlockSizeException e) {
-      e.printStackTrace();
-      throw new CryptoException(e);
-    } catch (BadPaddingException e) {
-      e.printStackTrace();
-      throw new CryptoException(e);
-    }
-    return outputMessage;
-  }
-
-  /*
-   * Helper to get a Cipher object.
-   * Input: None.
-   * Output: Cipher object.
-   */
-  private static Cipher getCipher() throws CryptoException {
-    Cipher cipher = null;
-    try {
-      cipher = Cipher.getInstance(TRANSFORMATION);
-    } catch (NoSuchAlgorithmException e) {
-      e.printStackTrace();
-      throw new CryptoException(e);
-    } catch (NoSuchPaddingException e) {
-      e.printStackTrace();
-      throw new CryptoException(e);
-    }
-    return cipher;
-  }
-
-  /*
-   * Helper to verify HMAC Input: CryptoInfo Output: true if HMAC is correct
-   */
-  private static boolean verifyHMAC(CryptoInfo bundle) throws NoSuchAlgorithmException, InvalidKeyException {
-    byte[] generatedHMAC = generateHMAC(bundle);
-    byte[] expectedHMAC  = bundle.getHMAC();
-    boolean eq = Arrays.equals(generatedHMAC, expectedHMAC);
-    if (!eq) {
-      System.err.println("Failed HMAC verification.");
-      System.err.println("Expecting: " + Utils.byte2hex(generatedHMAC));
-      System.err.println("Got:       " + Utils.byte2hex(expectedHMAC));
-    }
-    return eq;
-  }
-
-  /*
-   * Helper to generate HMAC Input: CryptoInfo Output: a generated HMAC for
-   * given cipher text
-   */
-  private static byte[] generateHMAC(CryptoInfo bundle) throws NoSuchAlgorithmException, InvalidKeyException {
-    Mac hmacHasher = HKDF.makeHMACHasher(bundle.getKeys().getHMACKey());
-    return hmacHasher.doFinal(Base64.encodeBase64(bundle.getMessage()));
-  }
-}
--- a/mobile/android/base/sync/crypto/KeyBundle.java
+++ b/mobile/android/base/sync/crypto/KeyBundle.java
@@ -36,24 +36,27 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.sync.crypto;
 
 import java.io.UnsupportedEncodingException;
 import java.security.NoSuchAlgorithmException;
 
+import javax.crypto.KeyGenerator;
 import javax.crypto.Mac;
 
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import java.security.InvalidKeyException;
 
 public class KeyBundle {
+    private static final String KEY_ALGORITHM_SPEC = "AES";
+    private static final int    KEY_SIZE           = 256;
 
     private byte[] encryptionKey;
     private byte[] hmacKey;
 
     // These are the same for every sync key bundle.
     private static final byte[] EMPTY_BYTES      = {};
     private static final byte[] ENCR_INPUT_BYTES = {1};
     private static final byte[] HMAC_INPUT_BYTES = {2};
@@ -125,29 +128,54 @@ public class KeyBundle {
       this.encryptionKey = encrKey;
     }
 
     public KeyBundle(byte[] encryptionKey, byte[] hmacKey) {
        this.setEncryptionKey(encryptionKey);
        this.setHMACKey(hmacKey);
     }
 
+    /**
+     * Make a KeyBundle with the specified base64-encoded keys.
+     *
+     * @return A KeyBundle with the specified keys.
+     */
+    public static KeyBundle fromBase64EncodedKeys(String base64EncryptionKey, String base64HmacKey) throws UnsupportedEncodingException {
+      return new KeyBundle(Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")),
+                           Base64.decodeBase64(base64HmacKey.getBytes("UTF-8")));
+    }
+
+    /**
+     * Make a KeyBundle with two random 256 bit keys (encryption and HMAC).
+     *
+     * @return A KeyBundle with random keys.
+     */
+    public static KeyBundle withRandomKeys() throws CryptoException {
+      KeyGenerator keygen;
+      try {
+        keygen = KeyGenerator.getInstance(KEY_ALGORITHM_SPEC);
+      } catch (NoSuchAlgorithmException e) {
+        throw new CryptoException(e);
+      }
+
+      keygen.init(KEY_SIZE);
+      byte[] encryptionKey = keygen.generateKey().getEncoded();
+      byte[] hmacKey = keygen.generateKey().getEncoded();
+
+      return new KeyBundle(encryptionKey, hmacKey);
+    }
+
     public byte[] getEncryptionKey() {
         return encryptionKey;
     }
 
     public void setEncryptionKey(byte[] encryptionKey) {
         this.encryptionKey = encryptionKey;
     }
 
     public byte[] getHMACKey() {
         return hmacKey;
     }
 
     public void setHMACKey(byte[] hmacKey) {
         this.hmacKey = hmacKey;
     }
-
-    public static KeyBundle decodeKeyStrings(String base64EncryptionKey, String base64HmacKey) throws UnsupportedEncodingException {
-      return new KeyBundle(Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")),
-                           Base64.decodeBase64(base64HmacKey.getBytes("UTF-8")));
-    }
 }
--- a/mobile/android/base/sync/jpake/JPakeClient.java
+++ b/mobile/android/base/sync/jpake/JPakeClient.java
@@ -55,17 +55,16 @@ import org.json.simple.parser.ParseExcep
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Logger;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.CryptoInfo;
-import org.mozilla.gecko.sync.crypto.Cryptographer;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
 import org.mozilla.gecko.sync.net.ResourceDelegate;
 import org.mozilla.gecko.sync.net.SyncResourceDelegate;
 import org.mozilla.gecko.sync.setup.Constants;
 import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
 
 import android.util.Log;
@@ -548,21 +547,18 @@ public class JPakeClient implements JPak
 
   /*
    * Helper function to verify an incoming ciphertext and IV against derived
    * keyBundle.
    */
   public boolean verifyCiphertext(String theirCiphertext, String iv,
       KeyBundle keyBundle) throws UnsupportedEncodingException, CryptoException {
     byte[] cleartextBytes = JPAKE_VERIFY_VALUE.getBytes("UTF-8");
-    CryptoInfo info = new CryptoInfo(cleartextBytes, keyBundle);
-    info.setIV(Base64.decodeBase64(iv));
-
-    Cryptographer.encrypt(info);
-    String myCiphertext = new String(Base64.encodeBase64(info.getMessage()));
+    CryptoInfo encrypted = CryptoInfo.encrypt(cleartextBytes, Base64.decodeBase64(iv), keyBundle);
+    String myCiphertext = new String(Base64.encodeBase64(encrypted.getMessage()));
     return myCiphertext.equals(theirCiphertext);
   }
 
   /*
    * (Sender Only)
    *
    * Called from controller, with Sync credentials to be encrypted and sent.
    */
@@ -1216,19 +1212,17 @@ public class JPakeClient implements JPak
    * @throws UnsupportedEncodingException
    */
   public byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle)
       throws CryptoException, UnsupportedEncodingException {
     byte[] ciphertext = Utils.decodeBase64((String) payload
         .get(Constants.JSON_KEY_CIPHERTEXT));
     byte[] iv = Utils.decodeBase64((String) payload.get(Constants.JSON_KEY_IV));
     byte[] hmac = Utils.hex2Byte((String) payload.get(Constants.JSON_KEY_HMAC));
-    byte[] plainbytes = Cryptographer.decrypt(new CryptoInfo(ciphertext, iv,
-        hmac, keybundle));
-    return plainbytes;
+    return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage();
   }
 
   /**
    * Helper method for doing actual encryption.
    *
    * Input: String of JSONObject KeyBundle with keys for decryption
    *
    * Output: ExtendedJSONObject with IV, hmac, ciphertext
@@ -1237,18 +1231,17 @@ public class JPakeClient implements JPak
    * @throws UnsupportedEncodingException
    */
   public ExtendedJSONObject encryptPayload(String data, KeyBundle keyBundle)
       throws UnsupportedEncodingException, CryptoException {
     if (keyBundle == null) {
       throw new NoKeyBundleException();
     }
     byte[] cleartextBytes = data.getBytes("UTF-8");
-    CryptoInfo info = new CryptoInfo(cleartextBytes, keyBundle);
-    Cryptographer.encrypt(info);
+    CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle);
     String message = new String(Base64.encodeBase64(info.getMessage()));
     String iv = new String(Base64.encodeBase64(info.getIV()));
 
     ExtendedJSONObject payload = new ExtendedJSONObject();
     payload.put(Constants.JSON_KEY_CIPHERTEXT, message);
     payload.put(Constants.JSON_KEY_IV, iv);
     if (this.state == State.ENCRYPT_PUT) {
       String hmac = Utils.byte2hex(info.getHMAC());
--- a/mobile/android/sync/java-sources.mn
+++ b/mobile/android/sync/java-sources.mn
@@ -1,1 +1,1 @@
-sync/AlreadySyncingException.java sync/CollectionKeys.java sync/CredentialsSource.java sync/crypto/CryptoException.java sync/crypto/Cryptographer.java sync/crypto/CryptoInfo.java sync/crypto/HKDF.java sync/crypto/HMACVerificationException.java sync/crypto/KeyBundle.java sync/crypto/MissingCryptoInputException.java sync/crypto/NoKeyBundleException.java sync/CryptoRecord.java sync/DelayedWorkTracker.java sync/delegates/FreshStartDelegate.java sync/delegates/GlobalSessionCallback.java sync/delegates/InfoCollectionsDelegate.java sync/delegates/KeyUploadDelegate.java sync/delegates/MetaGlobalDelegate.java sync/delegates/WipeServerDelegate.java sync/ExtendedJSONObject.java sync/GlobalSession.java sync/HTTPFailureException.java sync/InfoCollections.java sync/jpake/BigIntegerHelper.java sync/jpake/Gx3OrGx4IsZeroOrOneException.java sync/jpake/IncorrectZkpException.java sync/jpake/JPakeClient.java sync/jpake/JPakeCrypto.java sync/jpake/JPakeNoActivePairingException.java sync/jpake/JPakeNumGenerator.java sync/jpake/JPakeNumGeneratorRandom.java sync/jpake/JPakeParty.java sync/jpake/JPakeRequest.java sync/jpake/JPakeRequestDelegate.java sync/jpake/JPakeResponse.java sync/jpake/Zkp.java sync/Logger.java sync/MetaGlobal.java sync/MetaGlobalException.java sync/MetaGlobalMissingEnginesException.java sync/MetaGlobalNotSetException.java sync/middleware/Crypto5MiddlewareRepository.java sync/middleware/Crypto5MiddlewareRepositorySession.java sync/middleware/MiddlewareRepository.java sync/net/BaseResource.java sync/net/CompletedEntity.java sync/net/HandleProgressException.java sync/net/Resource.java sync/net/ResourceDelegate.java sync/net/SyncResourceDelegate.java sync/net/SyncResponse.java sync/net/SyncStorageCollectionRequest.java sync/net/SyncStorageCollectionRequestDelegate.java sync/net/SyncStorageRecordRequest.java sync/net/SyncStorageRequest.java sync/net/SyncStorageRequestDelegate.java sync/net/SyncStorageRequestIncrementalDelegate.java sync/net/SyncStorageResponse.java sync/net/TLSSocketFactory.java sync/net/WBOCollectionRequestDelegate.java sync/NoCollectionKeysSetException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.java sync/PrefsSource.java sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java sync/repositories/android/AndroidBrowserBookmarksRepository.java sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java sync/repositories/android/AndroidBrowserHistoryDataAccessor.java sync/repositories/android/AndroidBrowserHistoryDataExtender.java sync/repositories/android/AndroidBrowserHistoryRepository.java sync/repositories/android/AndroidBrowserHistoryRepositorySession.java sync/repositories/android/AndroidBrowserPasswordsDataAccessor.java sync/repositories/android/AndroidBrowserPasswordsRepository.java sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java sync/repositories/android/AndroidBrowserRepository.java sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java sync/repositories/android/AndroidBrowserRepositorySession.java sync/repositories/android/BrowserContract.java sync/repositories/android/PasswordColumns.java sync/repositories/android/RepoUtils.java sync/repositories/BookmarkNeedsReparentingException.java sync/repositories/BookmarksRepository.java sync/repositories/ConstrainedServer11Repository.java sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionBeginDelegate.java sync/repositories/delegates/RepositorySessionCleanDelegate.java sync/repositories/delegates/RepositorySessionCreationDelegate.java sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/RepositorySessionFinishDelegate.java sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java sync/repositories/delegates/RepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionWipeDelegate.java sync/repositories/domain/BookmarkRecord.java sync/repositories/domain/BookmarkRecordFactory.java sync/repositories/domain/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.java sync/repositories/HashSetStoreTracker.java sync/repositories/HistoryRepository.java sync/repositories/IdentityRecordFactory.java sync/repositories/InactiveSessionException.java sync/repositories/InvalidBookmarkTypeException.java sync/repositories/InvalidRequestException.java sync/repositories/InvalidSessionTransitionException.java sync/repositories/MultipleRecordsForGuidException.java sync/repositories/NoGuidForIdException.java sync/repositories/NoStoreDelegateException.java sync/repositories/NullCursorException.java sync/repositories/ParentNotFoundException.java sync/repositories/ProfileDatabaseException.java sync/repositories/RecordFactory.java sync/repositories/RecordFilter.java sync/repositories/Repository.java sync/repositories/RepositorySession.java sync/repositories/RepositorySessionBundle.java sync/repositories/Server11Repository.java sync/repositories/Server11RepositorySession.java sync/repositories/StoreTracker.java sync/repositories/StoreTrackingRepositorySession.java sync/setup/activities/AccountActivity.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.java sync/setup/SyncAuthenticatorService.java sync/stage/AndroidBrowserBookmarksServerSyncStage.java sync/stage/AndroidBrowserHistoryServerSyncStage.java sync/stage/CheckPreconditionsStage.java sync/stage/CompletedStage.java sync/stage/EnsureClusterURLStage.java sync/stage/EnsureKeysStage.java sync/stage/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/ServerSyncStage.java sync/StubActivity.java sync/syncadapter/SyncAdapter.java sync/syncadapter/SyncService.java sync/SyncConfiguration.java sync/SyncConfigurationException.java sync/SyncException.java sync/synchronizer/ConcurrentRecordConsumer.java sync/synchronizer/RecordConsumer.java sync/synchronizer/RecordsChannel.java sync/synchronizer/RecordsChannelDelegate.java sync/synchronizer/RecordsConsumerDelegate.java sync/synchronizer/SerialRecordConsumer.java sync/synchronizer/SessionNotBegunException.java sync/synchronizer/Synchronizer.java sync/synchronizer/SynchronizerDelegate.java sync/synchronizer/SynchronizerSession.java sync/synchronizer/SynchronizerSessionDelegate.java sync/synchronizer/UnbundleError.java sync/synchronizer/UnexpectedSessionException.java sync/SynchronizerConfiguration.java sync/SynchronizerConfigurations.java sync/ThreadPool.java sync/UnexpectedJSONException.java sync/UnknownSynchronizerConfigurationVersionException.java sync/Utils.java
+sync/AlreadySyncingException.java sync/CollectionKeys.java sync/CredentialsSource.java sync/crypto/CryptoException.java sync/crypto/CryptoInfo.java sync/crypto/HKDF.java sync/crypto/HMACVerificationException.java sync/crypto/KeyBundle.java sync/crypto/MissingCryptoInputException.java sync/crypto/NoKeyBundleException.java sync/CryptoRecord.java sync/DelayedWorkTracker.java sync/delegates/FreshStartDelegate.java sync/delegates/GlobalSessionCallback.java sync/delegates/InfoCollectionsDelegate.java sync/delegates/KeyUploadDelegate.java sync/delegates/MetaGlobalDelegate.java sync/delegates/WipeServerDelegate.java sync/ExtendedJSONObject.java sync/GlobalSession.java sync/HTTPFailureException.java sync/InfoCollections.java sync/jpake/BigIntegerHelper.java sync/jpake/Gx3OrGx4IsZeroOrOneException.java sync/jpake/IncorrectZkpException.java sync/jpake/JPakeClient.java sync/jpake/JPakeCrypto.java sync/jpake/JPakeNoActivePairingException.java sync/jpake/JPakeNumGenerator.java sync/jpake/JPakeNumGeneratorRandom.java sync/jpake/JPakeParty.java sync/jpake/JPakeRequest.java sync/jpake/JPakeRequestDelegate.java sync/jpake/JPakeResponse.java sync/jpake/Zkp.java sync/Logger.java sync/MetaGlobal.java sync/MetaGlobalException.java sync/MetaGlobalMissingEnginesException.java sync/MetaGlobalNotSetException.java sync/middleware/Crypto5MiddlewareRepository.java sync/middleware/Crypto5MiddlewareRepositorySession.java sync/middleware/MiddlewareRepository.java sync/net/BaseResource.java sync/net/CompletedEntity.java sync/net/HandleProgressException.java sync/net/Resource.java sync/net/ResourceDelegate.java sync/net/SyncResourceDelegate.java sync/net/SyncResponse.java sync/net/SyncStorageCollectionRequest.java sync/net/SyncStorageCollectionRequestDelegate.java sync/net/SyncStorageRecordRequest.java sync/net/SyncStorageRequest.java sync/net/SyncStorageRequestDelegate.java sync/net/SyncStorageRequestIncrementalDelegate.java sync/net/SyncStorageResponse.java sync/net/TLSSocketFactory.java sync/net/WBOCollectionRequestDelegate.java sync/NoCollectionKeysSetException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.java sync/PrefsSource.java sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java sync/repositories/android/AndroidBrowserBookmarksRepository.java sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java sync/repositories/android/AndroidBrowserHistoryDataAccessor.java sync/repositories/android/AndroidBrowserHistoryDataExtender.java sync/repositories/android/AndroidBrowserHistoryRepository.java sync/repositories/android/AndroidBrowserHistoryRepositorySession.java sync/repositories/android/AndroidBrowserPasswordsDataAccessor.java sync/repositories/android/AndroidBrowserPasswordsRepository.java sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java sync/repositories/android/AndroidBrowserRepository.java sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java sync/repositories/android/AndroidBrowserRepositorySession.java sync/repositories/android/BrowserContract.java sync/repositories/android/PasswordColumns.java sync/repositories/android/RepoUtils.java sync/repositories/BookmarkNeedsReparentingException.java sync/repositories/BookmarksRepository.java sync/repositories/ConstrainedServer11Repository.java sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionBeginDelegate.java sync/repositories/delegates/RepositorySessionCleanDelegate.java sync/repositories/delegates/RepositorySessionCreationDelegate.java sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/RepositorySessionFinishDelegate.java sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java sync/repositories/delegates/RepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionWipeDelegate.java sync/repositories/domain/BookmarkRecord.java sync/repositories/domain/BookmarkRecordFactory.java sync/repositories/domain/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.java sync/repositories/HashSetStoreTracker.java sync/repositories/HistoryRepository.java sync/repositories/IdentityRecordFactory.java sync/repositories/InactiveSessionException.java sync/repositories/InvalidBookmarkTypeException.java sync/repositories/InvalidRequestException.java sync/repositories/InvalidSessionTransitionException.java sync/repositories/MultipleRecordsForGuidException.java sync/repositories/NoGuidForIdException.java sync/repositories/NoStoreDelegateException.java sync/repositories/NullCursorException.java sync/repositories/ParentNotFoundException.java sync/repositories/ProfileDatabaseException.java sync/repositories/RecordFactory.java sync/repositories/RecordFilter.java sync/repositories/Repository.java sync/repositories/RepositorySession.java sync/repositories/RepositorySessionBundle.java sync/repositories/Server11Repository.java sync/repositories/Server11RepositorySession.java sync/repositories/StoreTracker.java sync/repositories/StoreTrackingRepositorySession.java sync/setup/activities/AccountActivity.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.java sync/setup/SyncAuthenticatorService.java sync/stage/AndroidBrowserBookmarksServerSyncStage.java sync/stage/AndroidBrowserHistoryServerSyncStage.java sync/stage/CheckPreconditionsStage.java sync/stage/CompletedStage.java sync/stage/EnsureClusterURLStage.java sync/stage/EnsureKeysStage.java sync/stage/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/ServerSyncStage.java sync/StubActivity.java sync/syncadapter/SyncAdapter.java sync/syncadapter/SyncService.java sync/SyncConfiguration.java sync/SyncConfigurationException.java sync/SyncException.java sync/synchronizer/ConcurrentRecordConsumer.java sync/synchronizer/RecordConsumer.java sync/synchronizer/RecordsChannel.java sync/synchronizer/RecordsChannelDelegate.java sync/synchronizer/RecordsConsumerDelegate.java sync/synchronizer/SerialRecordConsumer.java sync/synchronizer/SessionNotBegunException.java sync/synchronizer/Synchronizer.java sync/synchronizer/SynchronizerDelegate.java sync/synchronizer/SynchronizerSession.java sync/synchronizer/SynchronizerSessionDelegate.java sync/synchronizer/UnbundleError.java sync/synchronizer/UnexpectedSessionException.java sync/SynchronizerConfiguration.java sync/SynchronizerConfigurations.java sync/ThreadPool.java sync/UnexpectedJSONException.java sync/UnknownSynchronizerConfigurationVersionException.java sync/Utils.java