Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test runs only with pattern: os != 'android'
- Manifest: browser/components/backup/tests/xpcshell/xpcshell.toml
/* Any copyright is dedicated to the Public Domain.
"use strict";
const { ArchiveEncryptionState } = ChromeUtils.importESModule(
"resource:///modules/backup/ArchiveEncryptionState.sys.mjs"
);
const { ERRORS } = ChromeUtils.importESModule(
"chrome://browser/content/backup/backup-constants.mjs"
);
const TEST_PASSWORD = "This is some test password.";
const TEST_PASSWORD_ALT = "mozilla.org";
/**
* Creates a backup with this backup service and checks whether it can be
* decrypted with the given password. This ensures that encryption is
* enabled/disabled as needed, and that the backup service will actually use
* the given password if applicable.
*
* @param {BackupService} bs
* The BackupService to use.
* @param {string} profilePath
* The path to the current profile.
* @param {string?} password
* The expected password, or null if it should not be encrypted.
* @returns {boolean}
* True if the password matched, false if something went wrong.
*/
async function backupServiceUsesPassword(bs, profilePath, password = null) {
try {
const backup = await bs.createBackup({ profilePath });
let dest = await IOUtils.createUniqueFile(
PathUtils.parent(backup.archivePath),
"extracted-" + PathUtils.filename(backup.archivePath)
);
let { isEncrypted } = await bs.sampleArchive(backup.archivePath);
let shouldBeEncrypted = password != null;
Assert.equal(
isEncrypted,
shouldBeEncrypted,
`Archive is ${shouldBeEncrypted ? "" : "not "}encrypted`
);
// This should throw if the password is incorrect.
await bs.extractCompressedSnapshotFromArchive(
backup.archivePath,
dest,
password
);
await IOUtils.remove(backup.archivePath);
await IOUtils.remove(dest);
return true;
} catch (e) {
console.error(e);
return false;
}
}
add_setup(async () => {
// Much of this setup is copied from toolkit/profile/xpcshell/head.js. It is
// needed in order to put the xpcshell test environment into the state where
// it thinks its profile is the one pointed at by
// nsIToolkitProfileService.currentProfile.
let gProfD = do_get_profile();
let gDataHome = gProfD.clone();
gDataHome.append("data");
gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let gDataHomeLocal = gProfD.clone();
gDataHomeLocal.append("local");
gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
Ci.nsIXREDirProvider
);
xreDirProvider.setUserDataDirectory(gDataHome, false);
xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
);
let createdProfile = {};
let didCreate = profileSvc.selectStartupProfile(
["xpcshell"],
false,
AppConstants.UPDATE_CHANNEL,
"",
{},
{},
createdProfile
);
Assert.ok(didCreate, "Created a testing profile and set it to current.");
Assert.equal(
profileSvc.currentProfile,
createdProfile.value,
"Profile set to current"
);
});
/**
* Tests that if encryption is disabled that only BackupResource's with
* requiresEncryption set to `false` will have `backup()` called on them.
*/
add_task(async function test_disabled_encryption() {
let sandbox = sinon.createSandbox();
let bs = new BackupService({
FakeBackupResource1,
FakeBackupResource2,
FakeBackupResource3,
});
Assert.ok(
!bs.state.encryptionEnabled,
"State should indicate that encryption is disabled."
);
let testProfilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"testDisabledEncryption"
);
let encState = await bs.loadEncryptionState(testProfilePath);
Assert.ok(!encState, "Should not find an ArchiveEncryptionState.");
// Override FakeBackupResource2 so that it requires encryption.
sandbox.stub(FakeBackupResource2, "requiresEncryption").get(() => {
return true;
});
Assert.ok(
FakeBackupResource2.requiresEncryption,
"FakeBackupResource2 requires encryption."
);
// This is how these FakeBackupResource's are defined in head.js
Assert.ok(
!FakeBackupResource1.requiresEncryption,
"FakeBackupResource1 does not require encryption."
);
Assert.ok(
!FakeBackupResource3.requiresEncryption,
"FakeBackupResource3 does not require encryption."
);
let resourceWithoutEncryptionStubs = [
sandbox.stub(FakeBackupResource1.prototype, "backup").resolves(null),
sandbox.stub(FakeBackupResource3.prototype, "backup").resolves(null),
];
let resourceWithEncryptionStub = sandbox
.stub(FakeBackupResource2.prototype, "backup")
.resolves(null);
await bs.createBackup({ profilePath: testProfilePath });
Assert.ok(
resourceWithEncryptionStub.notCalled,
"FakeBackupResource2.backup should not have been called"
);
for (let resourceWithoutEncryptionStub of resourceWithoutEncryptionStubs) {
Assert.ok(
resourceWithoutEncryptionStub.calledOnce,
"backup called on resource that didn't require encryption"
);
}
await IOUtils.remove(testProfilePath, { recursive: true });
sandbox.restore();
});
/**
* Tests that if encryption is enabled from a non-enabled state, that an
* ArchiveEncryptionState is created, and state is written to the profile
* directory. Also tests that this allows BackupResource's with
* requiresEncryption set to `true` to have `backup()` called on them.
*/
add_task(async function test_enable_encryption() {
let sandbox = sinon.createSandbox();
let bs = new BackupService({
FakeBackupResource1,
FakeBackupResource2,
FakeBackupResource3,
});
Assert.ok(
!bs.state.encryptionEnabled,
"State should initially indicate that encryption is disabled."
);
let testProfilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"testEnableEncryption"
);
let encState = await bs.loadEncryptionState(testProfilePath);
Assert.ok(!encState, "Should not find an ArchiveEncryptionState.");
// Now enable encryption.
let stateUpdatePromise = new Promise(resolve => {
bs.addEventListener("BackupService:StateUpdate", resolve, { once: true });
});
await bs.enableEncryption(TEST_PASSWORD, testProfilePath);
await stateUpdatePromise;
Assert.ok(
bs.state.encryptionEnabled,
"State should indicate that encryption is enabled."
);
Assert.ok(
await IOUtils.exists(
PathUtils.join(
testProfilePath,
BackupService.PROFILE_FOLDER_NAME,
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
)
),
"Encryption state file should exist."
);
// Override FakeBackupResource2 so that it requires encryption.
sandbox.stub(FakeBackupResource2, "requiresEncryption").get(() => {
return true;
});
Assert.ok(
FakeBackupResource2.requiresEncryption,
"FakeBackupResource2 requires encryption."
);
// This is how these FakeBackupResource's are defined in head.js
Assert.ok(
!FakeBackupResource1.requiresEncryption,
"FakeBackupResource1 does not require encryption."
);
Assert.ok(
!FakeBackupResource3.requiresEncryption,
"FakeBackupResource3 does not require encryption."
);
let allResourceBackupStubs = [
sandbox.stub(FakeBackupResource1.prototype, "backup").resolves(null),
sandbox.stub(FakeBackupResource3.prototype, "backup").resolves(null),
sandbox.stub(FakeBackupResource2.prototype, "backup").resolves(null),
];
await bs.createBackup({
profilePath: testProfilePath,
});
for (let resourceBackupStub of allResourceBackupStubs) {
Assert.ok(resourceBackupStub.calledOnce, "backup called on resource");
}
Assert.ok(
await IOUtils.exists(
PathUtils.join(
testProfilePath,
BackupService.PROFILE_FOLDER_NAME,
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
)
),
"Encryption state file should still exist."
);
await IOUtils.remove(testProfilePath, { recursive: true });
sandbox.restore();
});
/**
* Tests that enabling encryption while it is already enabled changes the
* password.
*/
add_task(async function test_change_encryption_password() {
let bs = new BackupService();
let testProfilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"testAlreadyEnabledEncryption"
);
// Enable encryption.
await bs.enableEncryption(TEST_PASSWORD, testProfilePath);
let encState = await bs.loadEncryptionState(testProfilePath);
Assert.ok(encState, "ArchiveEncryptionState is available.");
Assert.ok(
await backupServiceUsesPassword(bs, testProfilePath, TEST_PASSWORD),
"BackupService is using TEST_PASSWORD"
);
// Change the password.
await bs.enableEncryption(TEST_PASSWORD_ALT, testProfilePath);
encState = await bs.loadEncryptionState(testProfilePath);
Assert.ok(encState, "ArchiveEncryptionState is still available.");
Assert.ok(
await backupServiceUsesPassword(bs, testProfilePath, TEST_PASSWORD_ALT),
"BackupService is using TEST_PASSWORD_ALT"
);
await IOUtils.remove(testProfilePath, { recursive: true });
});
/**
* Tests that if encryption is enabled that it can be disabled.
*/
add_task(async function test_disabling_encryption() {
let bs = new BackupService();
let testProfilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"testDisableEncryption"
);
// Enable encryption.
await bs.enableEncryption(TEST_PASSWORD, testProfilePath);
let encState = await bs.loadEncryptionState(testProfilePath);
Assert.ok(encState, "ArchiveEncryptionState is available.");
Assert.ok(
bs.state.encryptionEnabled,
"State should indicate that encryption is enabled."
);
Assert.ok(
await IOUtils.exists(
PathUtils.join(
testProfilePath,
BackupService.PROFILE_FOLDER_NAME,
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
)
),
"Encryption state file should exist."
);
// Now disable encryption.
let stateUpdatePromise = new Promise(resolve => {
bs.addEventListener("BackupService:StateUpdate", resolve, { once: true });
});
await bs.disableEncryption(testProfilePath);
await stateUpdatePromise;
Assert.ok(
!bs.state.encryptionEnabled,
"State should indicate that encryption is now disabled."
);
Assert.ok(
!(await IOUtils.exists(
PathUtils.join(
testProfilePath,
BackupService.PROFILE_FOLDER_NAME,
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
)
)),
"Encryption state file should have been removed."
);
Assert.ok(
await backupServiceUsesPassword(bs, testProfilePath, null),
"BackupService should not use encryption."
);
await IOUtils.remove(testProfilePath, { recursive: true });
});
/**
* Tests that if encryption is enabled, that we can call
* BackupService.createBackup using an alternative ArchiveEncryptionState if
* we'd like.
*/
add_task(async function test_alternative_archive_encryption_state() {
let sandbox = sinon.createSandbox();
let bs = new BackupService({
FakeBackupResource1,
FakeBackupResource2,
FakeBackupResource3,
});
Assert.ok(
!bs.state.encryptionEnabled,
"State should initially indicate that encryption is disabled."
);
let testProfilePath = await IOUtils.createUniqueDirectory(
PathUtils.tempDir,
"testAlternativeArchiveEncryptionState"
);
let encState = await bs.loadEncryptionState(testProfilePath);
Assert.ok(!encState, "Should not find an ArchiveEncryptionState.");
// Now enable encryption.
let stateUpdatePromise = new Promise(resolve => {
bs.addEventListener("BackupService:StateUpdate", resolve, { once: true });
});
await bs.enableEncryption(TEST_PASSWORD, testProfilePath);
await stateUpdatePromise;
Assert.ok(
bs.state.encryptionEnabled,
"State should indicate that encryption is enabled."
);
Assert.ok(
await IOUtils.exists(
PathUtils.join(
testProfilePath,
BackupService.PROFILE_FOLDER_NAME,
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
)
),
"Encryption state file should exist."
);
// Override FakeBackupResource2 so that it requires encryption.
sandbox.stub(FakeBackupResource2, "requiresEncryption").get(() => {
return true;
});
Assert.ok(
FakeBackupResource2.requiresEncryption,
"FakeBackupResource2 requires encryption."
);
// This is how these FakeBackupResource's are defined in head.js
Assert.ok(
!FakeBackupResource1.requiresEncryption,
"FakeBackupResource1 does not require encryption."
);
Assert.ok(
!FakeBackupResource3.requiresEncryption,
"FakeBackupResource3 does not require encryption."
);
let allResourceBackupStubs = [
sandbox.stub(FakeBackupResource1.prototype, "backup").resolves(null),
sandbox.stub(FakeBackupResource3.prototype, "backup").resolves(null),
sandbox.stub(FakeBackupResource2.prototype, "backup").resolves(null),
];
let { instance: alternativeEncState } =
await ArchiveEncryptionState.initialize(TEST_PASSWORD_ALT);
let { archivePath } = await bs.createBackup({
profilePath: testProfilePath,
encState: alternativeEncState,
});
for (let resourceBackupStub of allResourceBackupStubs) {
Assert.ok(resourceBackupStub.calledOnce, "backup called on resource");
}
Assert.ok(
await IOUtils.exists(
PathUtils.join(
testProfilePath,
BackupService.PROFILE_FOLDER_NAME,
BackupService.ARCHIVE_ENCRYPTION_STATE_FILE
)
),
"Encryption state file should still exist."
);
Assert.ok(
await IOUtils.exists(archivePath),
"Encrypted archive was created."
);
const EXTRACTION_PATH = PathUtils.join(testProfilePath, "extraction.bin");
// Trying to decrypt with TEST_PASSWORD, which is the password we set up with
// the service, should actually fail, because the ArchiveEncryptionState we
// passed to createBackup should only decrypt with TEST_PASSWORD_ALT.
try {
await bs.extractCompressedSnapshotFromArchive(
archivePath,
EXTRACTION_PATH,
TEST_PASSWORD
);
Assert.ok(
false,
"Should have failed to decrypt backup with TEST_PASSWORD."
);
} catch (e) {
Assert.equal(
e.cause,
ERRORS.UNAUTHORIZED,
"TEST_PASSWORD failed to decrypt the backup."
);
}
// Using TEST_PASSWORD_ALT should work to decrypt the backup.
await bs.extractCompressedSnapshotFromArchive(
archivePath,
EXTRACTION_PATH,
TEST_PASSWORD_ALT
);
// Do a quick check to make sure that the archive was extracted.
Assert.ok(
await IOUtils.exists(PathUtils.join(EXTRACTION_PATH)),
"Found the extracted archive."
);
await maybeRemovePath(testProfilePath);
sandbox.restore();
});