1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/CollectionKeys.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,195 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync; 1.9 + 1.10 +import java.io.IOException; 1.11 +import java.io.UnsupportedEncodingException; 1.12 +import java.util.HashMap; 1.13 +import java.util.HashSet; 1.14 +import java.util.Map.Entry; 1.15 +import java.util.Set; 1.16 + 1.17 +import org.json.simple.JSONArray; 1.18 +import org.json.simple.parser.ParseException; 1.19 +import org.mozilla.apache.commons.codec.binary.Base64; 1.20 +import org.mozilla.gecko.sync.crypto.CryptoException; 1.21 +import org.mozilla.gecko.sync.crypto.KeyBundle; 1.22 + 1.23 +public class CollectionKeys { 1.24 + private KeyBundle defaultKeyBundle = null; 1.25 + private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>(); 1.26 + 1.27 + /** 1.28 + * Randomly generate a basic CollectionKeys object. 1.29 + * @throws CryptoException 1.30 + */ 1.31 + public static CollectionKeys generateCollectionKeys() throws CryptoException { 1.32 + CollectionKeys ck = new CollectionKeys(); 1.33 + ck.clear(); 1.34 + ck.defaultKeyBundle = KeyBundle.withRandomKeys(); 1.35 + // TODO: eventually we would like to keep per-collection keys, just generate 1.36 + // new ones as appropriate. 1.37 + return ck; 1.38 + } 1.39 + 1.40 + public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException { 1.41 + if (this.defaultKeyBundle == null) { 1.42 + throw new NoCollectionKeysSetException(); 1.43 + } 1.44 + return this.defaultKeyBundle; 1.45 + } 1.46 + 1.47 + public boolean keyBundleForCollectionIsNotDefault(String collection) { 1.48 + return collectionKeyBundles.containsKey(collection); 1.49 + } 1.50 + 1.51 + public KeyBundle keyBundleForCollection(String collection) 1.52 + throws NoCollectionKeysSetException { 1.53 + if (this.defaultKeyBundle == null) { 1.54 + throw new NoCollectionKeysSetException(); 1.55 + } 1.56 + if (keyBundleForCollectionIsNotDefault(collection)) { 1.57 + return collectionKeyBundles.get(collection); 1.58 + } 1.59 + return this.defaultKeyBundle; 1.60 + } 1.61 + 1.62 + /** 1.63 + * Take a pair of values in a JSON array, handing them off to KeyBundle to 1.64 + * produce a usable keypair. 1.65 + */ 1.66 + private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException { 1.67 + String encKeyStr = (String) array.get(0); 1.68 + String hmacKeyStr = (String) array.get(1); 1.69 + return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr); 1.70 + } 1.71 + 1.72 + @SuppressWarnings("unchecked") 1.73 + private static JSONArray keyBundleToArray(KeyBundle bundle) { 1.74 + // Generate JSON. 1.75 + JSONArray keysArray = new JSONArray(); 1.76 + keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey()))); 1.77 + keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey()))); 1.78 + return keysArray; 1.79 + } 1.80 + 1.81 + private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException { 1.82 + ExtendedJSONObject json = new ExtendedJSONObject(); 1.83 + json.put("id", "keys"); 1.84 + json.put("collection", "crypto"); 1.85 + json.put("default", keyBundleToArray(this.defaultKeyBundle())); 1.86 + ExtendedJSONObject colls = new ExtendedJSONObject(); 1.87 + for (Entry<String, KeyBundle> collKey : collectionKeyBundles.entrySet()) { 1.88 + colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue())); 1.89 + } 1.90 + json.put("collections", colls); 1.91 + return json; 1.92 + } 1.93 + 1.94 + public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException { 1.95 + ExtendedJSONObject payload = this.asRecordContents(); 1.96 + CryptoRecord record = new CryptoRecord(payload); 1.97 + record.collection = "crypto"; 1.98 + record.guid = "keys"; 1.99 + record.deleted = false; 1.100 + return record; 1.101 + } 1.102 + 1.103 + /** 1.104 + * Set my key bundle and collection keys with the given key bundle and data 1.105 + * (possibly decrypted) from the given record. 1.106 + * 1.107 + * @param keys 1.108 + * A "crypto/keys" <code>CryptoRecord</code>, encrypted with 1.109 + * <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null. 1.110 + * @param syncKeyBundle 1.111 + * If non-null, the sync key bundle to decrypt <code>keys</code> with. 1.112 + */ 1.113 + public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle) 1.114 + throws CryptoException, IOException, ParseException, NonObjectJSONException { 1.115 + if (keys == null) { 1.116 + throw new IllegalArgumentException("cannot set key pairs from null record"); 1.117 + } 1.118 + if (syncKeyBundle != null) { 1.119 + keys.keyBundle = syncKeyBundle; 1.120 + keys.decrypt(); 1.121 + } 1.122 + ExtendedJSONObject cleartext = keys.payload; 1.123 + KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default")); 1.124 + 1.125 + ExtendedJSONObject collections = cleartext.getObject("collections"); 1.126 + HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>(); 1.127 + for (Entry<String, Object> pair : collections.entrySet()) { 1.128 + KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue()); 1.129 + collectionKeys.put(pair.getKey(), bundle); 1.130 + } 1.131 + 1.132 + this.collectionKeyBundles.clear(); 1.133 + this.collectionKeyBundles.putAll(collectionKeys); 1.134 + this.defaultKeyBundle = defaultKey; 1.135 + } 1.136 + 1.137 + public void setKeyBundleForCollection(String collection, KeyBundle keys) { 1.138 + this.collectionKeyBundles.put(collection, keys); 1.139 + } 1.140 + 1.141 + public void setDefaultKeyBundle(KeyBundle keys) { 1.142 + this.defaultKeyBundle = keys; 1.143 + } 1.144 + 1.145 + public void clear() { 1.146 + this.defaultKeyBundle = null; 1.147 + this.collectionKeyBundles.clear(); 1.148 + } 1.149 + 1.150 + /** 1.151 + * Return set of collections where key is either missing from one collection 1.152 + * or not the same in both collections. 1.153 + * <p> 1.154 + * Does not check for different default keys. 1.155 + */ 1.156 + public static Set<String> differences(CollectionKeys a, CollectionKeys b) { 1.157 + Set<String> differences = new HashSet<String>(); 1.158 + Set<String> collections = new HashSet<String>(a.collectionKeyBundles.keySet()); 1.159 + collections.addAll(b.collectionKeyBundles.keySet()); 1.160 + 1.161 + // Iterate through one collection, collecting missing and differences. 1.162 + for (String collection : collections) { 1.163 + KeyBundle keyA; 1.164 + KeyBundle keyB; 1.165 + try { 1.166 + keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate. 1.167 + keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate. 1.168 + } catch (NoCollectionKeysSetException e) { 1.169 + differences.add(collection); 1.170 + continue; 1.171 + } 1.172 + // keyA and keyB are not null at this point. 1.173 + if (!keyA.equals(keyB)) { 1.174 + differences.add(collection); 1.175 + } 1.176 + } 1.177 + 1.178 + return differences; 1.179 + } 1.180 + 1.181 + @Override 1.182 + public boolean equals(Object o) { 1.183 + if (!(o instanceof CollectionKeys)) { 1.184 + return false; 1.185 + } 1.186 + CollectionKeys other = (CollectionKeys) o; 1.187 + try { 1.188 + // It would be nice to use map equality here, but there can be map entries 1.189 + // where the key is the default key that should compare equal to a missing 1.190 + // map entry. Therefore, we always compute the set of differences. 1.191 + return defaultKeyBundle().equals(other.defaultKeyBundle()) && 1.192 + CollectionKeys.differences(this, other).isEmpty(); 1.193 + } catch (NoCollectionKeysSetException e) { 1.194 + // If either default key bundle is not set, we'll say the bundles are not equal. 1.195 + return false; 1.196 + } 1.197 + } 1.198 +}