|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync; |
|
6 |
|
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; |
|
13 |
|
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; |
|
19 |
|
20 public class CollectionKeys { |
|
21 private KeyBundle defaultKeyBundle = null; |
|
22 private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>(); |
|
23 |
|
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 } |
|
36 |
|
37 public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException { |
|
38 if (this.defaultKeyBundle == null) { |
|
39 throw new NoCollectionKeysSetException(); |
|
40 } |
|
41 return this.defaultKeyBundle; |
|
42 } |
|
43 |
|
44 public boolean keyBundleForCollectionIsNotDefault(String collection) { |
|
45 return collectionKeyBundles.containsKey(collection); |
|
46 } |
|
47 |
|
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 } |
|
58 |
|
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 } |
|
68 |
|
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 } |
|
77 |
|
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 } |
|
90 |
|
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 } |
|
99 |
|
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")); |
|
121 |
|
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 } |
|
128 |
|
129 this.collectionKeyBundles.clear(); |
|
130 this.collectionKeyBundles.putAll(collectionKeys); |
|
131 this.defaultKeyBundle = defaultKey; |
|
132 } |
|
133 |
|
134 public void setKeyBundleForCollection(String collection, KeyBundle keys) { |
|
135 this.collectionKeyBundles.put(collection, keys); |
|
136 } |
|
137 |
|
138 public void setDefaultKeyBundle(KeyBundle keys) { |
|
139 this.defaultKeyBundle = keys; |
|
140 } |
|
141 |
|
142 public void clear() { |
|
143 this.defaultKeyBundle = null; |
|
144 this.collectionKeyBundles.clear(); |
|
145 } |
|
146 |
|
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()); |
|
157 |
|
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 } |
|
174 |
|
175 return differences; |
|
176 } |
|
177 |
|
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 } |