tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });