tor-browser

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

head.js (15325B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 ChromeUtils.defineESModuleGetters(this, {
      8  BackupService: "resource:///modules/backup/BackupService.sys.mjs",
      9  BackupResource: "resource:///modules/backup/BackupResource.sys.mjs",
     10  MeasurementUtils: "resource:///modules/backup/MeasurementUtils.sys.mjs",
     11  TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
     12  Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
     13  sinon: "resource://testing-common/Sinon.sys.mjs",
     14  OSKeyStoreTestUtils: "resource://testing-common/OSKeyStoreTestUtils.sys.mjs",
     15  MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
     16  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     17  AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
     18  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     19 });
     20 
     21 const HISTORY_ENABLED_PREF = "places.history.enabled";
     22 const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown";
     23 const USE_OLD_CLEAR_HISTORY_DIALOG_PREF =
     24  "privacy.sanitize.useOldClearHistoryDialog";
     25 const FORM_HISTORY_CLEARED_ON_SHUTDOWN_PREF =
     26  "privacy.clearOnShutdown_v2.formdata";
     27 const HISTORY_CLEARED_ON_SHUTDOWN_PREF =
     28  "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads";
     29 const SITE_SETTINGS_CLEARED_ON_SHUTDOWN_PREF =
     30  "privacy.clearOnShutdown_v2.siteSettings";
     31 const SITE_SETTINGS_CLEARED_ON_SHUTDOWN_OLD_PREF =
     32  "privacy.clearOnShutdown.siteSettings";
     33 
     34 let gFakeOSKeyStore;
     35 
     36 add_setup(async () => {
     37  // During our unit tests, we're not interested in showing OSKeyStore
     38  // authentication dialogs, nor are we interested in actually using the "real"
     39  // OSKeyStore. We instead swap in our own implementation of nsIOSKeyStore
     40  // which provides some stubbed out values. We also set up OSKeyStoreTestUtils
     41  // which will suppress any reauthentication dialogs.
     42  gFakeOSKeyStore = {
     43    asyncEncryptBytes: sinon.stub(),
     44    asyncDecryptBytes: sinon.stub(),
     45    asyncDeleteSecret: sinon.stub().resolves(),
     46    asyncSecretAvailable: sinon.stub().resolves(true),
     47    asyncGetRecoveryPhrase: sinon.stub().resolves("SomeRecoveryPhrase"),
     48    asyncRecoverSecret: sinon.stub().resolves(),
     49    QueryInterface: ChromeUtils.generateQI([Ci.nsIOSKeyStore]),
     50  };
     51  let osKeyStoreCID = MockRegistrar.register(
     52    "@mozilla.org/security/oskeystore;1",
     53    gFakeOSKeyStore
     54  );
     55 
     56  OSKeyStoreTestUtils.setup();
     57  registerCleanupFunction(async () => {
     58    await OSKeyStoreTestUtils.cleanup();
     59    MockRegistrar.unregister(osKeyStoreCID);
     60  });
     61 
     62  // This will set gDirServiceProvider, which doesn't happen automatically
     63  // in xpcshell tests, but is needed by BackupService.
     64  Cc["@mozilla.org/xre/directory-provider;1"].getService(Ci.nsIXREDirProvider);
     65 });
     66 
     67 const BYTES_IN_KB = 1000;
     68 
     69 const FAKE_METADATA = {
     70  date: "2024-06-07T00:00:00+00:00",
     71  appName: "firefox",
     72  appVersion: "128.0",
     73  buildID: "20240604133346",
     74  profileName: "profile-default",
     75  machineName: "A super cool machine",
     76  osName: "Windows_NT",
     77  osVersion: "10.0",
     78  legacyClientID: "decafbad-0cd1-0cd2-0cd3-decafbad1000",
     79  profileGroupID: "decafbad-0cd1-0cd2-0cd3-decafbad2000",
     80  accountID: "",
     81  accountEmail: "",
     82 };
     83 
     84 do_get_profile();
     85 
     86 // Configure any backup files to get written into a temporary folder.
     87 Services.prefs.setStringPref("browser.backup.location", PathUtils.tempDir);
     88 
     89 /**
     90 * Some fake backup resource classes to test with.
     91 */
     92 class FakeBackupResource1 extends BackupResource {
     93  static get key() {
     94    return "fake1";
     95  }
     96  static get requiresEncryption() {
     97    return false;
     98  }
     99 }
    100 
    101 /**
    102 * Another fake backup resource class to test with.
    103 */
    104 class FakeBackupResource2 extends BackupResource {
    105  static get key() {
    106    return "fake2";
    107  }
    108  static get requiresEncryption() {
    109    return false;
    110  }
    111  static get priority() {
    112    return 1;
    113  }
    114 }
    115 
    116 /**
    117 * Yet another fake backup resource class to test with.
    118 */
    119 class FakeBackupResource3 extends BackupResource {
    120  static get key() {
    121    return "fake3";
    122  }
    123  static get requiresEncryption() {
    124    return false;
    125  }
    126  static get priority() {
    127    return 2;
    128  }
    129 }
    130 
    131 /**
    132 * Create a file of a given size in kilobytes.
    133 *
    134 * @param {string} path the path where the file will be created.
    135 * @param {number} sizeInKB size file in Kilobytes.
    136 * @returns {Promise<undefined>}
    137 */
    138 async function createKilobyteSizedFile(path, sizeInKB) {
    139  let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB);
    140  await IOUtils.write(path, bytes);
    141 }
    142 
    143 /**
    144 * @typedef {object} TestFileObject
    145 * @property {(string|Array.<string>)} path
    146 *   The relative path of the file. It can be a string or an array of strings
    147 *   in the event that directories need to be created. For example, this is
    148 *   an array of valid TestFileObjects.
    149 *
    150 *   [
    151 *     { path: "file1.txt" },
    152 *     { path: ["dir1", "file2.txt"] },
    153 *     { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 },
    154 *     { path: "file4.txt" },
    155 *   ]
    156 *
    157 * @property {number} [sizeInKB=10]
    158 *   The size of the created file in kilobytes. Defaults to 10.
    159 */
    160 
    161 /**
    162 * Easily creates a series of test files and directories under parentPath.
    163 *
    164 * @param {string} parentPath
    165 *   The path to the parent directory where the files will be created.
    166 * @param {TestFileObject[]} testFilesArray
    167 *   An array of TestFileObjects describing what test files to create within
    168 *   the parentPath.
    169 * @see TestFileObject
    170 * @returns {Promise<undefined>}
    171 */
    172 async function createTestFiles(parentPath, testFilesArray) {
    173  for (let { path, sizeInKB } of testFilesArray) {
    174    if (Array.isArray(path)) {
    175      // Make a copy of the array of path elements, chopping off the last one.
    176      // We'll assume the unchopped items are directories, and make sure they
    177      // exist first.
    178      let folders = path.slice(0, -1);
    179      await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders));
    180    }
    181 
    182    if (sizeInKB === undefined) {
    183      sizeInKB = 10;
    184    }
    185 
    186    // This little piece of cleverness coerces a string into an array of one
    187    // if path is a string, or just leaves it alone if it's already an array.
    188    let filePath = PathUtils.join(parentPath, ...[].concat(path));
    189    await createKilobyteSizedFile(filePath, sizeInKB);
    190  }
    191 }
    192 
    193 /**
    194 * Checks that files exist within a particular folder. The filesize is not
    195 * checked.
    196 *
    197 * @param {string} parentPath
    198 *   The path to the parent directory where the files should exist.
    199 * @param {TestFileObject[]} testFilesArray
    200 *   An array of TestFileObjects describing what test files to search for within
    201 *   parentPath.
    202 * @see TestFileObject
    203 * @returns {Promise<undefined>}
    204 */
    205 async function assertFilesExist(parentPath, testFilesArray) {
    206  for (let { path } of testFilesArray) {
    207    let copiedFileName = PathUtils.join(parentPath, ...[].concat(path));
    208    Assert.ok(
    209      await IOUtils.exists(copiedFileName),
    210      `${copiedFileName} should exist in the staging folder`
    211    );
    212  }
    213 }
    214 
    215 /**
    216 * Perform removalFn, potentially a few times, with special consideration for
    217 * Windows, which has issues with file removal.  Sometimes remove() throws
    218 * ERROR_LOCK_VIOLATION when the file is not unlocked soon enough.  This can
    219 * happen because kernel operations still hold a handle to it, or because e.g.
    220 * Windows Defender started a scan, or whatever.  Some applications (for
    221 * example SQLite) retry deletes with a delay on Windows.  We emulate that
    222 * here, since this happens very frequently in tests.
    223 *
    224 * This registers a test failure if it does not succeed.
    225 *
    226 * @param {Function} removalFn  Async function to (potentially repeatedly)
    227 *                              call.
    228 * @throws The resultant file system exception if removal fails, despite the
    229 *         special considerations.
    230 */
    231 async function doFileRemovalOperation(removalFn) {
    232  const kMaxRetries = 5;
    233  let nRetries = 0;
    234  let lastException;
    235  while (nRetries < kMaxRetries) {
    236    nRetries++;
    237    try {
    238      await removalFn();
    239      // Success!
    240      return;
    241    } catch (error) {
    242      if (
    243        AppConstants.platform !== "win" ||
    244        !/NS_ERROR_FILE_IS_LOCKED/.test(error.message)
    245      ) {
    246        throw error;
    247      }
    248      lastException = error;
    249    }
    250    await new Promise(res => setTimeout(res, 100));
    251  }
    252 
    253  // All retries failed.  Re-throw last exception.
    254  Assert.ok(false, `doRemovalOperation failed after ${nRetries} attempts`);
    255  throw lastException;
    256 }
    257 
    258 /**
    259 * Remove a file or directory at a path if it exists and files are unlocked.
    260 * Prefer this to IOUtils.remove in tests to avoid intermittent failures
    261 * because of the Windows-ism mentioned in doFileRemovalOperation.
    262 *
    263 * @param {string} path path to remove.
    264 */
    265 async function maybeRemovePath(path) {
    266  let nAttempts = 0;
    267  await doFileRemovalOperation(async () => {
    268    nAttempts++;
    269    await IOUtils.remove(path, { ignoreAbsent: true, recursive: true });
    270    Assert.ok(true, `Removed ${path} on attempt #${nAttempts}`);
    271  });
    272 }
    273 
    274 /**
    275 * A generator function for deterministically generating a sequence of
    276 * pseudo-random numbers between 0 and 255 with a fixed seed. This means we can
    277 * generate an arbitrary amount of nonsense information, but that generation
    278 * will be consistent between test runs. It's definitely not a cryptographically
    279 * secure random number generator! Please don't use it for that!
    280 *
    281 * @yields {number}
    282 *   The next number in the sequence.
    283 */
    284 function* seededRandomNumberGenerator() {
    285  // This is a verbatim copy of the public domain-licensed code in
    286  // https://github.com/bryc/code/blob/master/jshash/PRNGs.md for the sfc32
    287  // PRNG (see https://pracrand.sourceforge.net/RNG_engines.txt)
    288  let sfc32 = function (a, b, c, d) {
    289    return function () {
    290      a |= 0;
    291      b |= 0;
    292      c |= 0;
    293      d |= 0;
    294      var t = (((a + b) | 0) + d) | 0;
    295      d = (d + 1) | 0;
    296      a = b ^ (b >>> 9);
    297      b = (c + (c << 3)) | 0;
    298      c = (c << 21) | (c >>> 11);
    299      c = (c + t) | 0;
    300      return (t >>> 0) / 4294967296;
    301    };
    302  };
    303 
    304  // The seeds don't need to make sense, they just need to be the same from
    305  // test run to test run to give us a consistent stream of nonsense.
    306  const SEED1 = 123;
    307  const SEED2 = 456;
    308  const SEED3 = 789;
    309  const SEED4 = 101;
    310 
    311  let generator = sfc32(SEED1, SEED2, SEED3, SEED4);
    312 
    313  while (true) {
    314    yield Math.round(generator() * 1000) % 255;
    315  }
    316 }
    317 
    318 /**
    319 * Compares 2 Uint8Arrays and checks their similarity. This is mainly used to
    320 * test that the encrypted bytes passed through ArchiveEncryptor actually get
    321 * changed, and that the bytes that come out of ArchiveDecryptor match the
    322 * source bytes. This doesn't test that the resulting bytes have gone through
    323 * the Web Crypto API.
    324 *
    325 * When expecting similar arrays, we expect the byte lengths to be the same, and
    326 * for the bytes to match. When expecting dissimilar arrays, we expect the byte
    327 * lengths to be different and for at least one byte to be dissimilar between
    328 * the two arrays.
    329 *
    330 * @param {Uint8Array} uint8ArrayA
    331 *   The left-side of the Uint8Array comparison.
    332 * @param {Uint8Array} uint8ArrayB
    333 *   The right-side fo the Uint8Array comparison.
    334 * @param {boolean} expectSimilar
    335 *   True if the caller expects the two arrays to be similar, false otherwise.
    336 */
    337 function assertUint8ArraysSimilarity(uint8ArrayA, uint8ArrayB, expectSimilar) {
    338  let lengthToCheck;
    339  if (expectSimilar) {
    340    Assert.equal(
    341      uint8ArrayA.byteLength,
    342      uint8ArrayB.byteLength,
    343      "Uint8Arrays have the same byteLength"
    344    );
    345    lengthToCheck = uint8ArrayA.byteLength;
    346  } else {
    347    Assert.notEqual(
    348      uint8ArrayA.byteLength,
    349      uint8ArrayB.byteLength,
    350      "Uint8Arrays have differing byteLength"
    351    );
    352    lengthToCheck = Math.min(uint8ArrayA.byteLength, uint8ArrayB.byteLength);
    353  }
    354 
    355  let foundDifference = false;
    356  for (let i = 0; i < lengthToCheck; ++i) {
    357    if (uint8ArrayA[i] != uint8ArrayB[i]) {
    358      foundDifference = true;
    359      break;
    360    }
    361  }
    362 
    363  if (expectSimilar) {
    364    Assert.ok(!foundDifference, "Arrays contain the same bytes.");
    365  } else {
    366    Assert.ok(foundDifference, "Arrays contain different bytes.");
    367  }
    368 }
    369 
    370 /**
    371 * Returns the total number of measurements taken for this histogram, regardless
    372 * of the values of the measurements themselves.
    373 *
    374 * @param {object} histogram
    375 *   Telemetry histogram object, like from `getHistogramById`
    376 * @returns {number}
    377 *   Number of measurements in the latest snapshot of the histogram
    378 */
    379 function countHistogramMeasurements(histogram) {
    380  const snapshot = histogram.snapshot();
    381  const countsPerBucket = Object.values(snapshot.values);
    382  return countsPerBucket.reduce((sum, count) => sum + count, 0);
    383 }
    384 
    385 function setupProfile() {
    386  // FOG needs to be initialized in order for data to flow.
    387  Services.fog.initializeFOG();
    388 
    389  // Much of this setup is copied from toolkit/profile/xpcshell/head.js. It is
    390  // needed in order to put the xpcshell test environment into the state where
    391  // it thinks its profile is the one pointed at by
    392  // nsIToolkitProfileService.currentProfile.
    393  let gProfD = do_get_profile();
    394  let gDataHome = gProfD.clone();
    395  gDataHome.append("data");
    396  gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
    397  let gDataHomeLocal = gProfD.clone();
    398  gDataHomeLocal.append("local");
    399  gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
    400 
    401  let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
    402    Ci.nsIXREDirProvider
    403  );
    404  xreDirProvider.setUserDataDirectory(gDataHome, false);
    405  xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
    406 
    407  let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
    408    Ci.nsIToolkitProfileService
    409  );
    410 
    411  let createdProfile = {};
    412  let didCreate = profileSvc.selectStartupProfile(
    413    ["xpcshell"],
    414    false,
    415    AppConstants.UPDATE_CHANNEL,
    416    "",
    417    {},
    418    {},
    419    createdProfile
    420  );
    421  Assert.ok(didCreate, "Created a testing profile and set it to current.");
    422  Assert.equal(
    423    profileSvc.currentProfile,
    424    createdProfile.value,
    425    "Profile set to current"
    426  );
    427 
    428  return createdProfile.value;
    429 }
    430 
    431 /**
    432 * Asserts that a histogram received a certain number of measurements, regardless
    433 * of the values of the measurements themselves.
    434 *
    435 * @param {object} histogram
    436 *   Telemetry histogram object, like from `getHistogramById`
    437 * @param {number} expected
    438 *   Expected number of measurements to have been taken
    439 * @param {string?} message
    440 *   Optional message for test report
    441 * @returns {void}
    442 *   No return value; only runs assertions
    443 */
    444 function assertHistogramMeasurementQuantity(
    445  histogram,
    446  expected,
    447  message = "Should have taken a specific number of measurements in the histogram"
    448 ) {
    449  const totalCount = countHistogramMeasurements(histogram);
    450  Assert.equal(totalCount, expected, message);
    451 }
    452 
    453 /**
    454 * @param {GleanDistributionData?} timerTestValue
    455 *   Glean timer from `testGetValue`
    456 * @returns {void}
    457 *   No return value; only runs assertions
    458 */
    459 function assertSingleTimeMeasurement(timerTestValue) {
    460  Assert.notEqual(timerTestValue, null, "Timer should have something recorded");
    461  Assert.equal(
    462    timerTestValue.count,
    463    1,
    464    "Timer should have a single measurement"
    465  );
    466  Assert.greater(timerTestValue.sum, 0, "Timer measurement should be non-zero");
    467 }