tor-browser

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

test_CredentialsAndSecurityBackupResource.js (10562B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 https://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule(
      7  "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"
      8 );
      9 
     10 /**
     11 * Tests that we can measure credentials related files in the profile directory.
     12 */
     13 add_task(async function test_measure() {
     14  Services.fog.testResetFOG();
     15 
     16  const EXPECTED_CREDENTIALS_KILOBYTES_SIZE = 403;
     17  const EXPECTED_SECURITY_KILOBYTES_SIZE = 231;
     18 
     19  // Create resource files in temporary directory
     20  const tempDir = await IOUtils.createUniqueDirectory(
     21    PathUtils.tempDir,
     22    "CredentialsAndSecurityBackupResource-measurement-test"
     23  );
     24 
     25  const mockFiles = [
     26    // Set up credentials files
     27    { path: "key4.db", sizeInKB: 300 },
     28    { path: "logins.json", sizeInKB: 1 },
     29    { path: "logins-backup.json", sizeInKB: 1 },
     30    { path: "autofill-profiles.json", sizeInKB: 1 },
     31    { path: "credentialstate.sqlite", sizeInKB: 100 },
     32    // Set up security files
     33    { path: "cert9.db", sizeInKB: 230 },
     34    { path: "pkcs11.txt", sizeInKB: 1 },
     35  ];
     36 
     37  await createTestFiles(tempDir, mockFiles);
     38 
     39  let credentialsAndSecurityBackupResource =
     40    new CredentialsAndSecurityBackupResource();
     41  await credentialsAndSecurityBackupResource.measure(tempDir);
     42 
     43  let credentialsMeasurement =
     44    Glean.browserBackup.credentialsDataSize.testGetValue();
     45  let securityMeasurement = Glean.browserBackup.securityDataSize.testGetValue();
     46  let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false);
     47 
     48  // Credentials measurements
     49  TelemetryTestUtils.assertScalar(
     50    scalars,
     51    "browser.backup.credentials_data_size",
     52    credentialsMeasurement,
     53    "Glean and telemetry measurements for credentials data should be equal"
     54  );
     55 
     56  Assert.equal(
     57    credentialsMeasurement,
     58    EXPECTED_CREDENTIALS_KILOBYTES_SIZE,
     59    "Should have collected the correct glean measurement for credentials files"
     60  );
     61 
     62  // Security measurements
     63  TelemetryTestUtils.assertScalar(
     64    scalars,
     65    "browser.backup.security_data_size",
     66    securityMeasurement,
     67    "Glean and telemetry measurements for security data should be equal"
     68  );
     69  Assert.equal(
     70    securityMeasurement,
     71    EXPECTED_SECURITY_KILOBYTES_SIZE,
     72    "Should have collected the correct glean measurement for security files"
     73  );
     74 
     75  // Cleanup
     76  await maybeRemovePath(tempDir);
     77 });
     78 
     79 /**
     80 * Test that the backup method correctly copies items from the profile directory
     81 * into the staging directory.
     82 */
     83 add_task(async function test_backup() {
     84  let sandbox = sinon.createSandbox();
     85 
     86  let credentialsAndSecurityBackupResource =
     87    new CredentialsAndSecurityBackupResource();
     88  let sourcePath = await IOUtils.createUniqueDirectory(
     89    PathUtils.tempDir,
     90    "CredentialsAndSecurityBackupResource-source-test"
     91  );
     92  let stagingPath = await IOUtils.createUniqueDirectory(
     93    PathUtils.tempDir,
     94    "CredentialsAndSecurityBackupResource-staging-test"
     95  );
     96 
     97  const simpleCopyFiles = [
     98    { path: "logins.json", sizeInKB: 1 },
     99    { path: "logins-backup.json", sizeInKB: 1 },
    100    { path: "autofill-profiles.json", sizeInKB: 1 },
    101    { path: "pkcs11.txt", sizeInKB: 1 },
    102  ];
    103  await createTestFiles(sourcePath, simpleCopyFiles);
    104 
    105  // Create our fake database files. We don't expect these to be copied to the
    106  // staging directory in this test due to our stubbing of the backup method, so
    107  // we don't include it in `simpleCopyFiles`.
    108  await createTestFiles(sourcePath, [
    109    { path: "cert9.db" },
    110    { path: "key4.db" },
    111    { path: "credentialstate.sqlite" },
    112  ]);
    113 
    114  // We have no need to test that Sqlite.sys.mjs's backup method is working -
    115  // this is something that is tested in Sqlite's own tests. We can just make
    116  // sure that it's being called using sinon. Unfortunately, we cannot do the
    117  // same thing with IOUtils.copy, as its methods are not stubbable.
    118  let fakeConnection = {
    119    backup: sandbox.stub().resolves(true),
    120    close: sandbox.stub().resolves(true),
    121  };
    122  sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
    123 
    124  let manifestEntry = await credentialsAndSecurityBackupResource.backup(
    125    stagingPath,
    126    sourcePath
    127  );
    128 
    129  Assert.equal(
    130    manifestEntry,
    131    null,
    132    "CredentialsAndSecurityBackupResource.backup should return null as its ManifestEntry"
    133  );
    134 
    135  await assertFilesExist(stagingPath, simpleCopyFiles);
    136 
    137  // Next, we'll make sure that the Sqlite connection had `backup` called on it
    138  // with the right arguments.
    139  Assert.ok(
    140    fakeConnection.backup.calledThrice,
    141    "Called backup the expected number of times for all connections"
    142  );
    143  Assert.ok(
    144    fakeConnection.backup.firstCall.calledWith(
    145      PathUtils.join(stagingPath, "cert9.db")
    146    ),
    147    "Called backup on cert9.db connection first"
    148  );
    149  Assert.ok(
    150    fakeConnection.backup.secondCall.calledWith(
    151      PathUtils.join(stagingPath, "key4.db")
    152    ),
    153    "Called backup on key4.db connection second"
    154  );
    155  Assert.ok(
    156    fakeConnection.backup.thirdCall.calledWith(
    157      PathUtils.join(stagingPath, "credentialstate.sqlite")
    158    ),
    159    "Called backup on credentialstate.sqlite connection third"
    160  );
    161 
    162  await maybeRemovePath(stagingPath);
    163  await maybeRemovePath(sourcePath);
    164 
    165  sandbox.restore();
    166 });
    167 
    168 /**
    169 * Test that the recover method correctly copies items from the recovery
    170 * directory into the destination profile directory.
    171 */
    172 add_task(async function test_recover() {
    173  let credentialsAndSecurityBackupResource =
    174    new CredentialsAndSecurityBackupResource();
    175  let recoveryPath = await IOUtils.createUniqueDirectory(
    176    PathUtils.tempDir,
    177    "CredentialsAndSecurityBackupResource-recovery-test"
    178  );
    179  let destProfilePath = await IOUtils.createUniqueDirectory(
    180    PathUtils.tempDir,
    181    "CredentialsAndSecurityBackupResource-test-profile"
    182  );
    183 
    184  const files = [
    185    { path: "logins.json" },
    186    { path: "logins-backup.json" },
    187    { path: "credentialstate.sqlite" },
    188    { path: "cert9.db" },
    189    { path: "key4.db" },
    190    { path: "pkcs11.txt" },
    191  ];
    192  await createTestFiles(recoveryPath, files);
    193 
    194  const ENCRYPTED_CARD_FOR_BACKUP = "ThisIsAnEncryptedCard";
    195  const PLAINTEXT_CARD = "ThisIsAPlaintextCard";
    196 
    197  let plaintextBytes = new Uint8Array(PLAINTEXT_CARD.length);
    198  for (let i = 0; i < PLAINTEXT_CARD.length; i++) {
    199    plaintextBytes[i] = PLAINTEXT_CARD.charCodeAt(i);
    200  }
    201 
    202  const ENCRYPTED_CARD_AFTER_RECOVERY = "ThisIsAnEncryptedCardAfterRecovery";
    203 
    204  // Now construct a facimile of an autofill-profiles.json file. We need to
    205  // test the ability to decrypt credit card numbers within it via the
    206  // nativeOSKeyStore using the BackupService.RECOVERY_OSKEYSTORE_LABEL, and
    207  // re-encrypt them using the existing OSKeyStore.
    208  let autofillObject = {
    209    someOtherField: "test-123",
    210    creditCards: [
    211      { "cc-number-encrypted": ENCRYPTED_CARD_FOR_BACKUP, "cc-expiry": "1234" },
    212    ],
    213  };
    214  const AUTOFILL_PROFILES_FILENAME = "autofill-profiles.json";
    215  await IOUtils.writeJSON(
    216    PathUtils.join(recoveryPath, AUTOFILL_PROFILES_FILENAME),
    217    autofillObject
    218  );
    219 
    220  // Now we'll prepare the native OSKeyStore to accept a single call to
    221  // asyncDecryptBytes, and then a single call to asyncEncryptBytes.
    222  gFakeOSKeyStore.asyncDecryptBytes.resolves(plaintextBytes);
    223  gFakeOSKeyStore.asyncEncryptBytes.resolves(ENCRYPTED_CARD_AFTER_RECOVERY);
    224 
    225  // The backup method is expected to have returned a null ManifestEntry
    226  let postRecoveryEntry = await credentialsAndSecurityBackupResource.recover(
    227    null /* manifestEntry */,
    228    recoveryPath,
    229    destProfilePath
    230  );
    231 
    232  Assert.equal(
    233    postRecoveryEntry,
    234    null,
    235    "CredentialsAndSecurityBackupResource.recover should return null as its post " +
    236      "recovery entry"
    237  );
    238 
    239  await assertFilesExist(destProfilePath, files);
    240 
    241  const RECOVERED_AUTOFILL_FILE_PATH = PathUtils.join(
    242    destProfilePath,
    243    AUTOFILL_PROFILES_FILENAME
    244  );
    245  Assert.ok(
    246    await IOUtils.exists(RECOVERED_AUTOFILL_FILE_PATH),
    247    `${AUTOFILL_PROFILES_FILENAME} file was copied`
    248  );
    249 
    250  let recoveredAutofillObject = await IOUtils.readJSON(
    251    RECOVERED_AUTOFILL_FILE_PATH
    252  );
    253  let expectedAutofillObject = Object.assign({}, autofillObject);
    254  autofillObject.creditCards[0]["cc-number-encrypted"] =
    255    ENCRYPTED_CARD_AFTER_RECOVERY;
    256 
    257  Assert.deepEqual(
    258    recoveredAutofillObject,
    259    expectedAutofillObject,
    260    `${AUTOFILL_PROFILES_FILENAME} contained the expected data structure.`
    261  );
    262 
    263  await maybeRemovePath(recoveryPath);
    264  await maybeRemovePath(destProfilePath);
    265 
    266  gFakeOSKeyStore.asyncDecryptBytes.resetHistory();
    267  gFakeOSKeyStore.asyncEncryptBytes.resetHistory();
    268 });
    269 
    270 add_task(async function test_recover_without_autofill_profiles() {
    271  let credentialsAndSecurityBackupResource =
    272    new CredentialsAndSecurityBackupResource();
    273  let recoveryPath = await IOUtils.createUniqueDirectory(
    274    PathUtils.tempDir,
    275    "CredentialsAndSecurityBackupResource-recovery-test"
    276  );
    277  let destProfilePath = await IOUtils.createUniqueDirectory(
    278    PathUtils.tempDir,
    279    "CredentialsAndSecurityBackupResource-test-profile"
    280  );
    281 
    282  const files = [
    283    { path: "logins.json" },
    284    { path: "logins-backup.json" },
    285    { path: "credentialstate.sqlite" },
    286    { path: "cert9.db" },
    287    { path: "key4.db" },
    288    { path: "pkcs11.txt" },
    289  ];
    290  await createTestFiles(recoveryPath, files);
    291 
    292  const ENCRYPTED_CARD_AFTER_RECOVERY = "ThisIsAnEncryptedCardAfterRecovery";
    293  const PLAINTEXT_CARD = "ThisIsAPlaintextCard";
    294 
    295  let plaintextBytes = new Uint8Array(PLAINTEXT_CARD.length);
    296  for (let i = 0; i < PLAINTEXT_CARD.length; i++) {
    297    plaintextBytes[i] = PLAINTEXT_CARD.charCodeAt(i);
    298  }
    299 
    300  // Now we'll prepare the native OSKeyStore to accept a single call to
    301  // asyncDecryptBytes, and then a single call to asyncEncryptBytes.
    302  gFakeOSKeyStore.asyncDecryptBytes.resolves(plaintextBytes);
    303  gFakeOSKeyStore.asyncEncryptBytes.resolves(ENCRYPTED_CARD_AFTER_RECOVERY);
    304 
    305  // The backup method is expected to have returned a null ManifestEntry
    306  let postRecoveryEntry = await credentialsAndSecurityBackupResource.recover(
    307    null /* manifestEntry */,
    308    recoveryPath,
    309    destProfilePath
    310  );
    311 
    312  Assert.equal(
    313    postRecoveryEntry,
    314    null,
    315    "CredentialsAndSecurityBackupResource.recover should return null as its post " +
    316      "recovery entry"
    317  );
    318 
    319  await assertFilesExist(destProfilePath, files);
    320  await maybeRemovePath(recoveryPath);
    321  await maybeRemovePath(destProfilePath);
    322 
    323  gFakeOSKeyStore.asyncDecryptBytes.resetHistory();
    324  gFakeOSKeyStore.asyncEncryptBytes.resetHistory();
    325 });