Bug 1393332 - Add infrastructure for importing payment methods from Chrome-based browsers r=mconley,fluent-reviewers,flod

This tries to retrieve the credit card data from Chrome-based browsers using
SQLite queries and then inserts them into the Firefox database.

Differential Revision: https://phabricator.services.mozilla.com/D168434
This commit is contained in:
Zach Harris 2023-05-16 19:12:21 +00:00
parent a50a2052fe
commit 8b433920dc
7 changed files with 308 additions and 1 deletions

View File

@ -131,7 +131,8 @@ export class ChromeProfileMigrator extends MigratorBase {
];
if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) {
possibleResourcePromises.push(
this._GetPasswordsResource(profileFolder)
this._GetPasswordsResource(profileFolder),
this._GetPaymentMethodsResource(profileFolder)
);
}
let possibleResources = await Promise.all(possibleResourcePromises);
@ -380,6 +381,84 @@ export class ChromeProfileMigrator extends MigratorBase {
},
};
}
async _GetPaymentMethodsResource(aProfileFolder) {
let paymentMethodsPath = PathUtils.join(aProfileFolder, "Web Data");
if (!(await IOUtils.exists(paymentMethodsPath))) {
return null;
}
let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
paymentMethodsPath,
"Chrome Credit Cards",
"SELECT name_on_card, card_number_encrypted, expiration_month, expiration_year FROM credit_cards"
).catch(ex => {
console.error(ex);
});
if (!rows?.length) {
return null;
}
let {
_chromeUserDataPathSuffix,
_keychainServiceName,
_keychainAccountName,
_keychainMockPassphrase = null,
} = this;
return {
type: MigrationUtils.resourceTypes.PAYMENT_METHODS,
async migrate(aCallback) {
let crypto;
try {
if (AppConstants.platform == "win") {
let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
"resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
);
crypto = new ChromeWindowsLoginCrypto(_chromeUserDataPathSuffix);
} else if (AppConstants.platform == "macosx") {
let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
"resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
);
crypto = new ChromeMacOSLoginCrypto(
_keychainServiceName,
_keychainAccountName,
_keychainMockPassphrase
);
} else {
aCallback(false);
return;
}
} catch (ex) {
// Handle the user canceling Keychain access or other OSCrypto errors.
console.error(ex);
aCallback(false);
return;
}
let cards = [];
for (let row of rows) {
cards.push({
"cc-name": row.getResultByName("name_on_card"),
"cc-number": await crypto.decryptData(
row.getResultByName("card_number_encrypted"),
null
),
"cc-exp-month": parseInt(
row.getResultByName("expiration_month"),
10
),
"cc-exp-year": parseInt(row.getResultByName("expiration_year"), 10),
});
}
await MigrationUtils.insertCreditCardsWrapper(cards);
aCallback(true);
},
};
}
}
async function GetBookmarksResource(aProfileFolder, aBrowserKey) {

View File

@ -139,6 +139,7 @@ class MigrationUtils {
BOOKMARKS: 0x0020,
OTHERDATA: 0x0040,
SESSION: 0x0080,
PAYMENT_METHODS: 0x0100,
});
/**
@ -767,6 +768,7 @@ class MigrationUtils {
bookmarks: 0,
logins: 0,
history: 0,
cards: 0,
};
getImportedCount(type) {
@ -895,6 +897,18 @@ class MigrationUtils {
}
}
async insertCreditCardsWrapper(cards) {
this._importQuantities.cards += cards.length;
let { formAutofillStorage } = ChromeUtils.import(
"resource://autofill/FormAutofillStorage.jsm"
);
await formAutofillStorage.initialize();
for (let card of cards) {
await formAutofillStorage.creditCards.add(card);
}
}
initializeUndoData() {
gKeepUndoData = true;
gUndoData = new Map([

View File

@ -29,6 +29,7 @@ const kDataToStringMap = new Map([
["bookmarks", "browser-data-bookmarks"],
["otherdata", "browser-data-otherdata"],
["session", "browser-data-session"],
["payment_methods", "browser-data-payment-methods"],
]);
var MigrationWizard = {

View File

@ -0,0 +1,205 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/* global structuredClone */
const PROFILE = {
id: "Default",
name: "Default",
};
const PAYMENT_METHODS = [
{
name_on_card: "Name Name",
card_number: "4532962432748929", // Visa
expiration_month: 3,
expiration_year: 2027,
},
{
name_on_card: "Name Name Name",
card_number: "5359908373796416", // Mastercard
expiration_month: 5,
expiration_year: 2028,
},
{
name_on_card: "Name",
card_number: "346624461807588", // AMEX
expiration_month: 4,
expiration_year: 2026,
},
];
let OSKeyStoreTestUtils;
add_setup(async function os_key_store_setup() {
({ OSKeyStoreTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
));
OSKeyStoreTestUtils.setup();
registerCleanupFunction(async function cleanup() {
await OSKeyStoreTestUtils.cleanup();
});
});
let rootDir = do_get_file("chromefiles/", true);
function checkCardsAreEqual(importedCard, testCard, id) {
const CC_NUMBER_RE = /^(\*+)(.{4})$/;
Assert.equal(
importedCard["cc-name"],
testCard.name_on_card,
"The two logins ID " + id + " have the same name on card"
);
let matches = CC_NUMBER_RE.exec(importedCard["cc-number"]);
Assert.notEqual(matches, null);
Assert.equal(importedCard["cc-number"].length, testCard.card_number.length);
Assert.equal(testCard.card_number.endsWith(matches[2]), true);
Assert.notEqual(importedCard["cc-number-encrypted"], "");
Assert.equal(
importedCard["cc-exp-month"],
testCard.expiration_month,
"The two logins ID " + id + " have the same expiration month"
);
Assert.equal(
importedCard["cc-exp-year"],
testCard.expiration_year,
"The two logins ID " + id + " have the same expiration year"
);
}
add_task(async function setup_fakePaths() {
let pathId;
if (AppConstants.platform == "macosx") {
pathId = "ULibDir";
} else if (AppConstants.platform == "win") {
pathId = "LocalAppData";
} else {
pathId = "Home";
}
registerFakePath(pathId, rootDir);
});
add_task(async function test_credit_cards() {
let loginCrypto;
let profilePathSegments;
let mockMacOSKeychain = {
passphrase: "bW96aWxsYWZpcmVmb3g=",
serviceName: "TESTING Chrome Safe Storage",
accountName: "TESTING Chrome",
};
if (AppConstants.platform == "macosx") {
let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
"resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
);
loginCrypto = new ChromeMacOSLoginCrypto(
mockMacOSKeychain.serviceName,
mockMacOSKeychain.accountName,
mockMacOSKeychain.passphrase
);
profilePathSegments = [
"Application Support",
"Google",
"Chrome",
"Default",
];
} else if (AppConstants.platform == "win") {
let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
"resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
);
loginCrypto = new ChromeWindowsLoginCrypto("Chrome");
profilePathSegments = ["Google", "Chrome", "User Data", "Default"];
} else {
throw new Error("Not implemented");
}
let target = rootDir.clone();
let defaultFolderPath = PathUtils.join(target.path, ...profilePathSegments);
let webDataPath = PathUtils.join(defaultFolderPath, "Web Data");
let localStatePath = defaultFolderPath.replace("Default", "");
await IOUtils.makeDirectory(defaultFolderPath, {
createAncestor: true,
ignoreExisting: true,
});
// Copy Web Data database into Default profile
const sourcePathWebData = do_get_file(
"AppData/Local/Google/Chrome/User Data/Default/Web Data"
).path;
await IOUtils.copy(sourcePathWebData, webDataPath);
const sourcePathLocalState = do_get_file(
"AppData/Local/Google/Chrome/User Data/Local State"
).path;
await IOUtils.copy(sourcePathLocalState, localStatePath);
let dbConn = await Sqlite.openConnection({ path: webDataPath });
for (let card of PAYMENT_METHODS) {
let encryptedCardNumber = await loginCrypto.encryptData(card.card_number);
let cardNumberEncryptedValue = new Uint8Array(
loginCrypto.stringToArray(encryptedCardNumber)
);
let cardCopy = structuredClone(card);
cardCopy.card_number_encrypted = cardNumberEncryptedValue;
delete cardCopy.card_number;
await dbConn.execute(
`INSERT INTO credit_cards
(name_on_card, card_number_encrypted, expiration_month, expiration_year)
VALUES (:name_on_card, :card_number_encrypted, :expiration_month, :expiration_year)
`,
cardCopy
);
}
await dbConn.close();
let migrator = await MigrationUtils.getMigrator("chrome");
if (AppConstants.platform == "macosx") {
Object.assign(migrator, {
_keychainServiceName: mockMacOSKeychain.serviceName,
_keychainAccountName: mockMacOSKeychain.accountName,
_keychainMockPassphrase: mockMacOSKeychain.passphrase,
});
}
Assert.ok(
await migrator.isSourceAvailable(),
"Sanity check the source exists"
);
let { formAutofillStorage } = ChromeUtils.importESModule(
"resource://autofill/FormAutofillStorage.sys.mjs"
);
await formAutofillStorage.initialize();
await promiseMigration(
migrator,
MigrationUtils.resourceTypes.PAYMENT_METHODS,
PROFILE
);
let cards = await formAutofillStorage.creditCards.getAll();
Assert.equal(
cards.length,
PAYMENT_METHODS.length,
"Check there are still the same number of credit cards after re-importing the data"
);
Assert.equal(
cards.length,
MigrationUtils._importQuantities.cards,
"Check telemetry matches the actual import."
);
for (let i = 0; i < PAYMENT_METHODS.length; i++) {
checkCardsAreEqual(cards[i], PAYMENT_METHODS[i], i + 1);
}
});

View File

@ -16,6 +16,9 @@ run-if = os == "win"
[test_ChromeMigrationUtils_path_chromium_snap.js]
run-if = os == "linux"
[test_Chrome_bookmarks.js]
[test_Chrome_credit_cards.js]
skip-if = os != "win" # Disabled on macOS for perma failures (bug 1833460)
condprof # bug 1769154 - not realistic for condprof
[test_Chrome_formdata.js]
[test_Chrome_history.js]
skip-if = os != "mac" # Relies on ULibDir

View File

@ -163,3 +163,8 @@ browser-data-session-checkbox =
.label = Windows and Tabs
browser-data-session-label =
.value = Windows and Tabs
browser-data-payment-methods-checkbox =
.label = Payment methods
browser-data-payment-methods-label =
.value = Payment methods