test_storage_manager.js (17588B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Tests for the FxA storage manager. 7 8 const { FxAccountsStorageManager } = ChromeUtils.importESModule( 9 "resource://gre/modules/FxAccountsStorage.sys.mjs" 10 ); 11 const { DATA_FORMAT_VERSION, log } = ChromeUtils.importESModule( 12 "resource://gre/modules/FxAccountsCommon.sys.mjs" 13 ); 14 15 initTestLogging("Trace"); 16 log.level = Log.Level.Trace; 17 18 const DEVICE_REGISTRATION_VERSION = 42; 19 20 // A couple of mocks we can use. 21 function MockedPlainStorage(accountData) { 22 let data = null; 23 if (accountData) { 24 data = { 25 version: DATA_FORMAT_VERSION, 26 accountData, 27 }; 28 } 29 this.data = data; 30 this.numReads = 0; 31 } 32 MockedPlainStorage.prototype = { 33 async get() { 34 this.numReads++; 35 Assert.equal(this.numReads, 1, "should only ever be 1 read of acct data"); 36 return this.data; 37 }, 38 39 async set(data) { 40 this.data = data; 41 }, 42 }; 43 44 function MockedSecureStorage(accountData) { 45 let data = null; 46 if (accountData) { 47 data = { 48 version: DATA_FORMAT_VERSION, 49 accountData, 50 }; 51 } 52 this.data = data; 53 this.numReads = 0; 54 } 55 56 MockedSecureStorage.prototype = { 57 fetchCount: 0, 58 locked: false, 59 /* eslint-disable object-shorthand */ 60 // This constructor must be declared without 61 // object shorthand or we get an exception of 62 // "TypeError: this.STORAGE_LOCKED is not a constructor" 63 STORAGE_LOCKED: function () {}, 64 /* eslint-enable object-shorthand */ 65 async get() { 66 this.fetchCount++; 67 if (this.locked) { 68 throw new this.STORAGE_LOCKED(); 69 } 70 this.numReads++; 71 Assert.equal( 72 this.numReads, 73 1, 74 "should only ever be 1 read of unlocked data" 75 ); 76 return this.data; 77 }, 78 79 async set(uid, contents) { 80 this.data = contents; 81 }, 82 }; 83 84 function add_storage_task(testFunction) { 85 add_task(async function () { 86 print("Starting test with secure storage manager"); 87 await testFunction(new FxAccountsStorageManager()); 88 }); 89 add_task(async function () { 90 print("Starting test with simple storage manager"); 91 await testFunction(new FxAccountsStorageManager({ useSecure: false })); 92 }); 93 } 94 95 // initialized without account data and there's nothing to read. Not logged in. 96 add_storage_task(async function checkInitializedEmpty(sm) { 97 if (sm.secureStorage) { 98 sm.secureStorage = new MockedSecureStorage(null); 99 } 100 await sm.initialize(); 101 Assert.strictEqual(await sm.getAccountData(), null); 102 await Assert.rejects( 103 sm.updateAccountData({ scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys } }), 104 /No user is logged in/ 105 ); 106 }); 107 108 // Initialized with account data (ie, simulating a new user being logged in). 109 // Should reflect the initial data and be written to storage. 110 add_storage_task(async function checkNewUser(sm) { 111 let initialAccountData = { 112 uid: "uid", 113 email: "someone@somewhere.com", 114 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 115 device: { 116 id: "device id", 117 }, 118 }; 119 sm.plainStorage = new MockedPlainStorage(); 120 if (sm.secureStorage) { 121 sm.secureStorage = new MockedSecureStorage(null); 122 } 123 await sm.initialize(initialAccountData); 124 let accountData = await sm.getAccountData(); 125 Assert.equal(accountData.uid, initialAccountData.uid); 126 Assert.equal(accountData.email, initialAccountData.email); 127 Assert.deepEqual(accountData.scopedKeys, initialAccountData.scopedKeys); 128 Assert.deepEqual(accountData.device, initialAccountData.device); 129 130 // and it should have been written to storage. 131 Assert.equal(sm.plainStorage.data.accountData.uid, initialAccountData.uid); 132 Assert.equal( 133 sm.plainStorage.data.accountData.email, 134 initialAccountData.email 135 ); 136 Assert.deepEqual( 137 sm.plainStorage.data.accountData.device, 138 initialAccountData.device 139 ); 140 // check secure 141 if (sm.secureStorage) { 142 Assert.deepEqual( 143 sm.secureStorage.data.accountData.scopedKeys, 144 initialAccountData.scopedKeys 145 ); 146 } else { 147 Assert.deepEqual( 148 sm.plainStorage.data.accountData.scopedKeys, 149 initialAccountData.scopedKeys 150 ); 151 } 152 }); 153 154 // Initialized without account data but storage has it available. 155 add_storage_task(async function checkEverythingRead(sm) { 156 sm.plainStorage = new MockedPlainStorage({ 157 uid: "uid", 158 email: "someone@somewhere.com", 159 device: { 160 id: "wibble", 161 registrationVersion: null, 162 }, 163 }); 164 if (sm.secureStorage) { 165 sm.secureStorage = new MockedSecureStorage(null); 166 } 167 await sm.initialize(); 168 let accountData = await sm.getAccountData(); 169 Assert.ok(accountData, "read account data"); 170 Assert.equal(accountData.uid, "uid"); 171 Assert.equal(accountData.email, "someone@somewhere.com"); 172 Assert.deepEqual(accountData.device, { 173 id: "wibble", 174 registrationVersion: null, 175 }); 176 // Update the data - we should be able to fetch it back and it should appear 177 // in our storage. 178 await sm.updateAccountData({ 179 verified: true, 180 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 181 device: { 182 id: "wibble", 183 registrationVersion: DEVICE_REGISTRATION_VERSION, 184 }, 185 }); 186 accountData = await sm.getAccountData(); 187 Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); 188 Assert.deepEqual(accountData.device, { 189 id: "wibble", 190 registrationVersion: DEVICE_REGISTRATION_VERSION, 191 }); 192 // Check the new value was written to storage. 193 await sm._promiseStorageComplete; // storage is written in the background. 194 Assert.equal(sm.plainStorage.data.accountData.verified, true); 195 Assert.deepEqual(sm.plainStorage.data.accountData.device, { 196 id: "wibble", 197 registrationVersion: DEVICE_REGISTRATION_VERSION, 198 }); 199 // derive keys are secure 200 if (sm.secureStorage) { 201 Assert.deepEqual( 202 sm.secureStorage.data.accountData.scopedKeys, 203 MOCK_ACCOUNT_KEYS.scopedKeys 204 ); 205 } else { 206 Assert.deepEqual( 207 sm.plainStorage.data.accountData.scopedKeys, 208 MOCK_ACCOUNT_KEYS.scopedKeys 209 ); 210 } 211 }); 212 213 add_storage_task(async function checkInvalidUpdates(sm) { 214 sm.plainStorage = new MockedPlainStorage({ 215 uid: "uid", 216 email: "someone@somewhere.com", 217 }); 218 if (sm.secureStorage) { 219 sm.secureStorage = new MockedSecureStorage(null); 220 } 221 await sm.initialize(); 222 223 await Assert.rejects( 224 sm.updateAccountData({ uid: "another" }), 225 /Can't change uid/ 226 ); 227 }); 228 229 add_storage_task(async function checkNullUpdatesRemovedUnlocked(sm) { 230 if (sm.secureStorage) { 231 sm.plainStorage = new MockedPlainStorage({ 232 uid: "uid", 233 email: "someone@somewhere.com", 234 }); 235 sm.secureStorage = new MockedSecureStorage({ 236 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 237 unwrapBKey: "unwrapBKey", 238 }); 239 } else { 240 sm.plainStorage = new MockedPlainStorage({ 241 uid: "uid", 242 email: "someone@somewhere.com", 243 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 244 unwrapBKey: "unwrapBKey", 245 }); 246 } 247 await sm.initialize(); 248 249 await sm.updateAccountData({ unwrapBKey: null }); 250 let accountData = await sm.getAccountData(); 251 Assert.ok(!accountData.unwrapBKey); 252 Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); 253 }); 254 255 add_storage_task(async function checkNullRemovesUnlistedFields(sm) { 256 // kA and kB are not listed in FXA_PWDMGR_*_FIELDS, but we still want to 257 // be able to delete them (migration case). 258 if (sm.secureStorage) { 259 sm.plainStorage = new MockedPlainStorage({ 260 uid: "uid", 261 email: "someone@somewhere.com", 262 }); 263 sm.secureStorage = new MockedSecureStorage({ kA: "kA", kb: "kB" }); 264 } else { 265 sm.plainStorage = new MockedPlainStorage({ 266 uid: "uid", 267 email: "someone@somewhere.com", 268 kA: "kA", 269 kb: "kB", 270 }); 271 } 272 await sm.initialize(); 273 274 await sm.updateAccountData({ kA: null, kB: null }); 275 let accountData = await sm.getAccountData(); 276 Assert.ok(!accountData.kA); 277 Assert.ok(!accountData.kB); 278 }); 279 280 add_storage_task(async function checkDelete(sm) { 281 if (sm.secureStorage) { 282 sm.plainStorage = new MockedPlainStorage({ 283 uid: "uid", 284 email: "someone@somewhere.com", 285 }); 286 sm.secureStorage = new MockedSecureStorage({ 287 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 288 }); 289 } else { 290 sm.plainStorage = new MockedPlainStorage({ 291 uid: "uid", 292 email: "someone@somewhere.com", 293 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 294 }); 295 } 296 await sm.initialize(); 297 298 await sm.deleteAccountData(); 299 // Storage should have been reset to null. 300 Assert.equal(sm.plainStorage.data, null); 301 if (sm.secureStorage) { 302 Assert.equal(sm.secureStorage.data, null); 303 } 304 // And everything should reflect no user. 305 Assert.equal(await sm.getAccountData(), null); 306 }); 307 308 // Some tests only for the secure storage manager. 309 add_task(async function checkNullUpdatesRemovedLocked() { 310 let sm = new FxAccountsStorageManager(); 311 sm.plainStorage = new MockedPlainStorage({ 312 uid: "uid", 313 email: "someone@somewhere.com", 314 }); 315 sm.secureStorage = new MockedSecureStorage({ 316 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 317 unwrapBKey: "unwrapBKey is another secure value", 318 }); 319 sm.secureStorage.locked = true; 320 await sm.initialize(); 321 322 await sm.updateAccountData({ scopedKeys: null }); 323 let accountData = await sm.getAccountData(); 324 // No scopedKeys because it was removed. 325 Assert.ok(!accountData.scopedKeys); 326 // No unwrapBKey because we are locked 327 Assert.ok(!accountData.unwrapBKey); 328 329 // now unlock - should still be no scopedKeys but unwrapBKey should appear. 330 sm.secureStorage.locked = false; 331 accountData = await sm.getAccountData(); 332 Assert.ok(!accountData.scopedKeys); 333 Assert.equal(accountData.unwrapBKey, "unwrapBKey is another secure value"); 334 // And secure storage should have been written with our previously-cached 335 // data. 336 Assert.strictEqual(sm.secureStorage.data.accountData.scopedKeys, undefined); 337 Assert.strictEqual( 338 sm.secureStorage.data.accountData.unwrapBKey, 339 "unwrapBKey is another secure value" 340 ); 341 }); 342 343 add_task(async function checkEverythingReadSecure() { 344 let sm = new FxAccountsStorageManager(); 345 sm.plainStorage = new MockedPlainStorage({ 346 uid: "uid", 347 email: "someone@somewhere.com", 348 }); 349 sm.secureStorage = new MockedSecureStorage({ 350 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 351 }); 352 await sm.initialize(); 353 354 let accountData = await sm.getAccountData(); 355 Assert.ok(accountData, "read account data"); 356 Assert.equal(accountData.uid, "uid"); 357 Assert.equal(accountData.email, "someone@somewhere.com"); 358 Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); 359 }); 360 361 add_task(async function checkExplicitGet() { 362 let sm = new FxAccountsStorageManager(); 363 sm.plainStorage = new MockedPlainStorage({ 364 uid: "uid", 365 email: "someone@somewhere.com", 366 }); 367 sm.secureStorage = new MockedSecureStorage({ 368 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 369 }); 370 await sm.initialize(); 371 372 let accountData = await sm.getAccountData(["uid", "scopedKeys"]); 373 Assert.ok(accountData, "read account data"); 374 Assert.equal(accountData.uid, "uid"); 375 Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); 376 // We didn't ask for email so shouldn't have got it. 377 Assert.strictEqual(accountData.email, undefined); 378 }); 379 380 add_task(async function checkExplicitGetNoSecureRead() { 381 let sm = new FxAccountsStorageManager(); 382 sm.plainStorage = new MockedPlainStorage({ 383 uid: "uid", 384 email: "someone@somewhere.com", 385 }); 386 sm.secureStorage = new MockedSecureStorage({ 387 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 388 }); 389 await sm.initialize(); 390 391 Assert.equal(sm.secureStorage.fetchCount, 0); 392 // request 2 fields in secure storage - it should have caused a single fetch. 393 let accountData = await sm.getAccountData(["email", "uid"]); 394 Assert.ok(accountData, "read account data"); 395 Assert.equal(accountData.uid, "uid"); 396 Assert.equal(accountData.email, "someone@somewhere.com"); 397 Assert.strictEqual(accountData.scopedKeys, undefined); 398 Assert.equal(sm.secureStorage.fetchCount, 1); 399 }); 400 401 add_task(async function checkLockedUpdates() { 402 let sm = new FxAccountsStorageManager(); 403 sm.plainStorage = new MockedPlainStorage({ 404 uid: "uid", 405 email: "someone@somewhere.com", 406 }); 407 sm.secureStorage = new MockedSecureStorage({ 408 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 409 unwrapBKey: "unwrapBKey", 410 }); 411 sm.secureStorage.locked = true; 412 await sm.initialize(); 413 414 let accountData = await sm.getAccountData(); 415 // requesting scopedKeys will fail as storage is locked. 416 Assert.ok(!accountData.scopedKeys); 417 // While locked we can still update it and see the updated value. 418 sm.updateAccountData({ unwrapBKey: "new-unwrapBKey" }); 419 accountData = await sm.getAccountData(); 420 Assert.equal(accountData.unwrapBKey, "new-unwrapBKey"); 421 // unlock. 422 sm.secureStorage.locked = false; 423 accountData = await sm.getAccountData(); 424 // should reflect the value we updated and the one we didn't. 425 Assert.equal(accountData.unwrapBKey, "new-unwrapBKey"); 426 Assert.deepEqual(accountData.scopedKeys, MOCK_ACCOUNT_KEYS.scopedKeys); 427 // And storage should also reflect it. 428 Assert.deepEqual( 429 sm.secureStorage.data.accountData.scopedKeys, 430 MOCK_ACCOUNT_KEYS.scopedKeys 431 ); 432 Assert.strictEqual( 433 sm.secureStorage.data.accountData.unwrapBKey, 434 "new-unwrapBKey" 435 ); 436 }); 437 438 // Some tests for the "storage queue" functionality. 439 440 // A helper for our queued tests. It creates a StorageManager and then queues 441 // an unresolved promise. The tests then do additional setup and checks, then 442 // resolves or rejects the blocked promise. 443 async function setupStorageManagerForQueueTest() { 444 let sm = new FxAccountsStorageManager(); 445 sm.plainStorage = new MockedPlainStorage({ 446 uid: "uid", 447 email: "someone@somewhere.com", 448 }); 449 sm.secureStorage = new MockedSecureStorage({ 450 scopedKeys: { ...MOCK_ACCOUNT_KEYS.scopedKeys }, 451 }); 452 sm.secureStorage.locked = true; 453 await sm.initialize(); 454 455 let resolveBlocked, rejectBlocked; 456 let blockedPromise = new Promise((resolve, reject) => { 457 resolveBlocked = resolve; 458 rejectBlocked = reject; 459 }); 460 461 sm._queueStorageOperation(() => blockedPromise); 462 return { sm, blockedPromise, resolveBlocked, rejectBlocked }; 463 } 464 465 // First the general functionality. 466 add_task(async function checkQueueSemantics() { 467 let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); 468 469 // We've one unresolved promise in the queue - add another promise. 470 let resolveSubsequent; 471 let subsequentPromise = new Promise(resolve => { 472 resolveSubsequent = resolve; 473 }); 474 let subsequentCalled = false; 475 476 sm._queueStorageOperation(() => { 477 subsequentCalled = true; 478 resolveSubsequent(); 479 return subsequentPromise; 480 }); 481 482 // Our "subsequent" function should not have been called yet. 483 Assert.ok(!subsequentCalled); 484 485 // Release our blocked promise. 486 resolveBlocked(); 487 488 // Our subsequent promise should end up resolved. 489 await subsequentPromise; 490 Assert.ok(subsequentCalled); 491 await sm.finalize(); 492 }); 493 494 // Check that a queued promise being rejected works correctly. 495 add_task(async function checkQueueSemanticsOnError() { 496 let { sm, blockedPromise, rejectBlocked } = 497 await setupStorageManagerForQueueTest(); 498 499 let resolveSubsequent; 500 let subsequentPromise = new Promise(resolve => { 501 resolveSubsequent = resolve; 502 }); 503 let subsequentCalled = false; 504 505 sm._queueStorageOperation(() => { 506 subsequentCalled = true; 507 resolveSubsequent(); 508 return subsequentPromise; 509 }); 510 511 // Our "subsequent" function should not have been called yet. 512 Assert.ok(!subsequentCalled); 513 514 // Reject our blocked promise - the subsequent operations should still work 515 // correctly. 516 rejectBlocked("oh no"); 517 518 // Our subsequent promise should end up resolved. 519 await subsequentPromise; 520 Assert.ok(subsequentCalled); 521 522 // But the first promise should reflect the rejection. 523 try { 524 await blockedPromise; 525 Assert.ok(false, "expected this promise to reject"); 526 } catch (ex) { 527 Assert.equal(ex, "oh no"); 528 } 529 await sm.finalize(); 530 }); 531 532 // And some tests for the specific operations that are queued. 533 add_task(async function checkQueuedReadAndUpdate() { 534 let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); 535 // Mock the underlying operations 536 // _doReadAndUpdateSecure is queued by _maybeReadAndUpdateSecure 537 let _doReadCalled = false; 538 sm._doReadAndUpdateSecure = () => { 539 _doReadCalled = true; 540 return Promise.resolve(); 541 }; 542 543 let resultPromise = sm._maybeReadAndUpdateSecure(); 544 Assert.ok(!_doReadCalled); 545 546 resolveBlocked(); 547 await resultPromise; 548 Assert.ok(_doReadCalled); 549 await sm.finalize(); 550 }); 551 552 add_task(async function checkQueuedWrite() { 553 let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); 554 // Mock the underlying operations 555 let __writeCalled = false; 556 sm.__write = () => { 557 __writeCalled = true; 558 return Promise.resolve(); 559 }; 560 561 let writePromise = sm._write(); 562 Assert.ok(!__writeCalled); 563 564 resolveBlocked(); 565 await writePromise; 566 Assert.ok(__writeCalled); 567 await sm.finalize(); 568 }); 569 570 add_task(async function checkQueuedDelete() { 571 let { sm, resolveBlocked } = await setupStorageManagerForQueueTest(); 572 // Mock the underlying operations 573 let _deleteCalled = false; 574 sm._deleteAccountData = () => { 575 _deleteCalled = true; 576 return Promise.resolve(); 577 }; 578 579 let resultPromise = sm.deleteAccountData(); 580 Assert.ok(!_deleteCalled); 581 582 resolveBlocked(); 583 await resultPromise; 584 Assert.ok(_deleteCalled); 585 await sm.finalize(); 586 });