|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 * http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 Cu.import("resource://services-sync/constants.js"); |
|
5 Cu.import("resource://services-sync/identity.js"); |
|
6 Cu.import("resource://services-sync/keys.js"); |
|
7 Cu.import("resource://services-sync/record.js"); |
|
8 Cu.import("resource://services-sync/util.js"); |
|
9 |
|
10 let collectionKeys = new CollectionKeyManager(); |
|
11 |
|
12 function sha256HMAC(message, key) { |
|
13 let h = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, key); |
|
14 return Utils.digestBytes(message, h); |
|
15 } |
|
16 |
|
17 function do_check_array_eq(a1, a2) { |
|
18 do_check_eq(a1.length, a2.length); |
|
19 for (let i = 0; i < a1.length; ++i) { |
|
20 do_check_eq(a1[i], a2[i]); |
|
21 } |
|
22 } |
|
23 |
|
24 function do_check_keypair_eq(a, b) { |
|
25 do_check_eq(2, a.length); |
|
26 do_check_eq(2, b.length); |
|
27 do_check_eq(a[0], b[0]); |
|
28 do_check_eq(a[1], b[1]); |
|
29 } |
|
30 |
|
31 function test_time_keyFromString(iterations) { |
|
32 let k; |
|
33 let o; |
|
34 let b = new BulkKeyBundle("dummy"); |
|
35 let d = Utils.decodeKeyBase32("ababcdefabcdefabcdefabcdef"); |
|
36 b.generateRandom(); |
|
37 |
|
38 _("Running " + iterations + " iterations of hmacKeyObject + sha256HMAC."); |
|
39 for (let i = 0; i < iterations; ++i) { |
|
40 let k = b.hmacKeyObject; |
|
41 o = sha256HMAC(d, k); |
|
42 } |
|
43 do_check_true(!!o); |
|
44 _("Done."); |
|
45 } |
|
46 |
|
47 add_test(function test_set_invalid_values() { |
|
48 _("Ensure that setting invalid encryption and HMAC key values is caught."); |
|
49 |
|
50 let bundle = new BulkKeyBundle("foo"); |
|
51 |
|
52 let thrown = false; |
|
53 try { |
|
54 bundle.encryptionKey = null; |
|
55 } catch (ex) { |
|
56 thrown = true; |
|
57 do_check_eq(ex.message.indexOf("Encryption key can only be set to"), 0); |
|
58 } finally { |
|
59 do_check_true(thrown); |
|
60 thrown = false; |
|
61 } |
|
62 |
|
63 try { |
|
64 bundle.encryptionKey = ["trollololol"]; |
|
65 } catch (ex) { |
|
66 thrown = true; |
|
67 do_check_eq(ex.message.indexOf("Encryption key can only be set to"), 0); |
|
68 } finally { |
|
69 do_check_true(thrown); |
|
70 thrown = false; |
|
71 } |
|
72 |
|
73 try { |
|
74 bundle.hmacKey = Utils.generateRandomBytes(15); |
|
75 } catch (ex) { |
|
76 thrown = true; |
|
77 do_check_eq(ex.message.indexOf("HMAC key must be at least 128"), 0); |
|
78 } finally { |
|
79 do_check_true(thrown); |
|
80 thrown = false; |
|
81 } |
|
82 |
|
83 try { |
|
84 bundle.hmacKey = null; |
|
85 } catch (ex) { |
|
86 thrown = true; |
|
87 do_check_eq(ex.message.indexOf("HMAC key can only be set to string"), 0); |
|
88 } finally { |
|
89 do_check_true(thrown); |
|
90 thrown = false; |
|
91 } |
|
92 |
|
93 try { |
|
94 bundle.hmacKey = ["trollolol"]; |
|
95 } catch (ex) { |
|
96 thrown = true; |
|
97 do_check_eq(ex.message.indexOf("HMAC key can only be set to"), 0); |
|
98 } finally { |
|
99 do_check_true(thrown); |
|
100 thrown = false; |
|
101 } |
|
102 |
|
103 try { |
|
104 bundle.hmacKey = Utils.generateRandomBytes(15); |
|
105 } catch (ex) { |
|
106 thrown = true; |
|
107 do_check_eq(ex.message.indexOf("HMAC key must be at least 128"), 0); |
|
108 } finally { |
|
109 do_check_true(thrown); |
|
110 thrown = false; |
|
111 } |
|
112 |
|
113 run_next_test(); |
|
114 }); |
|
115 |
|
116 add_test(function test_repeated_hmac() { |
|
117 let testKey = "ababcdefabcdefabcdefabcdef"; |
|
118 let k = Utils.makeHMACKey("foo"); |
|
119 let one = sha256HMAC(Utils.decodeKeyBase32(testKey), k); |
|
120 let two = sha256HMAC(Utils.decodeKeyBase32(testKey), k); |
|
121 do_check_eq(one, two); |
|
122 |
|
123 run_next_test(); |
|
124 }); |
|
125 |
|
126 add_test(function test_sync_key_bundle_derivation() { |
|
127 _("Ensure derivation from known values works."); |
|
128 |
|
129 // The known values in this test were originally verified against Firefox |
|
130 // Home. |
|
131 let bundle = new SyncKeyBundle("st3fan", "q7ynpwq7vsc9m34hankbyi3s3i"); |
|
132 |
|
133 // These should be compared to the results from Home, as they once were. |
|
134 let e = "14b8c09fa84e92729ee695160af6e0385f8f6215a25d14906e1747bdaa2de426"; |
|
135 let h = "370e3566245d79fe602a3adb5137e42439cd2a571235197e0469d7d541b07875"; |
|
136 |
|
137 let realE = Utils.bytesAsHex(bundle.encryptionKey); |
|
138 let realH = Utils.bytesAsHex(bundle.hmacKey); |
|
139 |
|
140 _("Real E: " + realE); |
|
141 _("Real H: " + realH); |
|
142 do_check_eq(realH, h); |
|
143 do_check_eq(realE, e); |
|
144 |
|
145 run_next_test(); |
|
146 }); |
|
147 |
|
148 add_test(function test_keymanager() { |
|
149 let testKey = "ababcdefabcdefabcdefabcdef"; |
|
150 let username = "john@example.com"; |
|
151 |
|
152 // Decode the key here to mirror what generateEntry will do, |
|
153 // but pass it encoded into the KeyBundle call below. |
|
154 |
|
155 let sha256inputE = "" + HMAC_INPUT + username + "\x01"; |
|
156 let key = Utils.makeHMACKey(Utils.decodeKeyBase32(testKey)); |
|
157 let encryptKey = sha256HMAC(sha256inputE, key); |
|
158 |
|
159 let sha256inputH = encryptKey + HMAC_INPUT + username + "\x02"; |
|
160 let hmacKey = sha256HMAC(sha256inputH, key); |
|
161 |
|
162 // Encryption key is stored in base64 for WeaveCrypto convenience. |
|
163 do_check_eq(encryptKey, new SyncKeyBundle(username, testKey).encryptionKey); |
|
164 do_check_eq(hmacKey, new SyncKeyBundle(username, testKey).hmacKey); |
|
165 |
|
166 // Test with the same KeyBundle for both. |
|
167 let obj = new SyncKeyBundle(username, testKey); |
|
168 do_check_eq(hmacKey, obj.hmacKey); |
|
169 do_check_eq(encryptKey, obj.encryptionKey); |
|
170 |
|
171 run_next_test(); |
|
172 }); |
|
173 |
|
174 add_test(function test_collections_manager() { |
|
175 let log = Log.repository.getLogger("Test"); |
|
176 Log.repository.rootLogger.addAppender(new Log.DumpAppender()); |
|
177 |
|
178 let identity = new IdentityManager(); |
|
179 |
|
180 identity.account = "john@example.com"; |
|
181 identity.syncKey = "a-bbbbb-ccccc-ddddd-eeeee-fffff"; |
|
182 |
|
183 let keyBundle = identity.syncKeyBundle; |
|
184 |
|
185 /* |
|
186 * Build a test version of storage/crypto/keys. |
|
187 * Encrypt it with the sync key. |
|
188 * Pass it into the CollectionKeyManager. |
|
189 */ |
|
190 |
|
191 log.info("Building storage keys..."); |
|
192 let storage_keys = new CryptoWrapper("crypto", "keys"); |
|
193 let default_key64 = Svc.Crypto.generateRandomKey(); |
|
194 let default_hmac64 = Svc.Crypto.generateRandomKey(); |
|
195 let bookmarks_key64 = Svc.Crypto.generateRandomKey(); |
|
196 let bookmarks_hmac64 = Svc.Crypto.generateRandomKey(); |
|
197 |
|
198 storage_keys.cleartext = { |
|
199 "default": [default_key64, default_hmac64], |
|
200 "collections": {"bookmarks": [bookmarks_key64, bookmarks_hmac64]}, |
|
201 }; |
|
202 storage_keys.modified = Date.now()/1000; |
|
203 storage_keys.id = "keys"; |
|
204 |
|
205 log.info("Encrypting storage keys..."); |
|
206 |
|
207 // Use passphrase (sync key) itself to encrypt the key bundle. |
|
208 storage_keys.encrypt(keyBundle); |
|
209 |
|
210 // Sanity checking. |
|
211 do_check_true(null == storage_keys.cleartext); |
|
212 do_check_true(null != storage_keys.ciphertext); |
|
213 |
|
214 log.info("Updating collection keys."); |
|
215 |
|
216 // updateContents decrypts the object, releasing the payload for us to use. |
|
217 // Returns true, because the default key has changed. |
|
218 do_check_true(collectionKeys.updateContents(keyBundle, storage_keys)); |
|
219 let payload = storage_keys.cleartext; |
|
220 |
|
221 _("CK: " + JSON.stringify(collectionKeys._collections)); |
|
222 |
|
223 // Test that the CollectionKeyManager returns a similar WBO. |
|
224 let wbo = collectionKeys.asWBO("crypto", "keys"); |
|
225 |
|
226 _("WBO: " + JSON.stringify(wbo)); |
|
227 _("WBO cleartext: " + JSON.stringify(wbo.cleartext)); |
|
228 |
|
229 // Check the individual contents. |
|
230 do_check_eq(wbo.collection, "crypto"); |
|
231 do_check_eq(wbo.id, "keys"); |
|
232 do_check_eq(undefined, wbo.modified); |
|
233 do_check_eq(collectionKeys.lastModified, storage_keys.modified); |
|
234 do_check_true(!!wbo.cleartext.default); |
|
235 do_check_keypair_eq(payload.default, wbo.cleartext.default); |
|
236 do_check_keypair_eq(payload.collections.bookmarks, wbo.cleartext.collections.bookmarks); |
|
237 |
|
238 do_check_true('bookmarks' in collectionKeys._collections); |
|
239 do_check_false('tabs' in collectionKeys._collections); |
|
240 |
|
241 _("Updating contents twice with the same data doesn't proceed."); |
|
242 storage_keys.encrypt(keyBundle); |
|
243 do_check_false(collectionKeys.updateContents(keyBundle, storage_keys)); |
|
244 |
|
245 /* |
|
246 * Test that we get the right keys out when we ask for |
|
247 * a collection's tokens. |
|
248 */ |
|
249 let b1 = new BulkKeyBundle("bookmarks"); |
|
250 b1.keyPairB64 = [bookmarks_key64, bookmarks_hmac64]; |
|
251 let b2 = collectionKeys.keyForCollection("bookmarks"); |
|
252 do_check_keypair_eq(b1.keyPair, b2.keyPair); |
|
253 |
|
254 // Check key equality. |
|
255 do_check_true(b1.equals(b2)); |
|
256 do_check_true(b2.equals(b1)); |
|
257 |
|
258 b1 = new BulkKeyBundle("[default]"); |
|
259 b1.keyPairB64 = [default_key64, default_hmac64]; |
|
260 |
|
261 do_check_false(b1.equals(b2)); |
|
262 do_check_false(b2.equals(b1)); |
|
263 |
|
264 b2 = collectionKeys.keyForCollection(null); |
|
265 do_check_keypair_eq(b1.keyPair, b2.keyPair); |
|
266 |
|
267 /* |
|
268 * Checking for update times. |
|
269 */ |
|
270 let info_collections = {}; |
|
271 do_check_true(collectionKeys.updateNeeded(info_collections)); |
|
272 info_collections["crypto"] = 5000; |
|
273 do_check_false(collectionKeys.updateNeeded(info_collections)); |
|
274 info_collections["crypto"] = 1 + (Date.now()/1000); // Add one in case computers are fast! |
|
275 do_check_true(collectionKeys.updateNeeded(info_collections)); |
|
276 |
|
277 collectionKeys.lastModified = null; |
|
278 do_check_true(collectionKeys.updateNeeded({})); |
|
279 |
|
280 /* |
|
281 * Check _compareKeyBundleCollections. |
|
282 */ |
|
283 function newBundle(name) { |
|
284 let r = new BulkKeyBundle(name); |
|
285 r.generateRandom(); |
|
286 return r; |
|
287 } |
|
288 let k1 = newBundle("k1"); |
|
289 let k2 = newBundle("k2"); |
|
290 let k3 = newBundle("k3"); |
|
291 let k4 = newBundle("k4"); |
|
292 let k5 = newBundle("k5"); |
|
293 let coll1 = {"foo": k1, "bar": k2}; |
|
294 let coll2 = {"foo": k1, "bar": k2}; |
|
295 let coll3 = {"foo": k1, "bar": k3}; |
|
296 let coll4 = {"foo": k4}; |
|
297 let coll5 = {"baz": k5, "bar": k2}; |
|
298 let coll6 = {}; |
|
299 |
|
300 let d1 = collectionKeys._compareKeyBundleCollections(coll1, coll2); // [] |
|
301 let d2 = collectionKeys._compareKeyBundleCollections(coll1, coll3); // ["bar"] |
|
302 let d3 = collectionKeys._compareKeyBundleCollections(coll3, coll2); // ["bar"] |
|
303 let d4 = collectionKeys._compareKeyBundleCollections(coll1, coll4); // ["bar", "foo"] |
|
304 let d5 = collectionKeys._compareKeyBundleCollections(coll5, coll2); // ["baz", "foo"] |
|
305 let d6 = collectionKeys._compareKeyBundleCollections(coll6, coll1); // ["bar", "foo"] |
|
306 let d7 = collectionKeys._compareKeyBundleCollections(coll5, coll5); // [] |
|
307 let d8 = collectionKeys._compareKeyBundleCollections(coll6, coll6); // [] |
|
308 |
|
309 do_check_true(d1.same); |
|
310 do_check_false(d2.same); |
|
311 do_check_false(d3.same); |
|
312 do_check_false(d4.same); |
|
313 do_check_false(d5.same); |
|
314 do_check_false(d6.same); |
|
315 do_check_true(d7.same); |
|
316 do_check_true(d8.same); |
|
317 |
|
318 do_check_array_eq(d1.changed, []); |
|
319 do_check_array_eq(d2.changed, ["bar"]); |
|
320 do_check_array_eq(d3.changed, ["bar"]); |
|
321 do_check_array_eq(d4.changed, ["bar", "foo"]); |
|
322 do_check_array_eq(d5.changed, ["baz", "foo"]); |
|
323 do_check_array_eq(d6.changed, ["bar", "foo"]); |
|
324 |
|
325 run_next_test(); |
|
326 }); |
|
327 |
|
328 function run_test() { |
|
329 // Only do 1,000 to avoid a 5-second pause in test runs. |
|
330 test_time_keyFromString(1000); |
|
331 |
|
332 run_next_test(); |
|
333 } |