|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
|
5 Cu.import("resource://gre/modules/Log.jsm"); |
|
6 Cu.import("resource://services-sync/constants.js"); |
|
7 Cu.import("resource://services-sync/engines.js"); |
|
8 Cu.import("resource://services-sync/engines/tabs.js"); |
|
9 Cu.import("resource://services-sync/engines/history.js"); |
|
10 Cu.import("resource://services-sync/record.js"); |
|
11 Cu.import("resource://services-sync/service.js"); |
|
12 Cu.import("resource://services-sync/status.js"); |
|
13 Cu.import("resource://services-sync/util.js"); |
|
14 Cu.import("resource://testing-common/services/sync/utils.js"); |
|
15 Cu.import("resource://gre/modules/Promise.jsm"); |
|
16 |
|
17 add_task(function test_locally_changed_keys() { |
|
18 let passphrase = "abcdeabcdeabcdeabcdeabcdea"; |
|
19 |
|
20 let hmacErrorCount = 0; |
|
21 function counting(f) { |
|
22 return function() { |
|
23 hmacErrorCount++; |
|
24 return f.call(this); |
|
25 }; |
|
26 } |
|
27 |
|
28 Service.handleHMACEvent = counting(Service.handleHMACEvent); |
|
29 |
|
30 let server = new SyncServer(); |
|
31 let johndoe = server.registerUser("johndoe", "password"); |
|
32 johndoe.createContents({ |
|
33 meta: {}, |
|
34 crypto: {}, |
|
35 clients: {} |
|
36 }); |
|
37 server.start(); |
|
38 |
|
39 try { |
|
40 Svc.Prefs.set("registerEngines", "Tab"); |
|
41 _("Set up some tabs."); |
|
42 let myTabs = |
|
43 {windows: [{tabs: [{index: 1, |
|
44 entries: [{ |
|
45 url: "http://foo.com/", |
|
46 title: "Title" |
|
47 }], |
|
48 attributes: { |
|
49 image: "image" |
|
50 } |
|
51 }]}]}; |
|
52 delete Svc.Session; |
|
53 Svc.Session = { |
|
54 getBrowserState: function () JSON.stringify(myTabs) |
|
55 }; |
|
56 |
|
57 setBasicCredentials("johndoe", "password", passphrase); |
|
58 Service.serverURL = server.baseURI; |
|
59 Service.clusterURL = server.baseURI; |
|
60 |
|
61 Service.engineManager.register(HistoryEngine); |
|
62 |
|
63 function corrupt_local_keys() { |
|
64 Service.collectionKeys._default.keyPair = [Svc.Crypto.generateRandomKey(), |
|
65 Svc.Crypto.generateRandomKey()]; |
|
66 } |
|
67 |
|
68 _("Setting meta."); |
|
69 |
|
70 // Bump version on the server. |
|
71 let m = new WBORecord("meta", "global"); |
|
72 m.payload = {"syncID": "foooooooooooooooooooooooooo", |
|
73 "storageVersion": STORAGE_VERSION}; |
|
74 m.upload(Service.resource(Service.metaURL)); |
|
75 |
|
76 _("New meta/global: " + JSON.stringify(johndoe.collection("meta").wbo("global"))); |
|
77 |
|
78 // Upload keys. |
|
79 generateNewKeys(Service.collectionKeys); |
|
80 let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); |
|
81 serverKeys.encrypt(Service.identity.syncKeyBundle); |
|
82 do_check_true(serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success); |
|
83 |
|
84 // Check that login works. |
|
85 do_check_true(Service.login("johndoe", "ilovejane", passphrase)); |
|
86 do_check_true(Service.isLoggedIn); |
|
87 |
|
88 // Sync should upload records. |
|
89 Service.sync(); |
|
90 |
|
91 // Tabs exist. |
|
92 _("Tabs modified: " + johndoe.modified("tabs")); |
|
93 do_check_true(johndoe.modified("tabs") > 0); |
|
94 |
|
95 let coll_modified = Service.collectionKeys.lastModified; |
|
96 |
|
97 // Let's create some server side history records. |
|
98 let liveKeys = Service.collectionKeys.keyForCollection("history"); |
|
99 _("Keys now: " + liveKeys.keyPair); |
|
100 let visitType = Ci.nsINavHistoryService.TRANSITION_LINK; |
|
101 let history = johndoe.createCollection("history"); |
|
102 for (let i = 0; i < 5; i++) { |
|
103 let id = 'record-no--' + i; |
|
104 let modified = Date.now()/1000 - 60*(i+10); |
|
105 |
|
106 let w = new CryptoWrapper("history", "id"); |
|
107 w.cleartext = { |
|
108 id: id, |
|
109 histUri: "http://foo/bar?" + id, |
|
110 title: id, |
|
111 sortindex: i, |
|
112 visits: [{date: (modified - 5) * 1000000, type: visitType}], |
|
113 deleted: false}; |
|
114 w.encrypt(liveKeys); |
|
115 |
|
116 let payload = {ciphertext: w.ciphertext, |
|
117 IV: w.IV, |
|
118 hmac: w.hmac}; |
|
119 history.insert(id, payload, modified); |
|
120 } |
|
121 |
|
122 history.timestamp = Date.now() / 1000; |
|
123 let old_key_time = johndoe.modified("crypto"); |
|
124 _("Old key time: " + old_key_time); |
|
125 |
|
126 // Check that we can decrypt one. |
|
127 let rec = new CryptoWrapper("history", "record-no--0"); |
|
128 rec.fetch(Service.resource(Service.storageURL + "history/record-no--0")); |
|
129 _(JSON.stringify(rec)); |
|
130 do_check_true(!!rec.decrypt(liveKeys)); |
|
131 |
|
132 do_check_eq(hmacErrorCount, 0); |
|
133 |
|
134 // Fill local key cache with bad data. |
|
135 corrupt_local_keys(); |
|
136 _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair); |
|
137 |
|
138 do_check_eq(hmacErrorCount, 0); |
|
139 |
|
140 _("HMAC error count: " + hmacErrorCount); |
|
141 // Now syncing should succeed, after one HMAC error. |
|
142 Service.sync(); |
|
143 do_check_eq(hmacErrorCount, 1); |
|
144 _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair); |
|
145 |
|
146 // And look! We downloaded history! |
|
147 let store = Service.engineManager.get("history")._store; |
|
148 do_check_true(yield promiseIsURIVisited("http://foo/bar?record-no--0")); |
|
149 do_check_true(yield promiseIsURIVisited("http://foo/bar?record-no--1")); |
|
150 do_check_true(yield promiseIsURIVisited("http://foo/bar?record-no--2")); |
|
151 do_check_true(yield promiseIsURIVisited("http://foo/bar?record-no--3")); |
|
152 do_check_true(yield promiseIsURIVisited("http://foo/bar?record-no--4")); |
|
153 do_check_eq(hmacErrorCount, 1); |
|
154 |
|
155 _("Busting some new server values."); |
|
156 // Now what happens if we corrupt the HMAC on the server? |
|
157 for (let i = 5; i < 10; i++) { |
|
158 let id = 'record-no--' + i; |
|
159 let modified = 1 + (Date.now() / 1000); |
|
160 |
|
161 let w = new CryptoWrapper("history", "id"); |
|
162 w.cleartext = { |
|
163 id: id, |
|
164 histUri: "http://foo/bar?" + id, |
|
165 title: id, |
|
166 sortindex: i, |
|
167 visits: [{date: (modified - 5 ) * 1000000, type: visitType}], |
|
168 deleted: false}; |
|
169 w.encrypt(Service.collectionKeys.keyForCollection("history")); |
|
170 w.hmac = w.hmac.toUpperCase(); |
|
171 |
|
172 let payload = {ciphertext: w.ciphertext, |
|
173 IV: w.IV, |
|
174 hmac: w.hmac}; |
|
175 history.insert(id, payload, modified); |
|
176 } |
|
177 history.timestamp = Date.now() / 1000; |
|
178 |
|
179 _("Server key time hasn't changed."); |
|
180 do_check_eq(johndoe.modified("crypto"), old_key_time); |
|
181 |
|
182 _("Resetting HMAC error timer."); |
|
183 Service.lastHMACEvent = 0; |
|
184 |
|
185 _("Syncing..."); |
|
186 Service.sync(); |
|
187 _("Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair); |
|
188 _("Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history."); |
|
189 do_check_true(johndoe.modified("crypto") > old_key_time); |
|
190 do_check_eq(hmacErrorCount, 6); |
|
191 do_check_false(yield promiseIsURIVisited("http://foo/bar?record-no--5")); |
|
192 do_check_false(yield promiseIsURIVisited("http://foo/bar?record-no--6")); |
|
193 do_check_false(yield promiseIsURIVisited("http://foo/bar?record-no--7")); |
|
194 do_check_false(yield promiseIsURIVisited("http://foo/bar?record-no--8")); |
|
195 do_check_false(yield promiseIsURIVisited("http://foo/bar?record-no--9")); |
|
196 } finally { |
|
197 Svc.Prefs.resetBranch(""); |
|
198 let deferred = Promise.defer(); |
|
199 server.stop(deferred.resolve); |
|
200 yield deferred.promise; |
|
201 } |
|
202 }); |
|
203 |
|
204 function run_test() { |
|
205 let logger = Log.repository.rootLogger; |
|
206 Log.repository.rootLogger.addAppender(new Log.DumpAppender()); |
|
207 |
|
208 ensureLegacyIdentityManager(); |
|
209 |
|
210 run_next_test(); |
|
211 } |
|
212 |
|
213 /** |
|
214 * Asynchronously check a url is visited. |
|
215 * @param url the url |
|
216 * @return {Promise} |
|
217 * @resolves When the check has been added successfully. |
|
218 * @rejects JavaScript exception. |
|
219 */ |
|
220 function promiseIsURIVisited(url) { |
|
221 let deferred = Promise.defer(); |
|
222 PlacesUtils.asyncHistory.isURIVisited(Utils.makeURI(url), function(aURI, aIsVisited) { |
|
223 deferred.resolve(aIsVisited); |
|
224 }); |
|
225 |
|
226 return deferred.promise; |
|
227 } |