Bug 709323 - Handle key changes in info/collections. r=rnewman, a=blocking-fennec

This commit is contained in:
Nick Alexander 2012-05-04 12:27:06 -07:00
parent 328b5ebbe6
commit bb1e8744d8
5 changed files with 160 additions and 80 deletions

View File

@ -7,7 +7,9 @@ package org.mozilla.gecko.sync;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import org.json.simple.JSONArray;
import org.json.simple.parser.ParseException;
@ -16,21 +18,9 @@ import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
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.
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
@ -51,12 +41,16 @@ public class CollectionKeys {
return this.defaultKeyBundle;
}
public boolean keyBundleForCollectionIsNotDefault(String collection) {
return collectionKeyBundles.containsKey(collection);
}
public KeyBundle keyBundleForCollection(String collection)
throws NoCollectionKeysSetException {
throws NoCollectionKeysSetException {
if (this.defaultKeyBundle == null) {
throw new NoCollectionKeysSetException();
}
if (collectionKeyBundles.containsKey(collection)) {
if (keyBundleForCollectionIsNotDefault(collection)) {
return collectionKeyBundles.get(collection);
}
return this.defaultKeyBundle;
@ -145,4 +139,35 @@ public class CollectionKeys {
this.defaultKeyBundle = null;
this.collectionKeyBundles = new HashMap<String, KeyBundle>();
}
/**
* Return set of collections where key is either missing from one collection
* or not the same in both collections.
* <p>
* Does not check for different default keys.
*/
public static Set<String> differences(CollectionKeys a, CollectionKeys b) {
Set<String> differences = new HashSet<String>();
// Iterate through one collection, collecting missing and differences.
for (String collection : a.collectionKeyBundles.keySet()) {
KeyBundle key = b.collectionKeyBundles.get(collection);
if (key == null) {
differences.add(collection);
continue;
}
if (!key.equals(a.collectionKeyBundles.get(collection))) {
differences.add(collection);
}
}
// Now iterate through the other collection, collecting just the missing.
for (String collection : b.collectionKeyBundles.keySet()) {
if (!a.collectionKeyBundles.containsKey(collection)) {
differences.add(collection);
}
}
return differences;
}
}

View File

@ -6,6 +6,7 @@ package org.mozilla.gecko.sync;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ -22,20 +23,27 @@ public class InfoCollections implements SyncStorageRequestDelegate {
protected String infoURL;
protected String credentials;
// 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.timestamps == null) {
throw new IllegalStateException("No record fetched.");
}
return this.timestamps;
}
/**
* Fields fetched from the server, or <code>null</code> if not yet fetched.
* <p>
* Rather than storing decimal/double timestamps, as provided by the server,
* we convert immediately to milliseconds since epoch.
*/
private Map<String, Long> timestamps = null;
/**
* Return the timestamp for the given collection, or null if the timestamps
* have not been fetched or the given collection does not have a timestamp.
*
* @param collection
* The collection to inspect.
* @return the timestamp in milliseconds since epoch.
*/
public Long getTimestamp(String collection) {
return this.getTimestamps().get(collection);
if (timestamps == null) {
return null;
}
return timestamps.get(collection);
}
/**
@ -74,12 +82,8 @@ public class InfoCollections implements SyncStorageRequestDelegate {
}
public void fetch(InfoCollectionsDelegate callback) {
if (this.timestamps == null) {
this.callback = callback;
this.doFetch();
return;
}
callback.handleSuccess(this);
this.callback = callback;
this.doFetch();
}
private void doFetch() {

View File

@ -1,40 +1,6 @@
/* ***** 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 <jvoll@mozilla.com>
* 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.crypto;
@ -47,6 +13,7 @@ import javax.crypto.Mac;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.Utils;
import java.security.InvalidKeyException;
import java.util.Arrays;
import java.util.Locale;
public class KeyBundle {
@ -178,4 +145,14 @@ public class KeyBundle {
public void setHMACKey(byte[] hmacKey) {
this.hmacKey = hmacKey;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof KeyBundle)) {
return false;
}
KeyBundle other = (KeyBundle) o;
return Arrays.equals(other.encryptionKey, this.encryptionKey) &&
Arrays.equals(other.hmacKey, this.hmacKey);
}
}

View File

@ -78,6 +78,10 @@ public class PersistedCrypto5Keys {
}
}
public boolean persistedKeysExist() {
return lastModified() > 0;
}
public long lastModified() {
return prefs.getLong(CRYPTO5_KEYS_LAST_MODIFIED, -1);
}

View File

@ -6,6 +6,8 @@ package org.mozilla.gecko.sync.stage;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.Set;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.sync.CollectionKeys;
@ -14,8 +16,10 @@ 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.NoCollectionKeysSetException;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
@ -44,7 +48,7 @@ implements SyncStorageRequestDelegate, KeyUploadDelegate {
PersistedCrypto5Keys pck = session.config.persistedCryptoKeys();
long lastModified = pck.lastModified();
if (!infoCollections.updateNeeded(CRYPTO_COLLECTION, lastModified)) {
if (retrying || !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();
@ -79,16 +83,65 @@ implements SyncStorageRequestDelegate, KeyUploadDelegate {
return null;
}
protected void setAndPersist(PersistedCrypto5Keys pck, CollectionKeys keys, long timestamp) {
session.config.setCollectionKeys(keys);
pck.persistKeys(keys);
pck.persistLastModified(timestamp);
}
/**
* Return collections where either the individual key has changed, or if the
* new default key is not the same as the old default key, where the
* collection is using the default key.
*/
protected Set<String> collectionsToUpdate(CollectionKeys oldKeys, CollectionKeys newKeys) {
// These keys have explicitly changed; they definitely need updating.
Set<String> changedKeys = new HashSet<String>(CollectionKeys.differences(oldKeys, newKeys));
boolean defaultKeyChanged = true; // Most pessimistic is to assume default key has changed.
KeyBundle newDefaultKeyBundle = null;
try {
KeyBundle oldDefaultKeyBundle = oldKeys.defaultKeyBundle();
newDefaultKeyBundle = newKeys.defaultKeyBundle();
defaultKeyChanged = !oldDefaultKeyBundle.equals(newDefaultKeyBundle);
} catch (NoCollectionKeysSetException e) {
Logger.warn(LOG_TAG, "NoCollectionKeysSetException in EnsureCrypto5KeysStage.", e);
}
if (newDefaultKeyBundle == null) {
Logger.info(LOG_TAG, "New default key not provided; returning changed individual keys.");
return changedKeys;
}
if (!defaultKeyChanged) {
Logger.info(LOG_TAG, "New default key is the same as old default key; returning changed individual keys.");
return changedKeys;
}
// New keys have a different default/sync key; check known collections against the default key.
Logger.info(LOG_TAG, "New default key is not the same as old default key.");
for (Stage stage : Stage.getNamedStages()) {
String name = stage.getRepositoryName();
if (!newKeys.keyBundleForCollectionIsNotDefault(name)) {
// Default key has changed, so this collection has changed.
changedKeys.add(name);
}
}
return changedKeys;
}
@Override
public void handleRequestSuccess(SyncStorageResponse response) {
CollectionKeys k = new CollectionKeys();
// Take the timestamp from the response since it is later than the timestamp from info/collections.
long responseTimestamp = response.normalizedWeaveTimestamp();
CollectionKeys keys = 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);
keys.setKeyPairsFromWBO(CryptoRecord.fromJSONRecord(body), session.config.syncKeyBundle);
} catch (IllegalStateException e) {
session.abort(e, "Invalid keys WBO.");
return;
@ -107,15 +160,32 @@ implements SyncStorageRequestDelegate, KeyUploadDelegate {
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());
if (!pck.persistedKeysExist()) {
// New keys, and no old keys! Persist keys and server timestamp.
Logger.info(LOG_TAG, "Setting fetched keys for this session; persisting fetched keys and last modified.");
setAndPersist(pck, keys, responseTimestamp);
session.advance();
return;
}
// New keys, but we had old keys. Check for differences.
CollectionKeys oldKeys = pck.keys();
Set<String> changedCollections = collectionsToUpdate(oldKeys, keys);
if (!changedCollections.isEmpty()) {
// New keys, different from old keys.
Logger.info(LOG_TAG, "Fetched keys are not the same as persisted keys; " +
"setting fetched keys for this session before resetting changed engines.");
setAndPersist(pck, keys, responseTimestamp);
session.resetStagesByName(changedCollections);
session.abort(null, "crypto/keys changed on server.");
return;
}
// New keys don't differ from old keys; persist timestamp and move on.
Logger.trace(LOG_TAG, "Fetched keys are the same as persisted keys; persisting only last modified.");
session.config.setCollectionKeys(oldKeys);
pck.persistLastModified(response.normalizedWeaveTimestamp());
session.advance();
}