diff -r 000000000000 -r 6474c204b198 mobile/android/base/sync/CollectionKeys.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/sync/CollectionKeys.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,195 @@ +/* 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.HashSet; +import java.util.Map.Entry; +import java.util.Set; + +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; + +public class CollectionKeys { + private KeyBundle defaultKeyBundle = null; + private final HashMap collectionKeyBundles = new HashMap(); + + /** + * Randomly generate a basic CollectionKeys object. + * @throws CryptoException + */ + public static CollectionKeys generateCollectionKeys() throws CryptoException { + CollectionKeys ck = new CollectionKeys(); + ck.clear(); + ck.defaultKeyBundle = KeyBundle.withRandomKeys(); + // TODO: eventually we would like to keep per-collection keys, just generate + // new ones as appropriate. + return ck; + } + + public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException { + if (this.defaultKeyBundle == null) { + throw new NoCollectionKeysSetException(); + } + return this.defaultKeyBundle; + } + + public boolean keyBundleForCollectionIsNotDefault(String collection) { + return collectionKeyBundles.containsKey(collection); + } + + public KeyBundle keyBundleForCollection(String collection) + throws NoCollectionKeysSetException { + if (this.defaultKeyBundle == null) { + throw new NoCollectionKeysSetException(); + } + if (keyBundleForCollectionIsNotDefault(collection)) { + return collectionKeyBundles.get(collection); + } + return this.defaultKeyBundle; + } + + /** + * Take a pair of values in a JSON array, handing them off to KeyBundle to + * produce a usable keypair. + */ + private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException { + String encKeyStr = (String) array.get(0); + String hmacKeyStr = (String) array.get(1); + return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr); + } + + @SuppressWarnings("unchecked") + private static JSONArray keyBundleToArray(KeyBundle bundle) { + // Generate JSON. + JSONArray keysArray = new JSONArray(); + keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey()))); + keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey()))); + return keysArray; + } + + private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException { + ExtendedJSONObject json = new ExtendedJSONObject(); + json.put("id", "keys"); + json.put("collection", "crypto"); + json.put("default", keyBundleToArray(this.defaultKeyBundle())); + ExtendedJSONObject colls = new ExtendedJSONObject(); + for (Entry collKey : collectionKeyBundles.entrySet()) { + colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue())); + } + json.put("collections", colls); + return json; + } + + public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException { + ExtendedJSONObject payload = this.asRecordContents(); + CryptoRecord record = new CryptoRecord(payload); + record.collection = "crypto"; + record.guid = "keys"; + record.deleted = false; + return record; + } + + /** + * 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" CryptoRecord, encrypted with + * syncKeyBundle if syncKeyBundle is non-null. + * @param syncKeyBundle + * If non-null, the sync key bundle to decrypt keys with. + */ + public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle) + throws CryptoException, IOException, ParseException, NonObjectJSONException { + if (keys == null) { + throw new IllegalArgumentException("cannot set key pairs from null record"); + } + 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 collectionKeys = new HashMap(); + for (Entry pair : collections.entrySet()) { + KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue()); + collectionKeys.put(pair.getKey(), bundle); + } + + this.collectionKeyBundles.clear(); + this.collectionKeyBundles.putAll(collectionKeys); + this.defaultKeyBundle = defaultKey; + } + + public void setKeyBundleForCollection(String collection, KeyBundle keys) { + this.collectionKeyBundles.put(collection, keys); + } + + public void setDefaultKeyBundle(KeyBundle keys) { + this.defaultKeyBundle = keys; + } + + public void clear() { + this.defaultKeyBundle = null; + this.collectionKeyBundles.clear(); + } + + /** + * Return set of collections where key is either missing from one collection + * or not the same in both collections. + *

+ * Does not check for different default keys. + */ + public static Set differences(CollectionKeys a, CollectionKeys b) { + Set differences = new HashSet(); + Set collections = new HashSet(a.collectionKeyBundles.keySet()); + collections.addAll(b.collectionKeyBundles.keySet()); + + // Iterate through one collection, collecting missing and differences. + for (String collection : collections) { + KeyBundle keyA; + KeyBundle keyB; + try { + keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate. + keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate. + } catch (NoCollectionKeysSetException e) { + differences.add(collection); + continue; + } + // keyA and keyB are not null at this point. + if (!keyA.equals(keyB)) { + differences.add(collection); + } + } + + return differences; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CollectionKeys)) { + return false; + } + CollectionKeys other = (CollectionKeys) o; + try { + // It would be nice to use map equality here, but there can be map entries + // where the key is the default key that should compare equal to a missing + // map entry. Therefore, we always compute the set of differences. + return defaultKeyBundle().equals(other.defaultKeyBundle()) && + CollectionKeys.differences(this, other).isEmpty(); + } catch (NoCollectionKeysSetException e) { + // If either default key bundle is not set, we'll say the bundles are not equal. + return false; + } + } +}