732 lines
22 KiB
JavaScript
732 lines
22 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const lazy = {};
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
|
|
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
|
|
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
|
|
OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
|
|
});
|
|
|
|
class FormSection {
|
|
static ADDRESS = "address";
|
|
static CREDIT_CARD = "creditCard";
|
|
|
|
#fieldDetails = [];
|
|
|
|
#name = "";
|
|
|
|
constructor(fieldDetails) {
|
|
if (!fieldDetails.length) {
|
|
throw new TypeError("A section should contain at least one field");
|
|
}
|
|
|
|
fieldDetails.forEach(field => this.addField(field));
|
|
|
|
for (const fieldDetail of fieldDetails) {
|
|
if (lazy.FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
|
|
this.type = FormSection.ADDRESS;
|
|
break;
|
|
} else if (
|
|
lazy.FormAutofillUtils.isCreditCardField(fieldDetail.fieldName)
|
|
) {
|
|
this.type = FormSection.CREDIT_CARD;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.type ||= FormSection.ADDRESS;
|
|
}
|
|
|
|
get fieldDetails() {
|
|
return this.#fieldDetails;
|
|
}
|
|
|
|
get name() {
|
|
return this.#name;
|
|
}
|
|
|
|
addField(fieldDetail) {
|
|
this.#name ||= fieldDetail.sectionName;
|
|
this.#fieldDetails.push(fieldDetail);
|
|
}
|
|
}
|
|
|
|
export class FormAutofillSection {
|
|
/**
|
|
* Record information for fields that are in this section
|
|
*/
|
|
#fieldDetails = [];
|
|
|
|
constructor(fieldDetails) {
|
|
this.#fieldDetails = fieldDetails;
|
|
|
|
ChromeUtils.defineLazyGetter(this, "log", () =>
|
|
lazy.FormAutofill.defineLogGetter(this, "FormAutofillSection")
|
|
);
|
|
|
|
// Identifier used to correlate events relating to the same form
|
|
this.flowId = Services.uuid.generateUUID().toString();
|
|
this.log.debug(
|
|
"Creating new credit card section with flowId =",
|
|
this.flowId
|
|
);
|
|
}
|
|
|
|
get fieldDetails() {
|
|
return this.#fieldDetails;
|
|
}
|
|
|
|
get allFieldNames() {
|
|
return this.fieldDetails.map(field => field.fieldName);
|
|
}
|
|
|
|
/*
|
|
* Examine the section is a valid section or not based on its fieldDetails or
|
|
* other information. This method must be overrided.
|
|
*
|
|
* @returns {boolean} True for a valid section, otherwise false
|
|
*
|
|
*/
|
|
isValidSection() {
|
|
throw new TypeError("isValidSection method must be overrided");
|
|
}
|
|
|
|
/*
|
|
* Examine the section is an enabled section type or not based on its
|
|
* preferences. This method must be overrided.
|
|
*
|
|
* @returns {boolean} True for an enabled section type, otherwise false
|
|
*
|
|
*/
|
|
isEnabled() {
|
|
throw new TypeError("isEnabled method must be overrided");
|
|
}
|
|
|
|
/*
|
|
* Examine the section is createable for storing the profile. This method
|
|
* must be overrided.
|
|
*
|
|
* @param {Object} _record The record for examining createable
|
|
* @returns {boolean} True for the record is createable, otherwise false
|
|
*
|
|
*/
|
|
isRecordCreatable(_record) {
|
|
throw new TypeError("isRecordCreatable method must be overridden");
|
|
}
|
|
|
|
/**
|
|
* Override this method if the profile is needed to be customized for
|
|
* previewing values.
|
|
*
|
|
* @param {object} _profile
|
|
* A profile for pre-processing before previewing values.
|
|
* @returns {boolean} Whether the profile should be previewed.
|
|
*/
|
|
preparePreviewProfile(_profile) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Override this method if the profile is needed to be customized for filling
|
|
* values.
|
|
*
|
|
* @param {object} _profile
|
|
* A profile for pre-processing before filling values.
|
|
* @returns {boolean} Whether the profile should be filled.
|
|
*/
|
|
async prepareFillingProfile(_profile) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The result is an array contains the sections with its belonging field details.
|
|
*
|
|
* @param {Array<FieldDetails>} fieldDetails field detail array to be classified
|
|
* @param {object} options
|
|
* @param {boolean} [options.ignoreInvalidSection = false]
|
|
* True to keep invalid section in the return array. Only used by tests now
|
|
* @param {boolean} [options.ignoreUnknownField = true]
|
|
* False to keep unknown field in a section. Only used by developer tools now
|
|
* @returns {Array<FormSection>} The array with the sections.
|
|
*/
|
|
static classifySections(
|
|
fieldDetails,
|
|
{ ignoreInvalidSection = false, ignoreUnknownField = true } = {}
|
|
) {
|
|
const addressFields = [];
|
|
const creditCardFields = [];
|
|
|
|
// 'current' refers to the last list where an field was added to.
|
|
// It helps determine the appropriate list for unknown fields, defaulting to the address
|
|
// field list for simplicity
|
|
let current = addressFields;
|
|
for (const fieldDetail of fieldDetails) {
|
|
if (lazy.FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
|
|
current = addressFields;
|
|
} else if (
|
|
lazy.FormAutofillUtils.isCreditCardField(fieldDetail.fieldName)
|
|
) {
|
|
current = creditCardFields;
|
|
} else if (ignoreUnknownField) {
|
|
continue;
|
|
}
|
|
current.push(fieldDetail);
|
|
}
|
|
|
|
const addressSections = FormAutofillSection.groupFields(addressFields);
|
|
const creditCardSections =
|
|
FormAutofillSection.groupFields(creditCardFields);
|
|
|
|
const sections = [...addressSections, ...creditCardSections].sort(
|
|
(a, b) =>
|
|
fieldDetails.indexOf(a.fieldDetails[0]) -
|
|
fieldDetails.indexOf(b.fieldDetails[0])
|
|
);
|
|
|
|
const autofillableSections = [];
|
|
for (const section of sections) {
|
|
if (!section.fieldDetails.length) {
|
|
continue;
|
|
}
|
|
|
|
const autofillableSection =
|
|
section.type == FormSection.ADDRESS
|
|
? new FormAutofillAddressSection(section.fieldDetails)
|
|
: new FormAutofillCreditCardSection(section.fieldDetails);
|
|
|
|
if (ignoreInvalidSection && !autofillableSection.isValidSection()) {
|
|
continue;
|
|
}
|
|
|
|
autofillableSections.push(autofillableSection);
|
|
}
|
|
return autofillableSections;
|
|
}
|
|
|
|
/**
|
|
* Groups fields into sections based on:
|
|
* 1. Their `sectionName` attribute.
|
|
* 2. Whether the section already contains a field with the same `fieldName`,
|
|
* If so, a new section is created.
|
|
*
|
|
* @param {Array} fieldDetails An array of field detail objects.
|
|
* @returns {Array} An array of FormSection objects.
|
|
*/
|
|
static groupFields(fieldDetails) {
|
|
let sections = [];
|
|
for (let i = 0; i < fieldDetails.length; i++) {
|
|
const cur = fieldDetails[i];
|
|
const [currentSection] = sections.slice(-1);
|
|
|
|
// The section this field might be placed into.
|
|
let candidateSection = null;
|
|
|
|
// Use name group from autocomplete attribute (ex, section-xxx) to look for the section
|
|
// we might place this field into.
|
|
// If the field doesn't have a section name, the candidate section is the previous section.
|
|
if (!currentSection || !cur.sectionName) {
|
|
candidateSection = currentSection;
|
|
} else if (cur.sectionName) {
|
|
// If the field has a section name, the candidate section is the nearest section that
|
|
// either shares the same name or lacks a name.
|
|
for (let idx = sections.length - 1; idx >= 0; idx--) {
|
|
if (!sections[idx].name || sections[idx].name == cur.sectionName) {
|
|
candidateSection = sections[idx];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (candidateSection) {
|
|
// The field will still be placed in a new section if it is a duplicate of
|
|
// an existing field, unless it is a duplicate of the previous field. This
|
|
// allows for fields that might commonly appear twice such as a verification
|
|
// email field, an invisible field that appears next to the user-visible field,
|
|
// and simple cases where a page error where a field name is reused twice.
|
|
let dupIndex = candidateSection.fieldDetails.findIndex(
|
|
f =>
|
|
f.fieldName == cur.fieldName &&
|
|
f.isVisible &&
|
|
cur.isVisible &&
|
|
!f.isLookup
|
|
);
|
|
let isDuplicate = dupIndex != -1;
|
|
|
|
if (isDuplicate) {
|
|
const [last] = candidateSection.fieldDetails.slice(-1);
|
|
if (last.fieldName == cur.fieldName) {
|
|
isDuplicate = false;
|
|
} else if (
|
|
lazy.FormAutofillUtils.getCategoryFromFieldName(cur.fieldName) ==
|
|
"name"
|
|
) {
|
|
// If the duplicate field is in the "name" category (e.g., family-name, given-name),
|
|
// we check whether all fields starting from the first duplicate also belong to the
|
|
// name category. If they do, we don't consider the field a duplicate, since name
|
|
// fields often appear in groups like family-name + given-name.
|
|
isDuplicate = !candidateSection.fieldDetails
|
|
.slice(dupIndex)
|
|
.every(
|
|
f =>
|
|
lazy.FormAutofillUtils.getCategoryFromFieldName(
|
|
f.fieldName
|
|
) === "name"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!isDuplicate) {
|
|
candidateSection.addField(cur);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Create a new section
|
|
sections.push(new FormSection([cur]));
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
/**
|
|
* Return the record that is converted from the element's value.
|
|
* The `valueByElementId` is passed by the child process.
|
|
*
|
|
* @returns {object} object keyed by field name, and values are field values.
|
|
*/
|
|
createRecord(formFilledData) {
|
|
if (!this.fieldDetails.length) {
|
|
return {};
|
|
}
|
|
|
|
const data = {
|
|
flowId: this.flowId,
|
|
record: {},
|
|
};
|
|
|
|
for (const detail of this.fieldDetails) {
|
|
// Do not save security code.
|
|
if (detail.fieldName == "cc-csc") {
|
|
continue;
|
|
}
|
|
const { filledValue } = formFilledData.get(detail.elementId) ?? {};
|
|
|
|
if (
|
|
!filledValue ||
|
|
filledValue.length > lazy.FormAutofillUtils.MAX_FIELD_VALUE_LENGTH
|
|
) {
|
|
// Keep the property and preserve more information for updating
|
|
data.record[detail.fieldName] = "";
|
|
} else if (detail.part > 1) {
|
|
// If there are multiple parts for the same field, concatenate the values.
|
|
// This is now used in cases where the credit card number field
|
|
// is split into multiple fields.
|
|
data.record[detail.fieldName] += filledValue;
|
|
} else {
|
|
data.record[detail.fieldName] = filledValue;
|
|
}
|
|
}
|
|
|
|
if (!this.isRecordCreatable(data.record)) {
|
|
return null;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
shouldAutofillField(fieldDetail) {
|
|
// We don't save security code, but if somehow the profile has securty code,
|
|
// make sure we don't autofill it.
|
|
if (fieldDetail.fieldName == "cc-csc") {
|
|
return false;
|
|
}
|
|
|
|
// When both visible and invisible elements exist, we only autofill the
|
|
// visible element.
|
|
if (!fieldDetail.isVisible) {
|
|
return !this.fieldDetails.some(
|
|
field => field.fieldName == fieldDetail.fieldName && field.isVisible
|
|
);
|
|
}
|
|
|
|
// Only fill a street address lookup field if it is the only street
|
|
// address related field in this section. Similarly, for postal code
|
|
// fields.
|
|
if (fieldDetail.isLookup) {
|
|
const STREET_FIELDS = [
|
|
"street-address",
|
|
"address-line1",
|
|
"address-line2",
|
|
"address-line3",
|
|
];
|
|
|
|
let INTERESTED_FIELDS = [];
|
|
if (STREET_FIELDS.includes(fieldDetail.fieldName)) {
|
|
INTERESTED_FIELDS = STREET_FIELDS;
|
|
} else if (fieldDetail.fieldName == "postal-code") {
|
|
INTERESTED_FIELDS = ["postal-code"];
|
|
}
|
|
|
|
if (
|
|
INTERESTED_FIELDS.length &&
|
|
this.fieldDetails.some(
|
|
field =>
|
|
INTERESTED_FIELDS.includes(field.fieldName) &&
|
|
field.isVisible &&
|
|
!field.isLookup
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Heuristics to determine which fields to autofill when a section contains
|
|
* multiple fields of the same type.
|
|
*/
|
|
getAutofillFields() {
|
|
return this.fieldDetails.filter(fieldDetail =>
|
|
this.shouldAutofillField(fieldDetail)
|
|
);
|
|
}
|
|
|
|
/*
|
|
* For telemetry
|
|
*/
|
|
onDetected() {
|
|
if (!this.isValidSection()) {
|
|
return;
|
|
}
|
|
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"detected",
|
|
this.flowId,
|
|
this.fieldDetails
|
|
);
|
|
}
|
|
|
|
onPopupOpened(elementId) {
|
|
const fieldDetail = this.getFieldDetailByElementId(elementId);
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"popup_shown",
|
|
this.flowId,
|
|
[fieldDetail]
|
|
);
|
|
}
|
|
|
|
onFilled(filledResult) {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"filled",
|
|
this.flowId,
|
|
this.fieldDetails,
|
|
filledResult
|
|
);
|
|
}
|
|
|
|
onFilledOnFieldsUpdate(filledResult) {
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"filled_on_fields_update",
|
|
this.flowId,
|
|
this.fieldDetails,
|
|
filledResult
|
|
);
|
|
}
|
|
|
|
onFilledModified(elementId) {
|
|
const fieldDetail = this.getFieldDetailByElementId(elementId);
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"filled_modified",
|
|
this.flowId,
|
|
[fieldDetail]
|
|
);
|
|
}
|
|
|
|
onSubmitted(formFilledData) {
|
|
this.submitted = true;
|
|
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent(
|
|
"submitted",
|
|
this.flowId,
|
|
this.fieldDetails,
|
|
formFilledData
|
|
);
|
|
}
|
|
|
|
onCleared(elementId) {
|
|
const fieldDetail = this.getFieldDetailByElementId(elementId);
|
|
lazy.AutofillTelemetry.recordFormInteractionEvent("cleared", this.flowId, [
|
|
fieldDetail,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Utility functions
|
|
*/
|
|
getFieldDetailByElementId(elementId) {
|
|
return this.fieldDetails.find(detail => detail.elementId == elementId);
|
|
}
|
|
|
|
/**
|
|
* Groups an array of field details by their browsing context IDs.
|
|
*
|
|
* @param {Array} fieldDetails
|
|
* Array of fieldDetails object
|
|
*
|
|
* @returns {object}
|
|
* An object keyed by BrowsingContext Id, value is an array that
|
|
* contains all fieldDetails with the same BrowsingContext id.
|
|
*/
|
|
static groupFieldDetailsByBrowsingContext(fieldDetails) {
|
|
const detailsByBC = {};
|
|
for (const fieldDetail of fieldDetails) {
|
|
const bcid = fieldDetail.browsingContextId;
|
|
if (detailsByBC[bcid]) {
|
|
detailsByBC[bcid].push(fieldDetail);
|
|
} else {
|
|
detailsByBC[bcid] = [fieldDetail];
|
|
}
|
|
}
|
|
return detailsByBC;
|
|
}
|
|
}
|
|
|
|
export class FormAutofillAddressSection extends FormAutofillSection {
|
|
isValidSection() {
|
|
const fields = new Set(this.fieldDetails.map(f => f.fieldName));
|
|
return fields.size >= lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
|
|
}
|
|
|
|
isEnabled() {
|
|
return lazy.FormAutofill.isAutofillAddressesEnabled;
|
|
}
|
|
|
|
isRecordCreatable(record) {
|
|
const country = lazy.FormAutofillUtils.identifyCountryCode(
|
|
record.country || record["country-name"]
|
|
);
|
|
if (
|
|
country &&
|
|
!lazy.FormAutofill.isAutofillAddressesAvailableInCountry(country)
|
|
) {
|
|
// We don't want to save data in the wrong fields due to not having proper
|
|
// heuristic regexes in countries we don't yet support.
|
|
this.log.warn(
|
|
"isRecordCreatable: Country not supported:",
|
|
record.country
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Multiple name or tel fields are treat as 1 field while countng whether
|
|
// the number of fields exceed the valid address secton threshold
|
|
const categories = Object.entries(record)
|
|
.filter(e => !!e[1])
|
|
.map(e => lazy.FormAutofillUtils.getCategoryFromFieldName(e[0]));
|
|
|
|
return (
|
|
categories.reduce(
|
|
(acc, category) =>
|
|
["name", "tel"].includes(category) && acc.includes(category)
|
|
? acc
|
|
: [...acc, category],
|
|
[]
|
|
).length >= lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
|
|
);
|
|
}
|
|
}
|
|
|
|
export class FormAutofillCreditCardSection extends FormAutofillSection {
|
|
/**
|
|
* Determine whether a set of cc fields identified by our heuristics form a
|
|
* valid credit card section.
|
|
* There are 4 different cases when a field is considered a credit card field
|
|
* 1. Identified by autocomplete attribute. ex <input autocomplete="cc-number">
|
|
* 2. Identified by fathom and fathom is pretty confident (when confidence
|
|
* value is higher than `highConfidenceThreshold`)
|
|
* 3. Identified by fathom. Confidence value is between `fathom.confidenceThreshold`
|
|
* and `fathom.highConfidenceThreshold`
|
|
* 4. Identified by regex-based heurstic. There is no confidence value in thise case.
|
|
*
|
|
* A form is considered a valid credit card form when one of the following condition
|
|
* is met:
|
|
* A. One of the cc field is identified by autocomplete (case 1)
|
|
* B. One of the cc field is identified by fathom (case 2 or 3), and there is also
|
|
* another cc field found by any of our heuristic (case 2, 3, or 4)
|
|
* C. Only one cc field is found in the section, but fathom is very confident (Case 2).
|
|
* Currently we add an extra restriction to this rule to decrease the false-positive
|
|
* rate. See comments below for details.
|
|
*
|
|
* @returns {boolean} True for a valid section, otherwise false
|
|
*/
|
|
isValidSection() {
|
|
let ccNumberDetail = null;
|
|
let ccNameDetail = null;
|
|
let ccExpiryDetail = null;
|
|
|
|
for (let detail of this.fieldDetails) {
|
|
switch (detail.fieldName) {
|
|
case "cc-number":
|
|
ccNumberDetail = detail;
|
|
break;
|
|
case "cc-name":
|
|
case "cc-given-name":
|
|
case "cc-additional-name":
|
|
case "cc-family-name":
|
|
ccNameDetail = detail;
|
|
break;
|
|
case "cc-exp":
|
|
case "cc-exp-month":
|
|
case "cc-exp-year":
|
|
ccExpiryDetail = detail;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Condition A. Always trust autocomplete attribute. A section is considered a valid
|
|
// cc section as long as a field has autocomplete=cc-number, cc-name or cc-exp*
|
|
if (
|
|
ccNumberDetail?.reason == "autocomplete" ||
|
|
ccNameDetail?.reason == "autocomplete" ||
|
|
ccExpiryDetail?.reason == "autocomplete"
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// Condition B. One of the field is identified by fathom, if this section also
|
|
// contains another cc field found by our heuristic (Case 2, 3, or 4), we consider
|
|
// this section a valid credit card seciton
|
|
if (ccNumberDetail?.reason == "fathom") {
|
|
if (ccNameDetail || ccExpiryDetail) {
|
|
return true;
|
|
}
|
|
} else if (ccNameDetail?.reason == "fathom") {
|
|
if (ccNumberDetail || ccExpiryDetail) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Condition C.
|
|
if (
|
|
ccNumberDetail?.isOnlyVisibleFieldWithHighConfidence ||
|
|
ccNameDetail?.isOnlyVisibleFieldWithHighConfidence
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
isEnabled() {
|
|
return lazy.FormAutofill.isAutofillCreditCardsEnabled;
|
|
}
|
|
|
|
isRecordCreatable(record) {
|
|
return (
|
|
record["cc-number"] &&
|
|
lazy.FormAutofillUtils.isCCNumber(record["cc-number"])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Customize for previewing profile
|
|
*
|
|
* @param {object} profile
|
|
* A profile for pre-processing before previewing values.
|
|
* @returns {boolean} Whether the profile should be filled.
|
|
* @override
|
|
*/
|
|
preparePreviewProfile(profile) {
|
|
if (!profile) {
|
|
return true;
|
|
}
|
|
|
|
// Always show the decrypted credit card number when Master Password is
|
|
// disabled.
|
|
if (profile["cc-number-decrypted"]) {
|
|
profile["cc-number"] = profile["cc-number-decrypted"];
|
|
} else if (!profile["cc-number"].startsWith("****")) {
|
|
// Show the previewed credit card as "**** 4444" which is
|
|
// needed when a credit card number field has a maxlength of four.
|
|
profile["cc-number"] = "****" + profile["cc-number"];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Customize for filling profile
|
|
*
|
|
* @param {object} profile
|
|
* A profile for pre-processing before filling values.
|
|
* @returns {boolean} Whether the profile should be filled.
|
|
* @override
|
|
*/
|
|
async prepareFillingProfile(profile) {
|
|
// Prompt the OS login dialog to get the decrypted credit card number.
|
|
if (profile["cc-number-encrypted"]) {
|
|
const promptMessage = lazy.FormAutofillUtils.reauthOSPromptMessage(
|
|
"autofill-use-payment-method-os-prompt-macos",
|
|
"autofill-use-payment-method-os-prompt-windows",
|
|
"autofill-use-payment-method-os-prompt-other"
|
|
);
|
|
let decrypted;
|
|
let result;
|
|
try {
|
|
decrypted = await this.getDecryptedString(
|
|
profile["cc-number-encrypted"],
|
|
promptMessage
|
|
);
|
|
result = decrypted ? "success" : "fail_user_canceled";
|
|
} catch (ex) {
|
|
result = "fail_error";
|
|
} finally {
|
|
Glean.formautofill.promptShownOsReauth.record({
|
|
trigger: "autofill",
|
|
result,
|
|
});
|
|
}
|
|
if (!decrypted) {
|
|
// Early return if the decrypted is empty or undefined
|
|
return false;
|
|
}
|
|
profile["cc-number"] = decrypted;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async getDecryptedString(cipherText, reauth) {
|
|
if (
|
|
!lazy.FormAutofillUtils.getOSAuthEnabled(
|
|
lazy.FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF
|
|
)
|
|
) {
|
|
this.log.debug("Reauth is disabled");
|
|
reauth = false;
|
|
}
|
|
let string;
|
|
let errorResult = 0;
|
|
try {
|
|
string = await lazy.OSKeyStore.decrypt(cipherText, reauth);
|
|
} catch (e) {
|
|
errorResult = e.result;
|
|
if (e.result != Cr.NS_ERROR_ABORT) {
|
|
this.log.warn(`Decryption failed with result: ${e.result}`);
|
|
throw e;
|
|
}
|
|
this.log.warn("User canceled encryption login");
|
|
} finally {
|
|
Glean.creditcard.osKeystoreDecrypt.record({
|
|
isDecryptSuccess: errorResult === 0,
|
|
errorResult,
|
|
trigger: "autofill",
|
|
});
|
|
}
|
|
return string;
|
|
}
|
|
}
|