Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.sync;
7 import java.io.IOException;
8 import java.io.UnsupportedEncodingException;
9 import java.util.HashMap;
10 import java.util.HashSet;
11 import java.util.Map.Entry;
12 import java.util.Set;
14 import org.json.simple.JSONArray;
15 import org.json.simple.parser.ParseException;
16 import org.mozilla.apache.commons.codec.binary.Base64;
17 import org.mozilla.gecko.sync.crypto.CryptoException;
18 import org.mozilla.gecko.sync.crypto.KeyBundle;
20 public class CollectionKeys {
21 private KeyBundle defaultKeyBundle = null;
22 private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>();
24 /**
25 * Randomly generate a basic CollectionKeys object.
26 * @throws CryptoException
27 */
28 public static CollectionKeys generateCollectionKeys() throws CryptoException {
29 CollectionKeys ck = new CollectionKeys();
30 ck.clear();
31 ck.defaultKeyBundle = KeyBundle.withRandomKeys();
32 // TODO: eventually we would like to keep per-collection keys, just generate
33 // new ones as appropriate.
34 return ck;
35 }
37 public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException {
38 if (this.defaultKeyBundle == null) {
39 throw new NoCollectionKeysSetException();
40 }
41 return this.defaultKeyBundle;
42 }
44 public boolean keyBundleForCollectionIsNotDefault(String collection) {
45 return collectionKeyBundles.containsKey(collection);
46 }
48 public KeyBundle keyBundleForCollection(String collection)
49 throws NoCollectionKeysSetException {
50 if (this.defaultKeyBundle == null) {
51 throw new NoCollectionKeysSetException();
52 }
53 if (keyBundleForCollectionIsNotDefault(collection)) {
54 return collectionKeyBundles.get(collection);
55 }
56 return this.defaultKeyBundle;
57 }
59 /**
60 * Take a pair of values in a JSON array, handing them off to KeyBundle to
61 * produce a usable keypair.
62 */
63 private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException {
64 String encKeyStr = (String) array.get(0);
65 String hmacKeyStr = (String) array.get(1);
66 return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr);
67 }
69 @SuppressWarnings("unchecked")
70 private static JSONArray keyBundleToArray(KeyBundle bundle) {
71 // Generate JSON.
72 JSONArray keysArray = new JSONArray();
73 keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey())));
74 keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey())));
75 return keysArray;
76 }
78 private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException {
79 ExtendedJSONObject json = new ExtendedJSONObject();
80 json.put("id", "keys");
81 json.put("collection", "crypto");
82 json.put("default", keyBundleToArray(this.defaultKeyBundle()));
83 ExtendedJSONObject colls = new ExtendedJSONObject();
84 for (Entry<String, KeyBundle> collKey : collectionKeyBundles.entrySet()) {
85 colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue()));
86 }
87 json.put("collections", colls);
88 return json;
89 }
91 public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException {
92 ExtendedJSONObject payload = this.asRecordContents();
93 CryptoRecord record = new CryptoRecord(payload);
94 record.collection = "crypto";
95 record.guid = "keys";
96 record.deleted = false;
97 return record;
98 }
100 /**
101 * Set my key bundle and collection keys with the given key bundle and data
102 * (possibly decrypted) from the given record.
103 *
104 * @param keys
105 * A "crypto/keys" <code>CryptoRecord</code>, encrypted with
106 * <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null.
107 * @param syncKeyBundle
108 * If non-null, the sync key bundle to decrypt <code>keys</code> with.
109 */
110 public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
111 throws CryptoException, IOException, ParseException, NonObjectJSONException {
112 if (keys == null) {
113 throw new IllegalArgumentException("cannot set key pairs from null record");
114 }
115 if (syncKeyBundle != null) {
116 keys.keyBundle = syncKeyBundle;
117 keys.decrypt();
118 }
119 ExtendedJSONObject cleartext = keys.payload;
120 KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default"));
122 ExtendedJSONObject collections = cleartext.getObject("collections");
123 HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>();
124 for (Entry<String, Object> pair : collections.entrySet()) {
125 KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue());
126 collectionKeys.put(pair.getKey(), bundle);
127 }
129 this.collectionKeyBundles.clear();
130 this.collectionKeyBundles.putAll(collectionKeys);
131 this.defaultKeyBundle = defaultKey;
132 }
134 public void setKeyBundleForCollection(String collection, KeyBundle keys) {
135 this.collectionKeyBundles.put(collection, keys);
136 }
138 public void setDefaultKeyBundle(KeyBundle keys) {
139 this.defaultKeyBundle = keys;
140 }
142 public void clear() {
143 this.defaultKeyBundle = null;
144 this.collectionKeyBundles.clear();
145 }
147 /**
148 * Return set of collections where key is either missing from one collection
149 * or not the same in both collections.
150 * <p>
151 * Does not check for different default keys.
152 */
153 public static Set<String> differences(CollectionKeys a, CollectionKeys b) {
154 Set<String> differences = new HashSet<String>();
155 Set<String> collections = new HashSet<String>(a.collectionKeyBundles.keySet());
156 collections.addAll(b.collectionKeyBundles.keySet());
158 // Iterate through one collection, collecting missing and differences.
159 for (String collection : collections) {
160 KeyBundle keyA;
161 KeyBundle keyB;
162 try {
163 keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate.
164 keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate.
165 } catch (NoCollectionKeysSetException e) {
166 differences.add(collection);
167 continue;
168 }
169 // keyA and keyB are not null at this point.
170 if (!keyA.equals(keyB)) {
171 differences.add(collection);
172 }
173 }
175 return differences;
176 }
178 @Override
179 public boolean equals(Object o) {
180 if (!(o instanceof CollectionKeys)) {
181 return false;
182 }
183 CollectionKeys other = (CollectionKeys) o;
184 try {
185 // It would be nice to use map equality here, but there can be map entries
186 // where the key is the default key that should compare equal to a missing
187 // map entry. Therefore, we always compute the set of differences.
188 return defaultKeyBundle().equals(other.defaultKeyBundle()) &&
189 CollectionKeys.differences(this, other).isEmpty();
190 } catch (NoCollectionKeysSetException e) {
191 // If either default key bundle is not set, we'll say the bundles are not equal.
192 return false;
193 }
194 }
195 }