test_corrupt_keys.js (7431B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 const { Weave } = ChromeUtils.importESModule( 5 "resource://services-sync/main.sys.mjs" 6 ); 7 const { HistoryEngine } = ChromeUtils.importESModule( 8 "resource://services-sync/engines/history.sys.mjs" 9 ); 10 const { CryptoWrapper, WBORecord } = ChromeUtils.importESModule( 11 "resource://services-sync/record.sys.mjs" 12 ); 13 const { Service } = ChromeUtils.importESModule( 14 "resource://services-sync/service.sys.mjs" 15 ); 16 17 add_task(async function test_locally_changed_keys() { 18 enableValidationPrefs(); 19 20 let hmacErrorCount = 0; 21 function counting(f) { 22 return async 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.PrefBranch.setStringPref("registerEngines", "Tab"); 41 42 await configureIdentity({ username: "johndoe" }, server); 43 // We aren't doing a .login yet, so fudge the cluster URL. 44 Service.clusterURL = Service.identity._token.endpoint; 45 46 await Service.engineManager.register(HistoryEngine); 47 // Disable addon sync because AddonManager won't be initialized here. 48 await Service.engineManager.unregister("addons"); 49 await Service.engineManager.unregister("extension-storage"); 50 51 async function corrupt_local_keys() { 52 Service.collectionKeys._default.keyPair = [ 53 await Weave.Crypto.generateRandomKey(), 54 await Weave.Crypto.generateRandomKey(), 55 ]; 56 } 57 58 _("Setting meta."); 59 60 // Bump version on the server. 61 let m = new WBORecord("meta", "global"); 62 m.payload = { 63 syncID: "foooooooooooooooooooooooooo", 64 storageVersion: STORAGE_VERSION, 65 }; 66 await m.upload(Service.resource(Service.metaURL)); 67 68 _( 69 "New meta/global: " + 70 JSON.stringify(johndoe.collection("meta").wbo("global")) 71 ); 72 73 // Upload keys. 74 await generateNewKeys(Service.collectionKeys); 75 let serverKeys = Service.collectionKeys.asWBO("crypto", "keys"); 76 await serverKeys.encrypt(Service.identity.syncKeyBundle); 77 Assert.ok( 78 (await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success 79 ); 80 81 // Check that login works. 82 Assert.ok(await Service.login()); 83 Assert.ok(Service.isLoggedIn); 84 85 // Sync should upload records. 86 await sync_and_validate_telem(); 87 88 // Tabs exist. 89 _("Tabs modified: " + johndoe.modified("tabs")); 90 Assert.greater(johndoe.modified("tabs"), 0); 91 92 // Let's create some server side history records. 93 let liveKeys = Service.collectionKeys.keyForCollection("history"); 94 _("Keys now: " + liveKeys.keyPair); 95 let visitType = Ci.nsINavHistoryService.TRANSITION_LINK; 96 let history = johndoe.createCollection("history"); 97 for (let i = 0; i < 5; i++) { 98 let id = "record-no--" + i; 99 let modified = Date.now() / 1000 - 60 * (i + 10); 100 101 let w = new CryptoWrapper("history", "id"); 102 w.cleartext = { 103 id, 104 histUri: "http://foo/bar?" + id, 105 title: id, 106 sortindex: i, 107 visits: [{ date: (modified - 5) * 1000000, type: visitType }], 108 deleted: false, 109 }; 110 await w.encrypt(liveKeys); 111 112 let payload = { ciphertext: w.ciphertext, IV: w.IV, hmac: w.hmac }; 113 history.insert(id, payload, modified); 114 } 115 116 history.timestamp = Date.now() / 1000; 117 let old_key_time = johndoe.modified("crypto"); 118 _("Old key time: " + old_key_time); 119 120 // Check that we can decrypt one. 121 let rec = new CryptoWrapper("history", "record-no--0"); 122 await rec.fetch( 123 Service.resource(Service.storageURL + "history/record-no--0") 124 ); 125 _(JSON.stringify(rec)); 126 Assert.ok(!!(await rec.decrypt(liveKeys))); 127 128 Assert.equal(hmacErrorCount, 0); 129 130 // Fill local key cache with bad data. 131 await corrupt_local_keys(); 132 _( 133 "Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair 134 ); 135 136 Assert.equal(hmacErrorCount, 0); 137 138 _("HMAC error count: " + hmacErrorCount); 139 // Now syncing should succeed, after one HMAC error. 140 await sync_and_validate_telem(ping => { 141 Assert.equal( 142 ping.engines.find(e => e.name == "history").incoming.applied, 143 5 144 ); 145 }); 146 147 Assert.equal(hmacErrorCount, 1); 148 _( 149 "Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair 150 ); 151 152 // And look! We downloaded history! 153 Assert.ok( 154 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--0") 155 ); 156 Assert.ok( 157 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--1") 158 ); 159 Assert.ok( 160 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--2") 161 ); 162 Assert.ok( 163 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--3") 164 ); 165 Assert.ok( 166 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--4") 167 ); 168 Assert.equal(hmacErrorCount, 1); 169 170 _("Busting some new server values."); 171 // Now what happens if we corrupt the HMAC on the server? 172 for (let i = 5; i < 10; i++) { 173 let id = "record-no--" + i; 174 let modified = 1 + Date.now() / 1000; 175 176 let w = new CryptoWrapper("history", "id"); 177 w.cleartext = { 178 id, 179 histUri: "http://foo/bar?" + id, 180 title: id, 181 sortindex: i, 182 visits: [{ date: (modified - 5) * 1000000, type: visitType }], 183 deleted: false, 184 }; 185 await w.encrypt(Service.collectionKeys.keyForCollection("history")); 186 w.hmac = w.hmac.toUpperCase(); 187 188 let payload = { ciphertext: w.ciphertext, IV: w.IV, hmac: w.hmac }; 189 history.insert(id, payload, modified); 190 } 191 history.timestamp = Date.now() / 1000; 192 193 _("Server key time hasn't changed."); 194 Assert.equal(johndoe.modified("crypto"), old_key_time); 195 196 _("Resetting HMAC error timer."); 197 Service.lastHMACEvent = 0; 198 199 _("Syncing..."); 200 await sync_and_validate_telem(ping => { 201 Assert.equal( 202 ping.engines.find(e => e.name == "history").incoming.failed, 203 5 204 ); 205 }); 206 207 _( 208 "Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair 209 ); 210 _( 211 "Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history." 212 ); 213 Assert.greater(johndoe.modified("crypto"), old_key_time); 214 Assert.equal(hmacErrorCount, 6); 215 Assert.equal( 216 false, 217 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--5") 218 ); 219 Assert.equal( 220 false, 221 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--6") 222 ); 223 Assert.equal( 224 false, 225 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--7") 226 ); 227 Assert.equal( 228 false, 229 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--8") 230 ); 231 Assert.equal( 232 false, 233 await PlacesUtils.history.hasVisits("http://foo/bar?record-no--9") 234 ); 235 } finally { 236 for (const pref of Svc.PrefBranch.getChildList("")) { 237 Svc.PrefBranch.clearUserPref(pref); 238 } 239 await promiseStopServer(server); 240 } 241 }); 242 243 function run_test() { 244 Log.repository.rootLogger.addAppender(new Log.DumpAppender()); 245 validate_all_future_pings(); 246 247 run_next_test(); 248 }