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