tor-browser

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

commit 28805b4930b0b589b5c65f4504ed532c64b97524
parent 95f27bbefc2a4ba5ddfb794600218310125bedcd
Author: Niklas Baumgardner <nbaumgardner@mozilla.com>
Date:   Tue, 25 Nov 2025 20:05:01 +0000

Bug 1998890 - Copy profile should copy requiresEncryption=true data. r=profiles-reviewers,fchasen,jhirsch

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

Diffstat:
Mbrowser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs | 27+++++++++++++++++++--------
Mbrowser/components/profiles/SelectableProfile.sys.mjs | 21+++++++++++++++++++--
Mbrowser/components/profiles/tests/unit/test_copy_profile.js | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 102 insertions(+), 12 deletions(-)

diff --git a/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs @@ -92,16 +92,27 @@ export class CredentialsAndSecurityBackupResource extends BackupResource { for (let creditCard of autofillRecords.creditCards) { let oldEncryptedCard = creditCard["cc-number-encrypted"]; if (oldEncryptedCard) { + let plaintextCard; // We use the native OSKeyStore backend to decrypt the bytes with the // original secret in order to skip authentication dialogs. - let plaintextCardBytes = await lazy.nativeOSKeyStore.asyncDecryptBytes( - lazy.BackupService.RECOVERY_OSKEYSTORE_LABEL, - oldEncryptedCard - ); - let plaintextCard = String.fromCharCode.apply( - String, - plaintextCardBytes - ); + if ( + await lazy.nativeOSKeyStore.asyncSecretAvailable( + lazy.BackupService.RECOVERY_OSKEYSTORE_LABEL + ) + ) { + let plaintextCardBytes = + await lazy.nativeOSKeyStore.asyncDecryptBytes( + lazy.BackupService.RECOVERY_OSKEYSTORE_LABEL, + oldEncryptedCard + ); + plaintextCard = String.fromCharCode.apply(String, plaintextCardBytes); + } else { + plaintextCard = await lazy.OSKeyStore.decrypt( + oldEncryptedCard, + "backup_cc" + ); + } + // We're accessing the "real" OSKeyStore for this device here, and // encrypting the card with it. let newEncryptedCard = await lazy.OSKeyStore.encrypt(plaintextCard); diff --git a/browser/components/profiles/SelectableProfile.sys.mjs b/browser/components/profiles/SelectableProfile.sys.mjs @@ -479,7 +479,20 @@ export class SelectableProfile { // We set the pref here so the copied profile will inherit this pref and // the copied profile will not show the backup welcome messaging. Services.prefs.setBoolPref("browser.profiles.profile-copied", true); - const backupServiceInstance = BackupService.init(); + const backupServiceInstance = new BackupService(); + + let encState = await backupServiceInstance.loadEncryptionState(this.path); + let createdEncState = false; + if (!encState) { + // If we don't have encryption enabled, temporarily create encryption so + // we can copy resources that require encryption + await backupServiceInstance.enableEncryption( + Services.uuid.generateUUID().toString().slice(1, -1), + this.path + ); + encState = await backupServiceInstance.loadEncryptionState(this.path); + createdEncState = true; + } let result = await backupServiceInstance.createAndPopulateStagingFolder( this.path ); @@ -495,10 +508,14 @@ export class SelectableProfile { await backupServiceInstance.recoverFromSnapshotFolderIntoSelectableProfile( result.stagingPath, true, // shouldLaunch - null, // encState + encState, // encState this // copiedProfile ); + if (createdEncState) { + await backupServiceInstance.disableEncryption(this.path); + } + copiedProfile.theme = this.theme; await copiedProfile.setAvatar(this.avatar); diff --git a/browser/components/profiles/tests/unit/test_copy_profile.js b/browser/components/profiles/tests/unit/test_copy_profile.js @@ -6,6 +6,9 @@ https://creativecommons.org/publicdomain/zero/1.0/ */ const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" ); +const { BackupService } = ChromeUtils.importESModule( + "resource:///modules/backup/BackupService.sys.mjs" +); const execProcess = sinon.fake(); @@ -15,7 +18,7 @@ add_setup(async () => { sinon.replace(getSelectableProfileService(), "execProcess", execProcess); }); -add_task(async function test_create_profile() { +add_task(async function test_copy_profile() { startProfileService(); const SelectableProfileService = getSelectableProfileService(); @@ -26,12 +29,23 @@ add_task(async function test_create_profile() { Assert.ok(SelectableProfileService.isEnabled, "Service should be enabled"); let profiles = await SelectableProfileService.getAllProfiles(); - Assert.equal(profiles.length, 1, "Only one selectable profile exist"); + const backupServiceInstance = new BackupService(); + + let encState = await backupServiceInstance.loadEncryptionState( + SelectableProfileService.currentProfile.path + ); + Assert.ok(!encState, "No encryption state before copyProfile called"); + let copiedProfile = await SelectableProfileService.currentProfile.copyProfile(); + encState = await backupServiceInstance.loadEncryptionState( + SelectableProfileService.currentProfile.path + ); + Assert.ok(!encState, "No encryption state after copyProfile called"); + profiles = await SelectableProfileService.getAllProfiles(); Assert.equal(profiles.length, 2, "Two selectable profiles exist"); @@ -47,3 +61,51 @@ add_task(async function test_create_profile() { "Copied profile has the same theme" ); }); + +add_task(async function test_copy_profile_with_encryption() { + startProfileService(); + + const SelectableProfileService = getSelectableProfileService(); + const ProfilesDatastoreService = getProfilesDatastoreService(); + + await ProfilesDatastoreService.init(); + await SelectableProfileService.init(); + Assert.ok(SelectableProfileService.isEnabled, "Service should be enabled"); + + let profiles = await SelectableProfileService.getAllProfiles(); + Assert.equal(profiles.length, 2, "Only two selectable profiles exist"); + + const backupServiceInstance = new BackupService(); + await backupServiceInstance.enableEncryption( + "testCopyProfile", + SelectableProfileService.currentProfile.path.path + ); + + let encState = await backupServiceInstance.loadEncryptionState( + SelectableProfileService.currentProfile.path + ); + Assert.ok(encState, "Encryption state exists before copyProfile called"); + + let copiedProfile = + await SelectableProfileService.currentProfile.copyProfile(); + + encState = await backupServiceInstance.loadEncryptionState( + SelectableProfileService.currentProfile.path + ); + Assert.ok(encState, "Encryption state exists after copyProfile called"); + + profiles = await SelectableProfileService.getAllProfiles(); + Assert.equal(profiles.length, 3, "Three selectable profiles exist"); + + Assert.equal( + copiedProfile.avatar, + SelectableProfileService.currentProfile.avatar, + "Copied profile has the same avatar" + ); + + Assert.equal( + copiedProfile.theme.themeId, + SelectableProfileService.currentProfile.theme.themeId, + "Copied profile has the same theme" + ); +});