Bug 709324, Bug 730643: persist crypto/keys and meta/global. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Thu, 12 Apr 2012 20:15:53 -0700
changeset 94890 53b7f135e7b5147c3dbe7dc62ed88ee9363f11bc
parent 94889 bbadf6bceab14e36a343b8ba71db91a2d17b120c
child 94891 c379fa75d51011991f25335b1851d7b3920d3832
push id886
push userlsblakk@mozilla.com
push dateMon, 04 Jun 2012 19:57:52 +0000
treeherdermozilla-beta@bbd8d5efd6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs709324, 730643
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 709324, Bug 730643: persist crypto/keys and meta/global. r=rnewman
mobile/android/base/sync/CollectionKeys.java
mobile/android/base/sync/CredentialsSource.java
mobile/android/base/sync/GlobalSession.java
mobile/android/base/sync/InfoCollections.java
mobile/android/base/sync/MetaGlobal.java
mobile/android/base/sync/PersistedMetaGlobal.java
mobile/android/base/sync/PrefsSource.java
mobile/android/base/sync/SyncConfiguration.java
mobile/android/base/sync/crypto/PersistedCrypto5Keys.java
mobile/android/base/sync/delegates/MetaGlobalDelegate.java
mobile/android/base/sync/stage/EnsureCrypto5KeysStage.java
mobile/android/base/sync/stage/FetchMetaGlobalStage.java
mobile/android/base/sync/stage/ServerSyncStage.java
mobile/android/base/sync/stage/SyncClientsEngineStage.java
mobile/android/sync/java-sources.mn
--- a/mobile/android/base/sync/CollectionKeys.java
+++ b/mobile/android/base/sync/CollectionKeys.java
@@ -1,72 +1,37 @@
-/* ***** 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):
- *   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 ***** */
+/* 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;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.HashMap;
 import java.util.Map.Entry;
 
 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.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>();
 
   public static CryptoRecord generateCollectionKeysRecord() throws CryptoException {
     CollectionKeys ck = generateCollectionKeys();
     try {
       return ck.asCryptoRecord();
     } catch (NoCollectionKeysSetException e) {
       // Cannot occur.
-      Log.e(LOG_TAG, "generateCollectionKeys returned a value with no default key. Unpossible.", e);
+      Logger.error(LOG_TAG, "generateCollectionKeys returned a value with no default key.", e);
       throw new IllegalStateException("CollectionKeys should not have null default key.");
     }
   }
 
   /**
    * Randomly generate a basic CollectionKeys object.
    * @throws CryptoException
    */
@@ -133,58 +98,42 @@ public class CollectionKeys {
     ExtendedJSONObject payload = this.asRecordContents();
     CryptoRecord record = new CryptoRecord(payload);
     record.collection = "crypto";
     record.guid       = "keys";
     record.deleted    = false;
     return record;
   }
 
-  public static CollectionKeys fromCryptoRecord(CryptoRecord keys, KeyBundle syncKeyBundle) throws CryptoException, IOException, ParseException, NonObjectJSONException {
+  /**
+   * Set my key bundle and collection keys with the given key bundle and data
+   * (possibly decrypted) from the given record.
+   *
+   * @param keys
+   *          A "crypto/keys" <code>CryptoRecord</code>, encrypted with
+   *          <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null.
+   * @param syncKeyBundle
+   *          If non-null, the sync key bundle to decrypt <code>keys</code> with.
+   */
+  public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
+      throws CryptoException, IOException, ParseException, NonObjectJSONException {
     if (syncKeyBundle != null) {
       keys.keyBundle = syncKeyBundle;
       keys.decrypt();
     }
     ExtendedJSONObject cleartext = keys.payload;
     KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default"));
 
     ExtendedJSONObject collections = cleartext.getObject("collections");
     HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>();
     for (Entry<String, Object> pair : collections.entryIterable()) {
       KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue());
       collectionKeys.put(pair.getKey(), bundle);
     }
 
-    CollectionKeys ck = new CollectionKeys();
-    ck.collectionKeyBundles = collectionKeys;
-    ck.defaultKeyBundle     = defaultKey;
-    return ck;
-  }
-
-  /**
-   * Take a downloaded record, and the Sync Key, decrypting the record and
-   * setting our own keys accordingly.
-   */
-  public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
-                                                                            throws CryptoException,
-                                                                            IOException,
-                                                                            ParseException,
-                                                                            NonObjectJSONException {
-    keys.keyBundle = syncKeyBundle;
-    keys.decrypt();
-    ExtendedJSONObject cleartext = keys.payload;
-    KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default"));
-
-    ExtendedJSONObject collections = cleartext.getObject("collections");
-    HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>();
-    for (Entry<String, Object> pair : collections.entryIterable()) {
-      KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue());
-      collectionKeys.put(pair.getKey(), bundle);
-    }
-
     this.collectionKeyBundles = collectionKeys;
     this.defaultKeyBundle     = defaultKey;
   }
 
   public void setKeyBundleForCollection(String collection, KeyBundle keys) {
     this.collectionKeyBundles.put(collection, keys);
   }
 
--- a/mobile/android/base/sync/CredentialsSource.java
+++ b/mobile/android/base/sync/CredentialsSource.java
@@ -1,47 +1,9 @@
-/* ***** 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):
- *   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 ***** */
+/* 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;
 
-import org.mozilla.gecko.sync.crypto.KeyBundle;
-
 public interface CredentialsSource {
-
   public abstract String credentials();
-  public abstract CollectionKeys getCollectionKeys();
-  public abstract KeyBundle keyForCollection(String collection) throws NoCollectionKeysSetException;
 }
--- a/mobile/android/base/sync/GlobalSession.java
+++ b/mobile/android/base/sync/GlobalSession.java
@@ -32,17 +32,17 @@ import org.mozilla.gecko.sync.net.SyncSt
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
 import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
 import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage;
 import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
 import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
 import org.mozilla.gecko.sync.stage.CompletedStage;
 import org.mozilla.gecko.sync.stage.EnsureClusterURLStage;
-import org.mozilla.gecko.sync.stage.EnsureKeysStage;
+import org.mozilla.gecko.sync.stage.EnsureCrypto5KeysStage;
 import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
 import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
 import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 
@@ -65,26 +65,18 @@ public class GlobalSession implements Cr
 
   public final GlobalSessionCallback callback;
   private Context context;
   private ClientsDataDelegate clientsDelegate;
 
   /*
    * Key accessors.
    */
-  public void setCollectionKeys(CollectionKeys k) {
-    config.setCollectionKeys(k);
-  }
-  @Override
-  public CollectionKeys getCollectionKeys() {
-    return config.collectionKeys;
-  }
-  @Override
-  public KeyBundle keyForCollection(String collection) throws NoCollectionKeysSetException {
-    return config.keyForCollection(collection);
+  public KeyBundle keyBundleForCollection(String collection) throws NoCollectionKeysSetException {
+    return config.getCollectionKeys().keyBundleForCollection(collection);
   }
 
   /*
    * Config passthrough for convenience.
    */
   @Override
   public String credentials() {
     return config.credentials();
@@ -188,17 +180,17 @@ public class GlobalSession implements Cr
   }
 
   protected void prepareStages() {
     stages = new HashMap<Stage, GlobalSyncStage>();
     stages.put(Stage.checkPreconditions,      new CheckPreconditionsStage());
     stages.put(Stage.ensureClusterURL,        new EnsureClusterURLStage());
     stages.put(Stage.fetchInfoCollections,    new FetchInfoCollectionsStage());
     stages.put(Stage.fetchMetaGlobal,         new FetchMetaGlobalStage());
-    stages.put(Stage.ensureKeysStage,         new EnsureKeysStage());
+    stages.put(Stage.ensureKeysStage,         new EnsureCrypto5KeysStage());
     stages.put(Stage.syncClientsEngine,       new SyncClientsEngineStage());
 
     // TODO: more stages.
     stages.put(Stage.syncTabs,                new FennecTabsServerSyncStage());
     stages.put(Stage.syncPasswords,           new PasswordsServerSyncStage());
     stages.put(Stage.syncBookmarks,           new AndroidBrowserBookmarksServerSyncStage());
     stages.put(Stage.syncHistory,             new AndroidBrowserHistoryServerSyncStage());
     stages.put(Stage.syncFormHistory,         new FormHistoryServerSyncStage());
@@ -268,17 +260,16 @@ public class GlobalSession implements Cr
   /*
    * PrefsSource methods.
    */
   @Override
   public SharedPreferences getPrefs(String name, int mode) {
     return this.getContext().getSharedPreferences(name, mode);
   }
 
-  @Override
   public Context getContext() {
     return this.context;
   }
 
   /**
    * Begin a sync.
    * <p>
    * The caller is responsible for:
@@ -350,23 +341,16 @@ public class GlobalSession implements Cr
        * Alert our callback we have a 401 on a cluster URL. This GlobalSession
        * will fail, but the next one will fetch a new cluster URL and will
        * distinguish between "node reassignment" and "user password changed".
        */
       callback.informUnauthorizedResponse(this, config.getClusterURL());
     }
   }
 
-  public void fetchMetaGlobal(MetaGlobalDelegate callback) throws URISyntaxException {
-    if (this.config.metaGlobal == null) {
-      this.config.metaGlobal = new MetaGlobal(config.metaURL(), credentials());
-    }
-    this.config.metaGlobal.fetch(callback);
-  }
-
   public void fetchInfoCollections(InfoCollectionsDelegate callback) throws URISyntaxException {
     if (this.config.infoCollections == null) {
       this.config.infoCollections = new InfoCollections(config.infoURL(), credentials());
     }
     this.config.infoCollections.fetch(callback);
   }
 
   public void uploadKeys(CryptoRecord keysRecord,
@@ -424,16 +408,18 @@ public class GlobalSession implements Cr
     request.put(keysRecord);
   }
 
 
   /*
    * meta/global callbacks.
    */
   public void processMetaGlobal(MetaGlobal global) {
+    config.metaGlobal = global;
+
     Long storageVersion = global.getStorageVersion();
     if (storageVersion < STORAGE_VERSION) {
       // Outdated server.
       freshStart();
       return;
     }
     if (storageVersion > STORAGE_VERSION) {
       // Outdated client!
@@ -445,19 +431,17 @@ public class GlobalSession implements Cr
       // Corrupt meta/global.
       freshStart();
       return;
     }
     String localSyncID = this.getSyncID();
     if (!remoteSyncID.equals(localSyncID)) {
       // Sync ID has changed. Reset timestamps and fetch new keys.
       resetClient(null);
-      if (config.collectionKeys != null) {
-        config.collectionKeys.clear();
-      }
+      config.purgeCryptoKeys();
       config.syncID = remoteSyncID;
       // TODO TODO TODO
     }
     config.persistToPrefs();
     advance();
   }
 
   public void processMissingMetaGlobal(MetaGlobal global) {
@@ -498,17 +482,17 @@ public class GlobalSession implements Cr
     final String metaURL     = session.config.metaURL();
     final String credentials = session.credentials();
 
     wipeServer(session, new WipeServerDelegate() {
 
       @Override
       public void onWiped(long timestamp) {
         session.resetClient(null);
-        session.config.collectionKeys.clear();      // TODO: make sure we clear our keys timestamp.
+        session.config.purgeCryptoKeys();
         session.config.persistToPrefs();
 
         MetaGlobal mg = new MetaGlobal(metaURL, credentials);
         mg.setSyncID(newSyncID);
         mg.setStorageVersion(STORAGE_VERSION);
 
         // It would be good to set the X-If-Unmodified-Since header to `timestamp`
         // for this PUT to ensure at least some level of transactionality.
@@ -560,64 +544,16 @@ public class GlobalSession implements Cr
             freshStartDelegate.onFreshStartFailed(new HTTPFailureException(response));
           }
 
           @Override
           public void handleError(Exception e) {
             Logger.warn(LOG_TAG, "Got error uploading new meta/global.", e);
             freshStartDelegate.onFreshStartFailed(e);
           }
-
-          @Override
-          public MetaGlobalDelegate deferred() {
-            final MetaGlobalDelegate self = this;
-            return new MetaGlobalDelegate() {
-
-              @Override
-              public void handleSuccess(final MetaGlobal global, final SyncStorageResponse response) {
-                ThreadPool.run(new Runnable() {
-                  @Override
-                  public void run() {
-                    self.handleSuccess(global, response);
-                  }});
-              }
-
-              @Override
-              public void handleMissing(final MetaGlobal global, final SyncStorageResponse response) {
-                ThreadPool.run(new Runnable() {
-                  @Override
-                  public void run() {
-                    self.handleMissing(global, response);
-                  }});
-              }
-
-              @Override
-              public void handleFailure(final SyncStorageResponse response) {
-                ThreadPool.run(new Runnable() {
-                  @Override
-                  public void run() {
-                    self.handleFailure(response);
-                  }});
-              }
-
-              @Override
-              public void handleError(final Exception e) {
-                ThreadPool.run(new Runnable() {
-                  @Override
-                  public void run() {
-                    self.handleError(e);
-                  }});
-              }
-
-              @Override
-              public MetaGlobalDelegate deferred() {
-                return this;
-              }
-            };
-          }
         });
       }
 
       @Override
       public void onWipeFailed(Exception e) {
         Logger.warn(LOG_TAG, "Wipe failed.");
         freshStartDelegate.onFreshStartFailed(e);
       }
--- a/mobile/android/base/sync/InfoCollections.java
+++ b/mobile/android/base/sync/InfoCollections.java
@@ -1,44 +1,11 @@
-/* ***** 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):
- * 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 ***** */
+/* 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;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.HashMap;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -51,51 +18,69 @@ import org.mozilla.gecko.sync.net.SyncSt
 
 import android.util.Log;
 
 public class InfoCollections implements SyncStorageRequestDelegate {
   private static final String LOG_TAG = "InfoCollections";
   protected String infoURL;
   protected String credentials;
 
-  // Fetched objects.
-  protected SyncStorageResponse response;
-  private ExtendedJSONObject    record;
-
   // Fields.
   // Rather than storing decimal/double timestamps, as provided by the
   // server, we convert immediately to milliseconds since epoch.
   private HashMap<String, Long> timestamps;
 
   public HashMap<String, Long> getTimestamps() {
-    if (!this.wasSuccessful()) {
+    if (this.timestamps == null) {
       throw new IllegalStateException("No record fetched.");
     }
     return this.timestamps;
   }
 
   public Long getTimestamp(String collection) {
     return this.getTimestamps().get(collection);
   }
 
-  public boolean wasSuccessful() {
-    return this.response.wasSuccessful() &&
-           this.timestamps != null;
+  /**
+   * Test if a given collection needs to be updated.
+   *
+   * @param collection
+   *          The collection to test.
+   * @param lastModified
+   *          Timestamp when local record was last modified.
+   */
+  public boolean updateNeeded(String collection, long lastModified) {
+    Logger.trace(LOG_TAG, "Testing " + collection + " for updateNeeded. Local last modified is " + lastModified + ".");
+
+    // No local record of modification time? Need an update.
+    if (lastModified <= 0) {
+      return true;
+    }
+
+    // No meta/global on the server? We need an update. The server fetch will fail and
+    // then we will upload a fresh meta/global.
+    Long serverLastModified = getTimestamp(collection);
+    if (serverLastModified == null) {
+      return true;
+    }
+
+    // Otherwise, we need an update if our modification time is stale.
+    return (serverLastModified.longValue() > lastModified);
   }
 
   // Temporary location to store our callback.
   private InfoCollectionsDelegate callback;
 
   public InfoCollections(String metaURL, String credentials) {
     this.infoURL     = metaURL;
     this.credentials = credentials;
   }
 
   public void fetch(InfoCollectionsDelegate callback) {
-    if (this.response == null) {
+    if (this.timestamps == null) {
       this.callback = callback;
       this.doFetch();
       return;
     }
     callback.handleSuccess(this);
   }
 
   private void doFetch() {
@@ -113,39 +98,22 @@ public class InfoCollections implements 
             callback.handleError(e);
           }
         }});
     } catch (Exception e) {
       callback.handleError(e);
     }
   }
 
-  public SyncStorageResponse getResponse() {
-    return this.response;
-  }
-
-  protected ExtendedJSONObject ensureRecord() {
-    if (record == null) {
-      record = new ExtendedJSONObject();
-    }
-    return record;
-  }
-
-  protected void setRecord(ExtendedJSONObject record) {
-    this.record = record;
-  }
-
   @SuppressWarnings("unchecked")
-  private void unpack(SyncStorageResponse response) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
-    this.response = response;
-    this.setRecord(response.jsonObjectBody());
-    Log.i(LOG_TAG, "info/collections is " + this.record.toJSONString());
+  public void setFromRecord(ExtendedJSONObject record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
+    Log.i(LOG_TAG, "info/collections is " + record.toJSONString());
     HashMap<String, Long> map = new HashMap<String, Long>();
 
-    Set<Entry<String, Object>> entrySet = this.record.object.entrySet();
+    Set<Entry<String, Object>> entrySet = record.object.entrySet();
     for (Entry<String, Object> entry : entrySet) {
       // These objects are most likely going to be Doubles. Regardless, we
       // want to get them in a more sane time format.
       String key = entry.getKey();
       Object value = entry.getValue();
       if (value instanceof Double) {
         map.put(key, Utils.decimalSecondsToMilliseconds((Double) value));
         continue;
@@ -170,17 +138,17 @@ public class InfoCollections implements 
 
   public String ifUnmodifiedSince() {
     return null;
   }
 
   public void handleRequestSuccess(SyncStorageResponse response) {
     if (response.wasSuccessful()) {
       try {
-        this.unpack(response);
+        this.setFromRecord(response.jsonObjectBody());
         this.callback.handleSuccess(this);
         this.callback = null;
       } catch (Exception e) {
         this.callback.handleError(e);
         this.callback = null;
       }
       return;
     }
--- a/mobile/android/base/sync/MetaGlobal.java
+++ b/mobile/android/base/sync/MetaGlobal.java
@@ -1,44 +1,11 @@
-/* ***** 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):
- * 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 ***** */
+/* 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;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
@@ -48,22 +15,16 @@ import org.mozilla.gecko.sync.net.SyncSt
 
 import android.util.Log;
 
 public class MetaGlobal implements SyncStorageRequestDelegate {
   private static final String LOG_TAG = "MetaGlobal";
   protected String metaURL;
   protected String credentials;
 
-  public boolean isModified;
-  protected boolean isNew;
-
-  // Fetched object.
-  private CryptoRecord record;
-
   // Fields.
   protected ExtendedJSONObject  engines;
   protected Long                storageVersion;
   protected String              syncID;
 
   // Temporary location to store our callback.
   private MetaGlobalDelegate callback;
 
@@ -99,61 +60,62 @@ public class MetaGlobal implements SyncS
       // TODO: PUT! Body!
       r.delegate = this;
       r.deferPut(null);
     } catch (URISyntaxException e) {
       callback.handleError(e);
     }
   }
 
-  private CryptoRecord ensureRecord() {
-    if (this.record == null) {
-      this.record = new CryptoRecord(new ExtendedJSONObject());
-    }
-    return this.record;
+  protected ExtendedJSONObject asRecordContents() {
+    ExtendedJSONObject json = new ExtendedJSONObject();
+    json.put("storageVersion", storageVersion);
+    json.put("engines", engines);
+    json.put("syncID", syncID);
+    return json;
   }
 
-  protected void setRecord(ExtendedJSONObject obj) throws IOException, ParseException, NonObjectJSONException {
-    this.record = CryptoRecord.fromJSONRecord(obj);
+  public CryptoRecord asCryptoRecord() {
+    ExtendedJSONObject payload = this.asRecordContents();
+    CryptoRecord record = new CryptoRecord(payload);
+    record.collection = "meta";
+    record.guid       = "global";
+    record.deleted    = false;
+    return record;
   }
 
-  private void unpack(SyncStorageResponse response) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
-    this.setRecord(response.jsonObjectBody());
+  public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException {
     Log.i(LOG_TAG, "meta/global is " + record.payload.toJSONString());
-    this.isModified = false;
     this.storageVersion = (Long) record.payload.get("storageVersion");
-    this.engines  = record.payload.getObject("engines");
+    this.engines = record.payload.getObject("engines");
     this.syncID = (String) record.payload.get("syncID");
   }
 
   public Long getStorageVersion() {
     return this.storageVersion;
   }
+
   public void setStorageVersion(Long version) {
     this.storageVersion = version;
-    this.ensureRecord().payload.put("storageVersion", version);
-    this.isModified = true;
   }
 
   public ExtendedJSONObject getEngines() {
     return engines;
   }
+
   public void setEngines(ExtendedJSONObject engines) {
     this.engines = engines;
-    this.ensureRecord().payload.put("engines", engines);
-    this.isModified = true;
   }
 
   public String getSyncID() {
     return syncID;
   }
+
   public void setSyncID(String syncID) {
     this.syncID = syncID;
-    this.ensureRecord().payload.put("syncID", syncID);
-    this.isModified = true;
   }
 
   // SyncStorageRequestDelegate methods for fetching.
   public String credentials() {
     return this.credentials;
   }
 
   public String ifUnmodifiedSince() {
@@ -164,25 +126,25 @@ public class MetaGlobal implements SyncS
     if (this.isUploading) {
       this.handleUploadSuccess(response);
     } else {
       this.handleDownloadSuccess(response);
     }
   }
 
   private void handleUploadSuccess(SyncStorageResponse response) {
-    this.isModified = false;
     this.callback.handleSuccess(this, response);
     this.callback = null;
   }
 
   private void handleDownloadSuccess(SyncStorageResponse response) {
     if (response.wasSuccessful()) {
       try {
-        this.unpack(response);
+        CryptoRecord record = CryptoRecord.fromJSONRecord(response.jsonObjectBody());
+        this.setFromRecord(record);
         this.callback.handleSuccess(this, response);
         this.callback = null;
       } catch (Exception e) {
         this.callback.handleError(e);
         this.callback = null;
       }
       return;
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/PersistedMetaGlobal.java
@@ -0,0 +1,75 @@
+/* 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;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.Logger;
+
+import android.content.SharedPreferences;
+
+public class PersistedMetaGlobal {
+  public static final String LOG_TAG = "PersistedMetaGlobal";
+
+  public static final String META_GLOBAL_SERVER_RESPONSE_BODY = "metaGlobalServerResponseBody";
+  public static final String META_GLOBAL_LAST_MODIFIED        = "metaGlobalLastModified";
+
+  protected SharedPreferences prefs;
+
+  public PersistedMetaGlobal(SharedPreferences prefs) {
+    this.prefs = prefs;
+  }
+
+  public MetaGlobal metaGlobal() {
+    String json = prefs.getString(META_GLOBAL_SERVER_RESPONSE_BODY, null);
+    if (json == null) {
+      return null;
+    }
+    MetaGlobal metaGlobal = null;
+    try {
+      CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(json);
+      MetaGlobal mg = new MetaGlobal(null, null);
+      mg.setFromRecord(cryptoRecord);
+      metaGlobal = mg;
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception decrypting persisted meta/global.", e);
+    }
+    return metaGlobal;
+  }
+
+  public void persistMetaGlobal(MetaGlobal metaGlobal) {
+    if (metaGlobal == null) {
+      Logger.debug(LOG_TAG, "Clearing persisted meta/global.");
+      prefs.edit().remove(META_GLOBAL_SERVER_RESPONSE_BODY).commit();
+      return;
+    }
+    try {
+      CryptoRecord cryptoRecord = metaGlobal.asCryptoRecord();
+      String json = cryptoRecord.toJSONString();
+      Logger.debug(LOG_TAG, "Persisting meta/global.");
+      prefs.edit().putString(META_GLOBAL_SERVER_RESPONSE_BODY, json).commit();
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception encrypting while persisting meta/global.", e);
+    }
+  }
+
+  public long lastModified() {
+    return prefs.getLong(META_GLOBAL_LAST_MODIFIED, -1);
+  }
+
+  public void persistLastModified(long lastModified) {
+    if (lastModified <= 0) {
+      Logger.debug(LOG_TAG, "Clearing persisted meta/global last modified timestamp.");
+      prefs.edit().remove(META_GLOBAL_LAST_MODIFIED).commit();
+      return;
+    }
+    Logger.debug(LOG_TAG, "Persisting meta/global last modified timestamp " + lastModified + ".");
+    prefs.edit().putLong(META_GLOBAL_LAST_MODIFIED, lastModified).commit();
+  }
+
+  public void purge() {
+    persistLastModified(-1);
+    persistMetaGlobal(null);
+  }
+}
--- a/mobile/android/base/sync/PrefsSource.java
+++ b/mobile/android/base/sync/PrefsSource.java
@@ -1,63 +1,27 @@
-/* ***** 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):
- *   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 ***** */
+/* 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;
 
-import android.content.Context;
 import android.content.SharedPreferences;
 
 /**
  * Implement PrefsSource to allow other components to fetch a SharedPreferences
  * instance via a Context that you provide.
  *
  * This allows components to use SharedPreferences without being tightly
  * coupled to an Activity.
  *
  * @author rnewman
  *
  */
 public interface PrefsSource {
-  public Context getContext();
-
   /**
    * Return a SharedPreferences instance.
    * @param name
    *        A String, used to identify a preferences 'branch'. Must not be null.
    * @param mode
    *        A bitmask mode, as described in http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29.
    * @return
    *        A new or existing SharedPreferences instance.
--- a/mobile/android/base/sync/SyncConfiguration.java
+++ b/mobile/android/base/sync/SyncConfiguration.java
@@ -5,16 +5,17 @@
 package org.mozilla.gecko.sync;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Map;
 import java.util.Set;
 
 import org.mozilla.gecko.sync.crypto.KeyBundle;
+import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
 
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 
 public class SyncConfiguration implements CredentialsSource {
 
   public class EditorBranch implements Editor {
 
@@ -227,17 +228,19 @@ public class SyncConfiguration implement
       } catch (URISyntaxException e) {
         Logger.warn(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e);
       }
     }
     if (prefs.contains("syncID")) {
       syncID = prefs.getString("syncID", null);
       Logger.info(LOG_TAG, "Set syncID from bundle: " + syncID);
     }
-    // TODO: MetaGlobal, password, infoCollections, collectionKeys.
+    // We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON
+    // and we won't have it on construction.
+    // TODO: MetaGlobal, password, infoCollections.
   }
 
   public void persistToPrefs() {
     this.persistToPrefs(this.getPrefs());
   }
 
   public void persistToPrefs(SharedPreferences prefs) {
     Editor edit = prefs.edit();
@@ -253,26 +256,20 @@ public class SyncConfiguration implement
     // TODO: keys.
   }
 
   @Override
   public String credentials() {
     return username + ":" + password;
   }
 
-  @Override
   public CollectionKeys getCollectionKeys() {
     return collectionKeys;
   }
 
-  @Override
-  public KeyBundle keyForCollection(String collection) throws NoCollectionKeysSetException {
-    return getCollectionKeys().keyBundleForCollection(collection);
-  }
-
   public void setCollectionKeys(CollectionKeys k) {
     collectionKeys = k;
   }
 
   public String nodeWeaveURL() {
     return this.nodeWeaveURL((this.serverURL == null) ? null : this.serverURL.toASCIIString());
   }
 
@@ -374,9 +371,24 @@ public class SyncConfiguration implement
 
   public void persistServerClientRecordTimestamp(long timestamp) {
     getEditor().putLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, timestamp).commit();
   }
 
   public long getPersistedServerClientRecordTimestamp() {
     return getPrefs().getLong(SyncConfiguration.CLIENT_RECORD_TIMESTAMP, 0);
   }
+
+  public void purgeCryptoKeys() {
+    if (collectionKeys != null) {
+      collectionKeys.clear();
+    }
+    persistedCryptoKeys().purge();
+  }
+
+  public PersistedCrypto5Keys persistedCryptoKeys() {
+    return new PersistedCrypto5Keys(getPrefs(), syncKeyBundle);
+  }
+
+  public PersistedMetaGlobal persistedMetaGlobal() {
+    return new PersistedMetaGlobal(getPrefs());
+  }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/crypto/PersistedCrypto5Keys.java
@@ -0,0 +1,99 @@
+/* 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 org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.Logger;
+
+import android.content.SharedPreferences;
+
+public class PersistedCrypto5Keys {
+  public static final String LOG_TAG = "PersistedC5Keys";
+
+  public static final String CRYPTO5_KEYS_SERVER_RESPONSE_BODY = "crypto5KeysServerResponseBody";
+  public static final String CRYPTO5_KEYS_LAST_MODIFIED        = "crypto5KeysLastModified";
+
+  protected SharedPreferences prefs;
+  protected KeyBundle syncKeyBundle;
+
+  public PersistedCrypto5Keys(SharedPreferences prefs, KeyBundle syncKeyBundle) {
+    if (syncKeyBundle == null) {
+      throw new IllegalArgumentException("Null syncKeyBundle passed in to PersistedCrypto5Keys constructor.");
+    }
+    this.prefs = prefs;
+    this.syncKeyBundle = syncKeyBundle;
+  }
+
+  /**
+   * Get persisted crypto/keys.
+   * <p>
+   * crypto/keys is fetched from an encrypted JSON-encoded <code>CryptoRecord</code>.
+   *
+   * @return A <code>CollectionKeys</code> instance or <code>null</code> if none
+   *         is currently persisted.
+   */
+  public CollectionKeys keys() {
+    String keysJSON = prefs.getString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, null);
+    if (keysJSON == null) {
+      return null;
+    }
+    try {
+      CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(keysJSON);
+      CollectionKeys keys = new CollectionKeys();
+      keys.setKeyPairsFromWBO(cryptoRecord, syncKeyBundle);
+      return keys;
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception decrypting persisted crypto/keys.", e);
+      return null;
+    }
+  }
+
+  /**
+   * Persist crypto/keys.
+   * <p>
+   * crypto/keys is stored as an encrypted JSON-encoded <code>CryptoRecord</code>.
+   *
+   * @param keys
+   *          The <code>CollectionKeys</code> object to persist, which should
+   *          have the same default key bundle as the sync key bundle.
+   */
+  public void persistKeys(CollectionKeys keys) {
+    if (keys == null) {
+      Logger.debug(LOG_TAG, "Clearing persisted crypto/keys.");
+      prefs.edit().remove(CRYPTO5_KEYS_SERVER_RESPONSE_BODY).commit();
+      return;
+    }
+    try {
+      CryptoRecord cryptoRecord = keys.asCryptoRecord();
+      cryptoRecord.keyBundle = syncKeyBundle;
+      cryptoRecord.encrypt();
+      String keysJSON = cryptoRecord.toJSONString();
+      Logger.debug(LOG_TAG, "Persisting crypto/keys.");
+      prefs.edit().putString(CRYPTO5_KEYS_SERVER_RESPONSE_BODY, keysJSON).commit();
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception encrypting while persisting crypto/keys.", e);
+    }
+  }
+
+  public long lastModified() {
+    return prefs.getLong(CRYPTO5_KEYS_LAST_MODIFIED, -1);
+  }
+
+  public void persistLastModified(long lastModified) {
+    if (lastModified <= 0) {
+      Logger.debug(LOG_TAG, "Clearing persisted crypto/keys last modified timestamp.");
+      prefs.edit().remove(CRYPTO5_KEYS_LAST_MODIFIED).commit();
+      return;
+    }
+    Logger.debug(LOG_TAG, "Persisting crypto/keys last modified timestamp " + lastModified + ".");
+    prefs.edit().putLong(CRYPTO5_KEYS_LAST_MODIFIED, lastModified).commit();
+  }
+
+  public void purge() {
+    persistLastModified(-1);
+    persistKeys(null);
+  }
+}
--- a/mobile/android/base/sync/delegates/MetaGlobalDelegate.java
+++ b/mobile/android/base/sync/delegates/MetaGlobalDelegate.java
@@ -1,49 +1,15 @@
-/* ***** 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):
- * 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 ***** */
+/* 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.delegates;
 
 import org.mozilla.gecko.sync.MetaGlobal;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
 public interface MetaGlobalDelegate {
   public void handleSuccess(MetaGlobal global, SyncStorageResponse response);
   public void handleMissing(MetaGlobal global, SyncStorageResponse response);
   public void handleFailure(SyncStorageResponse response);
   public void handleError(Exception e);
-  public MetaGlobalDelegate deferred();
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/stage/EnsureCrypto5KeysStage.java
@@ -0,0 +1,175 @@
+/* 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.stage;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.sync.CollectionKeys;
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.Logger;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.crypto.CryptoException;
+import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
+import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
+import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class EnsureCrypto5KeysStage implements GlobalSyncStage, SyncStorageRequestDelegate, KeyUploadDelegate {
+  private static final String LOG_TAG = "EnsureC5KeysStage";
+  private static final String CRYPTO_COLLECTION = "crypto";
+  protected GlobalSession session;
+  protected boolean retrying = false;
+
+  @Override
+  public void execute(GlobalSession session) throws NoSuchStageException {
+    this.session = session;
+
+    InfoCollections infoCollections = session.config.infoCollections;
+    if (infoCollections == null) {
+      session.abort(null, "No info/collections set in EnsureCrypto5KeysStage.");
+      return;
+    }
+
+    PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
+    long lastModified = pck.lastModified();
+    if (!infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) {
+      // Try to use our local collection keys for this session.
+      Logger.info(LOG_TAG, "Trying to use persisted collection keys for this session.");
+      CollectionKeys keys = pck.keys();
+      if (keys != null) {
+        Logger.info(LOG_TAG, "Using persisted collection keys for this session.");
+        session.config.setCollectionKeys(keys);
+        session.advance();
+        return;
+      }
+      Logger.info(LOG_TAG, "Failed to use persisted collection keys for this session.");
+    }
+
+    // We need an update: fetch or upload keys as necessary.
+    Logger.info(LOG_TAG, "Fetching fresh collection keys for this session.");
+    try {
+      SyncStorageRecordRequest request = new SyncStorageRecordRequest(session.wboURI(CRYPTO_COLLECTION, "keys"));
+      request.delegate = this;
+      request.get();
+    } catch (URISyntaxException e) {
+      session.abort(e, "Invalid URI.");
+    }
+  }
+
+  @Override
+  public String credentials() {
+    return session.credentials();
+  }
+
+  @Override
+  public String ifUnmodifiedSince() {
+    // TODO: last key time!
+    return null;
+  }
+
+  @Override
+  public void handleRequestSuccess(SyncStorageResponse response) {
+    CollectionKeys k = new CollectionKeys();
+    try {
+      ExtendedJSONObject body = response.jsonObjectBody();
+      if (Logger.LOG_PERSONAL_INFORMATION) {
+        Logger.pii(LOG_TAG, "Fetched keys: " + body.toJSONString());
+      }
+      k.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle);
+
+    } catch (IllegalStateException e) {
+      session.abort(e, "Invalid keys WBO.");
+      return;
+    } catch (ParseException e) {
+      session.abort(e, "Invalid keys WBO.");
+      return;
+    } catch (NonObjectJSONException e) {
+      session.abort(e, "Invalid keys WBO.");
+      return;
+    } catch (IOException e) {
+      // Some kind of lower-level error.
+      session.abort(e, "IOException fetching keys.");
+      return;
+    } catch (CryptoException e) {
+      session.abort(e, "CryptoException handling keys WBO.");
+      return;
+    }
+
+    // New keys! Persist keys and server timestamp.
+    Logger.info(LOG_TAG, "Setting fetched keys for this session.");
+    session.config.setCollectionKeys(k);
+    Logger.trace(LOG_TAG, "Persisting fetched keys and last modified.");
+    PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
+    pck.persistKeys(k);
+    // Take the timestamp from the response since it is later than the timestamp from info/collections.
+    pck.persistLastModified(response.normalizedWeaveTimestamp());
+
+    session.advance();
+  }
+
+  @Override
+  public void handleRequestFailure(SyncStorageResponse response) {
+    if (retrying) {
+      session.handleHTTPError(response, "Failure in refetching uploaded keys.");
+      return;
+    }
+
+    int statusCode = response.getStatusCode();
+    Logger.debug(LOG_TAG, "Got " + statusCode + " fetching keys.");
+    if (statusCode == 404) {
+      // No keys. Generate and upload, then refetch.
+      CryptoRecord keysWBO;
+      try {
+        keysWBO = CollectionKeys.generateCollectionKeysRecord();
+      } catch (CryptoException e) {
+        session.abort(e, "Couldn't generate new key bundle.");
+        return;
+      }
+      keysWBO.keyBundle = session.config.syncKeyBundle;
+      try {
+        keysWBO.encrypt();
+      } catch (UnsupportedEncodingException e) {
+        // Shouldn't occur, so let's not waste too much time on niceties. TODO
+        session.abort(e, "Couldn't encrypt new key bundle: unsupported encoding.");
+        return;
+      } catch (CryptoException e) {
+        session.abort(e, "Couldn't encrypt new key bundle.");
+        return;
+      }
+      session.uploadKeys(keysWBO, this);
+      return;
+    }
+    session.handleHTTPError(response, "Failure fetching keys.");
+  }
+
+  @Override
+  public void handleRequestError(Exception ex) {
+    session.abort(ex, "Failure fetching keys.");
+  }
+
+  @Override
+  public void onKeysUploaded() {
+    Logger.debug(LOG_TAG, "New keys uploaded. Starting stage again to fetch them.");
+    try {
+      retrying = true;
+      this.execute(this.session);
+    } catch (NoSuchStageException e) {
+      session.abort(e, "No such stage.");
+    }
+  }
+
+  @Override
+  public void onKeyUploadFailed(Exception e) {
+    Logger.warn(LOG_TAG, "Key upload failed. Aborting sync.");
+    session.abort(e, "Key upload failed.");
+  }
+}
--- a/mobile/android/base/sync/stage/FetchMetaGlobalStage.java
+++ b/mobile/android/base/sync/stage/FetchMetaGlobalStage.java
@@ -1,65 +1,41 @@
-/* ***** 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):
- *  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 ***** */
+/* 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.stage;
 
-import java.net.URISyntaxException;
-
 import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.InfoCollections;
+import org.mozilla.gecko.sync.Logger;
 import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.PersistedMetaGlobal;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
 public class FetchMetaGlobalStage implements GlobalSyncStage {
+  private static final String LOG_TAG = "FetchMetaGlobalStage";
+  private static final String META_COLLECTION = "meta";
 
   public class StageMetaGlobalDelegate implements MetaGlobalDelegate {
 
     private GlobalSession session;
     public StageMetaGlobalDelegate(GlobalSession session) {
       this.session = session;
     }
 
     @Override
     public void handleSuccess(MetaGlobal global, SyncStorageResponse response) {
+      Logger.trace(LOG_TAG, "Persisting fetched meta/global and last modified.");
+      PersistedMetaGlobal pmg = session.config.persistedMetaGlobal();
+      pmg.persistMetaGlobal(global);
+      // Take the timestamp from the response since it is later than the timestamp from info/collections.
+      pmg.persistLastModified(response.normalizedWeaveTimestamp());
+
       session.processMetaGlobal(global);
     }
 
     @Override
     public void handleFailure(SyncStorageResponse response) {
       session.handleHTTPError(response, "Failure fetching meta/global.");
     }
 
@@ -67,26 +43,37 @@ public class FetchMetaGlobalStage implem
     public void handleError(Exception e) {
       session.abort(e, "Failure fetching meta/global.");
     }
 
     @Override
     public void handleMissing(MetaGlobal global, SyncStorageResponse response) {
       session.processMissingMetaGlobal(global);
     }
-
-    @Override
-    public MetaGlobalDelegate deferred() {
-      // TODO: defer!
-      return this;
-    }
   }
 
   @Override
   public void execute(GlobalSession session) throws NoSuchStageException {
-    try {
-      session.fetchMetaGlobal(new StageMetaGlobalDelegate(session));
-    } catch (URISyntaxException e) {
-      session.abort(e, "Invalid URI.");
+    InfoCollections infoCollections = session.config.infoCollections;
+    if (infoCollections == null) {
+      session.abort(null, "No info/collections set in FetchMetaGlobalStage.");
+      return;
     }
-  }
 
+    long lastModified = session.config.persistedMetaGlobal().lastModified();
+    if (!infoCollections.updateNeeded(META_COLLECTION, lastModified)) {
+      // Try to use our local collection keys for this session.
+      Logger.info(LOG_TAG, "Trying to use persisted meta/global for this session.");
+      MetaGlobal global = session.config.persistedMetaGlobal().metaGlobal();
+      if (global != null) {
+        Logger.info(LOG_TAG, "Using persisted meta/global for this session.");
+        session.processMetaGlobal(global); // Calls session.advance().
+        return;
+      }
+      Logger.info(LOG_TAG, "Failed to use persisted meta/global for this session.");
+    }
+
+    // We need an update: fetch or upload meta/global as necessary.
+    Logger.info(LOG_TAG, "Fetching fresh meta/global for this session.");
+    MetaGlobal global = new MetaGlobal(session.config.metaURL(), session.credentials());
+    global.fetch(new StageMetaGlobalDelegate(session));
+  }
 }
--- a/mobile/android/base/sync/stage/ServerSyncStage.java
+++ b/mobile/android/base/sync/stage/ServerSyncStage.java
@@ -62,17 +62,17 @@ public abstract class ServerSyncStage im
   /**
    * Return a Crypto5Middleware-wrapped Server11Repository.
    *
    * @throws NoCollectionKeysSetException
    * @throws URISyntaxException
    */
   protected Repository wrappedServerRepo() throws NoCollectionKeysSetException, URISyntaxException {
     String collection = this.getCollection();
-    KeyBundle collectionKey = session.keyForCollection(collection);
+    KeyBundle collectionKey = session.keyBundleForCollection(collection);
     Crypto5MiddlewareRepository cryptoRepo = new Crypto5MiddlewareRepository(getRemoteRepository(), collectionKey);
     cryptoRepo.recordFactory = getRecordFactory();
     return cryptoRepo;
   }
 
   protected String bundlePrefix() {
     return this.getCollection() + ".";
   }
--- a/mobile/android/base/sync/stage/SyncClientsEngineStage.java
+++ b/mobile/android/base/sync/stage/SyncClientsEngineStage.java
@@ -157,17 +157,17 @@ public class SyncClientsEngineStage impl
         return;
       }
       wipeAndStore(r);
     }
 
     @Override
     public KeyBundle keyBundle() {
       try {
-        return session.keyForCollection(COLLECTION_NAME);
+        return session.keyBundleForCollection(COLLECTION_NAME);
       } catch (NoCollectionKeysSetException e) {
         session.abort(e, "No collection keys set.");
         return null;
       }
     }
   }
 
   public class ClientUploadDelegate extends WBORequestDelegate {
@@ -236,17 +236,17 @@ public class SyncClientsEngineStage impl
     public void handleRequestError(Exception ex) {
       Logger.info(LOG_TAG, "Client upload error. Aborting sync.");
       session.abort(ex, "Client upload failed.");
     }
 
     @Override
     public KeyBundle keyBundle() {
       try {
-        return session.keyForCollection(COLLECTION_NAME);
+        return session.keyBundleForCollection(COLLECTION_NAME);
       } catch (NoCollectionKeysSetException e) {
         session.abort(e, "No collection keys set.");
         return null;
       }
     }
   }
 
   @Override
--- 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/CommandProcessor.java sync/CommandRunner.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/ClientsDataDelegate.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/JPakeJson.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/stage/CompleteStage.java sync/jpake/stage/ComputeFinalStage.java sync/jpake/stage/ComputeKeyVerificationStage.java sync/jpake/stage/ComputeStepOneStage.java sync/jpake/stage/ComputeStepTwoStage.java sync/jpake/stage/DecryptDataStage.java sync/jpake/stage/DeleteChannel.java sync/jpake/stage/GetChannelStage.java sync/jpake/stage/GetRequestStage.java sync/jpake/stage/JPakeStage.java sync/jpake/stage/PutRequestStage.java sync/jpake/stage/VerifyPairingStage.java sync/jpake/Zkp.java sync/KeyBundleProvider.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/middleware/MiddlewareRepositorySession.java sync/net/BaseResource.java sync/net/CompletedEntity.java sync/net/ConnectionMonitorThread.java sync/net/HandleProgressException.java sync/net/HttpResponseObserver.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/net/WBORequestDelegate.java sync/NoCollectionKeysSetException.java sync/NodeAuthenticationException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.java sync/NullClusterURLException.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/AndroidBrowserRepository.java sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java sync/repositories/android/AndroidBrowserRepositorySession.java sync/repositories/android/BookmarksDeletionManager.java sync/repositories/android/BrowserContractHelpers.java sync/repositories/android/CachedSQLiteOpenHelper.java sync/repositories/android/ClientsDatabase.java sync/repositories/android/ClientsDatabaseAccessor.java sync/repositories/android/FennecTabsRepository.java sync/repositories/android/FormHistoryRepositorySession.java sync/repositories/android/PasswordsRepositorySession.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/ClientRecord.java sync/repositories/domain/ClientRecordFactory.java sync/repositories/domain/FormHistoryRecord.java sync/repositories/domain/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.java sync/repositories/domain/TabsRecord.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/NoContentProviderException.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/ActivityUtils.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.java sync/setup/InvalidSyncKeyException.java sync/setup/SyncAccounts.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/FennecTabsServerSyncStage.java sync/stage/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/FormHistoryServerSyncStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/PasswordsServerSyncStage.java sync/stage/ServerSyncStage.java sync/stage/SyncClientsEngineStage.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/CommandProcessor.java sync/CommandRunner.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/crypto/PersistedCrypto5Keys.java sync/CryptoRecord.java sync/DelayedWorkTracker.java sync/delegates/ClientsDataDelegate.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/JPakeJson.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/stage/CompleteStage.java sync/jpake/stage/ComputeFinalStage.java sync/jpake/stage/ComputeKeyVerificationStage.java sync/jpake/stage/ComputeStepOneStage.java sync/jpake/stage/ComputeStepTwoStage.java sync/jpake/stage/DecryptDataStage.java sync/jpake/stage/DeleteChannel.java sync/jpake/stage/GetChannelStage.java sync/jpake/stage/GetRequestStage.java sync/jpake/stage/JPakeStage.java sync/jpake/stage/PutRequestStage.java sync/jpake/stage/VerifyPairingStage.java sync/jpake/Zkp.java sync/KeyBundleProvider.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/middleware/MiddlewareRepositorySession.java sync/net/BaseResource.java sync/net/CompletedEntity.java sync/net/ConnectionMonitorThread.java sync/net/HandleProgressException.java sync/net/HttpResponseObserver.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/net/WBORequestDelegate.java sync/NoCollectionKeysSetException.java sync/NodeAuthenticationException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.java sync/NullClusterURLException.java sync/PersistedMetaGlobal.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/AndroidBrowserRepository.java sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java sync/repositories/android/AndroidBrowserRepositorySession.java sync/repositories/android/BookmarksDeletionManager.java sync/repositories/android/BrowserContractHelpers.java sync/repositories/android/CachedSQLiteOpenHelper.java sync/repositories/android/ClientsDatabase.java sync/repositories/android/ClientsDatabaseAccessor.java sync/repositories/android/FennecTabsRepository.java sync/repositories/android/FormHistoryRepositorySession.java sync/repositories/android/PasswordsRepositorySession.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/ClientRecord.java sync/repositories/domain/ClientRecordFactory.java sync/repositories/domain/FormHistoryRecord.java sync/repositories/domain/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.java sync/repositories/domain/TabsRecord.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/NoContentProviderException.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/ActivityUtils.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.java sync/setup/InvalidSyncKeyException.java sync/setup/SyncAccounts.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/EnsureCrypto5KeysStage.java sync/stage/FennecTabsServerSyncStage.java sync/stage/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/FormHistoryServerSyncStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/PasswordsServerSyncStage.java sync/stage/ServerSyncStage.java sync/stage/SyncClientsEngineStage.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