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:
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"
+ );
+});