tor-browser

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

commit 0439d9bfbab4c699b602fdb1ff5b54eabda98887
parent c3697d43c4b4a996f816bd27d2b90d8bb163ff1e
Author: Nicholas Rishel <nrishel@mozilla.com>
Date:   Thu,  9 Oct 2025 16:32:03 +0000

Bug 1991724 - Add ability to force disable profile backup and restore. r=cdupuis,sthompson

Differential Revision: https://phabricator.services.mozilla.com/D267327

Diffstat:
Mbrowser/components/backup/BackupService.sys.mjs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/backup/tests/xpcshell/test_BackupService_enabled.js | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/backup/tests/xpcshell/xpcshell.toml | 2++
Mtoolkit/components/nimbus/FeatureManifest.yaml | 10++++++++++
4 files changed, 223 insertions(+), 0 deletions(-)

diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs @@ -72,6 +72,7 @@ ChromeUtils.defineESModuleGetters(lazy, { FileUtils: "resource://gre/modules/FileUtils.sys.mjs", JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs", NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", UIState: "resource://services-sync/UIState.sys.mjs", }); @@ -594,6 +595,52 @@ export class BackupService extends EventTarget { static #errorRetries = 0; /** + * @typedef {object} EnabledStatus + * @property {boolean} enabled + * True if the feature is enabled. + * @property {string} [reason] + * Reason the feature is disabled if `enabled` is false. + */ + + /** + * Context for whether creating a backup archive is enabled. + * + * @type {EnabledStatus} + */ + get archiveEnabledStatus() { + // Check if disabled by Nimbus killswitch. + const archiveKillswitchTriggered = + lazy.NimbusFeatures.backupService.getVariable("archiveKillswitch"); + if (archiveKillswitchTriggered) { + return { + enabled: false, + reason: "Archiving a profile disabled remotely.", + }; + } + + return { enabled: true }; + } + + /** + * Context for whether restore from backup is enabled. + * + * @type {EnabledStatus} + */ + get restoreEnabledStatus() { + // Check if disabled by Nimbus killswitch. + const restoreKillswitchTriggered = + lazy.NimbusFeatures.backupService.getVariable("restoreKillswitch"); + if (restoreKillswitchTriggered) { + return { + enabled: false, + reason: "Restore from backup disabled remotely.", + }; + } + + return { enabled: true }; + } + + /** * Set to true if a backup is currently in progress. Causes stateUpdate() * to be called. * @@ -1228,6 +1275,12 @@ export class BackupService extends EventTarget { * created, or null if the backup failed. */ async createBackup({ profilePath = PathUtils.profileDir } = {}) { + const status = this.archiveEnabledStatus; + if (!status.enabled) { + lazy.logConsole.debug(status.reason); + return null; + } + // createBackup does not allow re-entry or concurrent backups. if (this.#backupInProgress) { lazy.logConsole.warn("Backup attempt already in progress"); @@ -2636,6 +2689,11 @@ export class BackupService extends EventTarget { profilePath = PathUtils.profileDir, profileRootPath = null ) { + const status = this.restoreEnabledStatus; + if (!status.enabled) { + throw new Error(status.reason); + } + // No concurrent recoveries. if (this.#_state.recoveryInProgress) { lazy.logConsole.warn("Recovery attempt already in progress"); diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_enabled.js b/browser/components/backup/tests/xpcshell/test_BackupService_enabled.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ExperimentAPI } = ChromeUtils.importESModule( + "resource://nimbus/ExperimentAPI.sys.mjs" +); +const { NimbusTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/NimbusTestUtils.sys.mjs" +); + +add_setup(async () => { + setupProfile(); + + NimbusTestUtils.init(this); + + const { cleanup: nimbusCleanup } = await NimbusTestUtils.setupTest(); + + await ExperimentAPI.ready(); + + let backupDir = await IOUtils.createUniqueDirectory( + PathUtils.profileDir, + "backup" + ); + + // Use temporary directory for backups. + Services.prefs.setStringPref("browser.backup.location", backupDir); + + registerCleanupFunction(async () => { + const nimbusCleanupPromise = nimbusCleanup(); + const backupDirCleanupPromise = IOUtils.remove(backupDir, { + recursive: true, + }); + + await Promise.all([nimbusCleanupPromise, backupDirCleanupPromise]); + + Services.prefs.clearUserPref("browser.backup.location"); + }); + + BackupService.init(); +}); + +add_task(async function test_archive_killswitch_enrollment() { + const bs = BackupService.get(); + + const cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig({ + featureId: "backupService", + value: { archiveKillswitch: true }, + }); + + Assert.ok( + !bs.archiveEnabledStatus.enabled, + "The backup service should report that archiving is disabled when the archive killswitch is active." + ); + + Assert.equal( + bs.archiveEnabledStatus.reason, + "Archiving a profile disabled remotely.", + "`archiveEnabledStatus` should report that it is disabled by the archive killswitch." + ); + + let backup = await bs.createBackup(); + Assert.ok( + !backup, + "Creating a backup should fail when the archive killswitch is active." + ); + + // End the experiment. + await cleanupExperiment(); + + Assert.ok( + bs.archiveEnabledStatus.enabled, + "The backup service should report that archiving is enabled once the archive killswitch experiment ends." + ); + + backup = await bs.createBackup(); + ok( + backup, + "Creating a backup should succeed once the archive killswitch experiment ends." + ); + ok( + await IOUtils.exists(backup.archivePath), + "Archive file should exist on disk." + ); +}); + +add_task(async function test_restore_killswitch_enrollment() { + const bs = BackupService.get(); + const backup = await bs.createBackup(); + + Assert.ok( + backup && backup.archivePath, + "Archive should have been created on disk." + ); + + let cleanupExperiment = await NimbusTestUtils.enrollWithFeatureConfig({ + featureId: "backupService", + value: { restoreKillswitch: true }, + }); + + const recoveryDir = await IOUtils.createUniqueDirectory( + PathUtils.profileDir, + "recovered-profiles" + ); + + Assert.ok( + !bs.restoreEnabledStatus.enabled, + "The backup service should report that restoring is disabled when the restore killswitch is active." + ); + + Assert.equal( + bs.restoreEnabledStatus.reason, + "Restore from backup disabled remotely.", + "`restoreEnabledStatus` should report that it is disabled by the restore killswitch." + ); + + await Assert.rejects( + bs.recoverFromBackupArchive( + backup.archivePath, + null, + false, + PathUtils.profileDir, + recoveryDir + ), + /Restore from backup disabled remotely\./, + "Recovery should throw when the restore killswitch is active." + ); + + // End the experiment. + await cleanupExperiment(); + + Assert.ok( + bs.restoreEnabledStatus.enabled, + "The backup service should report that restoring is enabled once the restore killswitch experiment ends." + ); + + let recoveredProfile = await bs.recoverFromBackupArchive( + backup.archivePath, + null, + false, + PathUtils.profileDir, + recoveryDir + ); + Assert.ok( + recoveredProfile, + "Recovery should succeed once the restore killswitch experiment ends." + ); + Assert.ok( + await IOUtils.exists(recoveredProfile.rootDir.path), + "Recovered profile directory should exist on disk." + ); +}); diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -35,6 +35,8 @@ support-files = [ ["test_BackupService_enable_disable_encryption.js"] +["test_BackupService_enabled.js"] + ["test_BackupService_finalizeSingleFileArchive.js"] ["test_BackupService_onedrive.js"] diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml @@ -4811,6 +4811,16 @@ backupService: setPref: branch: user pref: browser.backup.scheduled.minimum-time-between-backups-seconds + archiveKillswitch: + type: boolean + default: false + description: >- + Disables backup archiving, overrides all other configs when true. + restoreKillswitch: + type: boolean + default: false + description: >- + Disables backup restoration, overrides all other configs when true. pqcrypto: description: Prefs that control the use of post-quantum cryptography.