Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
// _AboutLogins is only exported for testing
var EXPORTED_SYMBOLS = ["AboutLoginsParent", "_AboutLogins"];
const { setTimeout, clearTimeout } = ChromeUtils.importESModule(
);
const { XPCOMUtils } = ChromeUtils.importESModule(
);
const { AppConstants } = ChromeUtils.importESModule(
);
const { E10SUtils } = ChromeUtils.importESModule(
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyModuleGetters(lazy, {
});
XPCOMUtils.defineLazyGetter(lazy, "log", () => {
return lazy.LoginHelper.createLogger("AboutLoginsParent");
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"BREACH_ALERTS_ENABLED",
"signon.management.page.breach-alerts.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"FXA_ENABLED",
"identity.fxaccounts.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"OS_AUTH_ENABLED",
"signon.management.page.os-auth.enabled",
true
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"VULNERABLE_PASSWORDS_ENABLED",
"signon.management.page.vulnerable-passwords.enabled",
false
);
XPCOMUtils.defineLazyGetter(lazy, "AboutLoginsL10n", () => {
return new Localization(["branding/brand.ftl", "browser/aboutLogins.ftl"]);
});
const ABOUT_LOGINS_ORIGIN = "about:logins";
const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const PRIMARY_PASSWORD_NOTIFICATION_ID = "primary-password-login-required";
// about:logins will always use the privileged content process,
// even if it is disabled for other consumers such as about:newtab.
const EXPECTED_ABOUTLOGINS_REMOTE_TYPE = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
let _gPasswordRemaskTimeout = null;
const convertSubjectToLogin = subject => {
subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
const login = lazy.LoginHelper.loginToVanillaObject(subject);
if (!lazy.LoginHelper.isUserFacingLogin(login)) {
return null;
}
return augmentVanillaLoginObject(login);
};
const SUBDOMAIN_REGEX = new RegExp(/^www\d*\./);
const augmentVanillaLoginObject = login => {
// Note that `displayOrigin` can also include a httpRealm.
let title = login.displayOrigin.replace(SUBDOMAIN_REGEX, "");
return Object.assign({}, login, {
title,
});
};
class AboutLoginsParent extends JSWindowActorParent {
async receiveMessage(message) {
if (!this.browsingContext.embedderElement) {
return;
}
// Only respond to messages sent from a privlegedabout process. Ideally
// we would also check the contentPrincipal.originNoSuffix but this
// check has been removed due to bug 1576722.
if (
this.browsingContext.embedderElement.remoteType !=
EXPECTED_ABOUTLOGINS_REMOTE_TYPE
) {
throw new Error(
`AboutLoginsParent: Received ${message.name} message the remote type didn't match expectations: ${this.browsingContext.embedderElement.remoteType} == ${EXPECTED_ABOUTLOGINS_REMOTE_TYPE}`
);
}
AboutLogins.subscribers.add(this.browsingContext);
switch (message.name) {
case "AboutLogins:CreateLogin": {
this.#createLogin(message.data.login);
break;
}
case "AboutLogins:DeleteLogin": {
this.#deleteLogin(message.data.login);
break;
}
case "AboutLogins:SortChanged": {
this.#sortChanged(message.data);
break;
}
case "AboutLogins:SyncEnable": {
this.#syncEnable();
break;
}
case "AboutLogins:SyncOptions": {
this.#syncOptions();
break;
}
case "AboutLogins:ImportFromBrowser": {
this.#importFromBrowser();
break;
}
case "AboutLogins:ImportReportInit": {
this.#importReportInit();
break;
}
case "AboutLogins:GetHelp": {
this.#getHelp();
break;
}
case "AboutLogins:OpenPreferences": {
this.#openPreferences();
break;
}
case "AboutLogins:PrimaryPasswordRequest": {
await this.#primaryPasswordRequest(message.data);
break;
}
case "AboutLogins:Subscribe": {
await this.#subscribe();
break;
}
case "AboutLogins:UpdateLogin": {
this.#updateLogin(message.data.login);
break;
}
case "AboutLogins:ExportPasswords": {
await this.#exportPasswords();
break;
}
case "AboutLogins:ImportFromFile": {
await this.#importFromFile();
break;
}
case "AboutLogins:RemoveAllLogins": {
this.#removeAllLogins();
break;
}
}
}
get #ownerGlobal() {
return this.browsingContext.embedderElement.ownerGlobal;
}
#createLogin(newLogin) {
if (!Services.policies.isAllowed("removeMasterPassword")) {
if (!lazy.LoginHelper.isPrimaryPasswordSet()) {
this.#ownerGlobal.openDialog(
"",
"centerscreen,chrome,modal,titlebar"
);
if (!lazy.LoginHelper.isPrimaryPasswordSet()) {
return;
}
}
}
// Remove the path from the origin, if it was provided.
let origin = lazy.LoginHelper.getLoginOrigin(newLogin.origin);
if (!origin) {
console.error(
"AboutLogins:CreateLogin: Unable to get an origin from the login details."
);
return;
}
newLogin.origin = origin;
Object.assign(newLogin, {
formActionOrigin: "",
usernameField: "",
passwordField: "",
});
newLogin = lazy.LoginHelper.vanillaObjectToLogin(newLogin);
try {
Services.logins.addLogin(newLogin);
} catch (error) {
this.#handleLoginStorageErrors(newLogin, error);
}
}
#deleteLogin(loginObject) {
let login = lazy.LoginHelper.vanillaObjectToLogin(loginObject);
Services.logins.removeLogin(login);
}
#sortChanged(sort) {
Services.prefs.setCharPref("signon.management.page.sort", sort);
}
#syncEnable() {
this.#ownerGlobal.gSync.openFxAEmailFirstPage("password-manager");
}
#syncOptions() {
this.#ownerGlobal.gSync.openFxAManagePage("password-manager");
}
#importFromBrowser() {
try {
lazy.MigrationUtils.showMigrationWizard(this.#ownerGlobal, {
entrypoint: lazy.MigrationUtils.MIGRATION_ENTRYPOINTS.PASSWORDS,
});
} catch (ex) {
console.error(ex);
}
}
#importReportInit() {
let reportData = lazy.LoginCSVImport.lastImportReport;
this.sendAsyncMessage("AboutLogins:ImportReportData", reportData);
}
#getHelp() {
const SUPPORT_URL =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"password-manager-remember-delete-edit-logins";
this.#ownerGlobal.openWebLinkIn(SUPPORT_URL, "tab", {
relatedToCurrent: true,
});
}
#openPreferences() {
this.#ownerGlobal.openPreferences("privacy-logins");
}
async #primaryPasswordRequest(messageId) {
if (!messageId) {
throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId.");
}
let messageText = { value: "NOT SUPPORTED" };
let captionText = { value: "" };
// This feature is only supported on Windows and macOS
// but we still call in to OSKeyStore on Linux to get
// the proper auth_details for Telemetry.
// See bug 1614874 for Linux support.
if (lazy.OS_AUTH_ENABLED && lazy.OSKeyStore.canReauth()) {
messageId += "-" + AppConstants.platform;
[messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
{
id: messageId,
},
{
id: "about-logins-os-auth-dialog-caption",
},
]);
}
let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
this.browsingContext.embedderElement,
lazy.OS_AUTH_ENABLED,
AboutLogins._authExpirationTime,
messageText.value,
captionText.value
);
this.sendAsyncMessage("AboutLogins:PrimaryPasswordResponse", {
result: isAuthorized,
telemetryEvent,
});
if (isAuthorized) {
AboutLogins._authExpirationTime = Date.now() + AUTH_TIMEOUT_MS;
const remaskPasswords = () => {
this.sendAsyncMessage("AboutLogins:RemaskPassword");
};
clearTimeout(_gPasswordRemaskTimeout);
_gPasswordRemaskTimeout = setTimeout(remaskPasswords, AUTH_TIMEOUT_MS);
}
}
async #subscribe() {
AboutLogins._authExpirationTime = Number.NEGATIVE_INFINITY;
AboutLogins.addObservers();
const logins = await AboutLogins.getAllLogins();
try {
let syncState = AboutLogins.getSyncState();
let selectedSort = Services.prefs.getCharPref(
"signon.management.page.sort",
"name"
);
if (selectedSort == "breached") {
// The "breached" value was used since Firefox 70 and
// replaced with "alerts" in Firefox 76.
selectedSort = "alerts";
}
this.sendAsyncMessage("AboutLogins:Setup", {
logins,
selectedSort,
syncState,
primaryPasswordEnabled: lazy.LoginHelper.isPrimaryPasswordSet(),
passwordRevealVisible: Services.policies.isAllowed("passwordReveal"),
importVisible:
Services.policies.isAllowed("profileImport") &&
AppConstants.platform != "linux",
});
await AboutLogins.sendAllLoginRelatedObjects(
logins,
this.browsingContext
);
} catch (ex) {
if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
throw ex;
}
// The message manager may be destroyed before the replies can be sent.
lazy.log.debug(
"AboutLogins:Subscribe: exception when replying with logins",
ex
);
}
}
#updateLogin(loginUpdates) {
let logins = lazy.LoginHelper.searchLoginsWithObject({
guid: loginUpdates.guid,
});
if (logins.length != 1) {
lazy.log.warn(
`AboutLogins:UpdateLogin: expected to find a login for guid: ${loginUpdates.guid} but found ${logins.length}`
);
return;
}
let modifiedLogin = logins[0].clone();
if (loginUpdates.hasOwnProperty("username")) {
modifiedLogin.username = loginUpdates.username;
}
if (loginUpdates.hasOwnProperty("password")) {
modifiedLogin.password = loginUpdates.password;
}
try {
Services.logins.modifyLogin(logins[0], modifiedLogin);
} catch (error) {
this.#handleLoginStorageErrors(modifiedLogin, error);
}
}
async #exportPasswords() {
let messageText = { value: "NOT SUPPORTED" };
let captionText = { value: "" };
// This feature is only supported on Windows and macOS
// but we still call in to OSKeyStore on Linux to get
// the proper auth_details for Telemetry.
// See bug 1614874 for Linux support.
if (lazy.OSKeyStore.canReauth()) {
let messageId =
"about-logins-export-password-os-auth-dialog-message-" +
AppConstants.platform;
[messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
{
id: messageId,
},
{
id: "about-logins-os-auth-dialog-caption",
},
]);
}
let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
this.browsingContext.embedderElement,
true,
null, // Prompt regardless of a recent prompt
messageText.value,
captionText.value
);
let { method, object, extra = {}, value = null } = telemetryEvent;
Services.telemetry.recordEvent("pwmgr", method, object, value, extra);
if (!isAuthorized) {
return;
}
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
function fpCallback(aResult) {
if (aResult != Ci.nsIFilePicker.returnCancel) {
lazy.LoginExport.exportAsCSV(fp.file.path);
Services.telemetry.recordEvent(
"pwmgr",
"mgmt_menu_item_used",
"export_complete"
);
}
}
let [
title,
defaultFilename,
okButtonLabel,
csvFilterTitle,
] = await lazy.AboutLoginsL10n.formatValues([
{
id: "about-logins-export-file-picker-title",
},
{
id: "about-logins-export-file-picker-default-filename",
},
{
id: "about-logins-export-file-picker-export-button",
},
{
id: "about-logins-export-file-picker-csv-filter-title",
},
]);
fp.init(this.#ownerGlobal, title, Ci.nsIFilePicker.modeSave);
fp.appendFilter(csvFilterTitle, "*.csv");
fp.appendFilters(Ci.nsIFilePicker.filterAll);
fp.defaultString = defaultFilename;
fp.defaultExtension = "csv";
fp.okButtonLabel = okButtonLabel;
fp.open(fpCallback);
}
async #importFromFile() {
let [
title,
okButtonLabel,
csvFilterTitle,
tsvFilterTitle,
] = await lazy.AboutLoginsL10n.formatValues([
{
id: "about-logins-import-file-picker-title",
},
{
id: "about-logins-import-file-picker-import-button",
},
{
id: "about-logins-import-file-picker-csv-filter-title",
},
{
id: "about-logins-import-file-picker-tsv-filter-title",
},
]);
let { result, path } = await this.openFilePickerDialog(
title,
okButtonLabel,
[
{
title: csvFilterTitle,
extensionPattern: "*.csv",
},
{
title: tsvFilterTitle,
extensionPattern: "*.tsv",
},
],
this.#ownerGlobal
);
if (result != Ci.nsIFilePicker.returnCancel) {
let summary;
try {
summary = await lazy.LoginCSVImport.importFromCSV(path);
} catch (e) {
console.error(e);
this.sendAsyncMessage(
"AboutLogins:ImportPasswordsErrorDialog",
e.errorType
);
}
if (summary) {
this.sendAsyncMessage("AboutLogins:ImportPasswordsDialog", summary);
Services.telemetry.recordEvent(
"pwmgr",
"mgmt_menu_item_used",
"import_csv_complete"
);
}
}
}
#removeAllLogins() {
Services.logins.removeAllUserFacingLogins();
}
#handleLoginStorageErrors(login, error) {
let messageObject = {
login: augmentVanillaLoginObject(
lazy.LoginHelper.loginToVanillaObject(login)
),
errorMessage: error.message,
};
if (error.message.includes("This login already exists")) {
// See comment in LoginHelper.createLoginAlreadyExistsError as to
// why we need to call .toString() on the nsISupportsString.
messageObject.existingLoginGuid = error.data.toString();
}
this.sendAsyncMessage("AboutLogins:ShowLoginItemError", messageObject);
}
async openFilePickerDialog(title, okButtonLabel, appendFilters, ownerGlobal) {
return new Promise(resolve => {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(ownerGlobal, title, Ci.nsIFilePicker.modeOpen);
for (const appendFilter of appendFilters) {
fp.appendFilter(appendFilter.title, appendFilter.extensionPattern);
}
fp.appendFilters(Ci.nsIFilePicker.filterAll);
fp.okButtonLabel = okButtonLabel;
fp.open(async result => {
resolve({ result, path: fp.file.path });
});
});
}
}
class AboutLoginsInternal {
subscribers = new WeakSet();
#observersAdded = false;
authExpirationTime = Number.NEGATIVE_INFINITY;
async observe(subject, topic, type) {
if (!ChromeUtils.nondeterministicGetWeakSetKeys(this.subscribers).length) {
this.#removeObservers();
return;
}
switch (topic) {
case "passwordmgr-reload-all": {
await this.#reloadAllLogins();
break;
}
case "passwordmgr-crypto-login": {
this.#removeNotifications(PRIMARY_PASSWORD_NOTIFICATION_ID);
await this.#reloadAllLogins();
break;
}
case "passwordmgr-crypto-loginCanceled": {
this.#showPrimaryPasswordLoginNotifications();
break;
}
case lazy.UIState.ON_UPDATE: {
this.#messageSubscribers("AboutLogins:SyncState", this.getSyncState());
break;
}
case "passwordmgr-storage-changed": {
switch (type) {
case "addLogin": {
await this.#addLogin(subject);
break;
}
case "modifyLogin": {
this.#modifyLogin(subject);
break;
}
case "removeLogin": {
this.#removeLogin(subject);
break;
}
case "removeAllLogins": {
this.#removeAllLogins();
break;
}
}
}
}
}
async #addLogin(subject) {
const login = convertSubjectToLogin(subject);
if (!login) {
return;
}
if (lazy.BREACH_ALERTS_ENABLED) {
this.#messageSubscribers(
"AboutLogins:UpdateBreaches",
await lazy.LoginBreaches.getPotentialBreachesByLoginGUID([login])
);
if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
this.#messageSubscribers(
"AboutLogins:UpdateVulnerableLogins",
await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
[login]
)
);
}
}
this.#messageSubscribers("AboutLogins:LoginAdded", login);
}
async #modifyLogin(subject) {
subject.QueryInterface(Ci.nsIArrayExtensions);
const login = convertSubjectToLogin(subject.GetElementAt(1));
if (!login) {
return;
}
if (lazy.BREACH_ALERTS_ENABLED) {
let breachesForThisLogin = await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(
[login]
);
let breachData = breachesForThisLogin.size
? breachesForThisLogin.get(login.guid)
: false;
this.#messageSubscribers(
"AboutLogins:UpdateBreaches",
new Map([[login.guid, breachData]])
);
if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
let vulnerablePasswordsForThisLogin = await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
[login]
);
let isLoginVulnerable = !!vulnerablePasswordsForThisLogin.size;
this.#messageSubscribers(
"AboutLogins:UpdateVulnerableLogins",
new Map([[login.guid, isLoginVulnerable]])
);
}
}
this.#messageSubscribers("AboutLogins:LoginModified", login);
}
#removeLogin(subject) {
const login = convertSubjectToLogin(subject);
if (!login) {
return;
}
this.#messageSubscribers("AboutLogins:LoginRemoved", login);
}
#removeAllLogins() {
this.#messageSubscribers("AboutLogins:RemoveAllLogins", []);
}
async #reloadAllLogins() {
let logins = await this.getAllLogins();
this.#messageSubscribers("AboutLogins:AllLogins", logins);
await this.sendAllLoginRelatedObjects(logins);
}
#showPrimaryPasswordLoginNotifications() {
this.#showNotifications({
id: PRIMARY_PASSWORD_NOTIFICATION_ID,
priority: "PRIORITY_WARNING_MEDIUM",
messageId: "about-logins-primary-password-notification-message",
buttonIds: ["master-password-reload-button"],
onClicks: [
function onReloadClick(browser) {
browser.reload();
},
],
});
this.#messageSubscribers("AboutLogins:PrimaryPasswordAuthRequired");
}
#showNotifications({
id,
priority,
iconURL,
messageId,
buttonIds,
onClicks,
extraFtl = [],
} = {}) {
for (let subscriber of this.#subscriberIterator()) {
let browser = subscriber.embedderElement;
let MozXULElement = browser.ownerGlobal.MozXULElement;
MozXULElement.insertFTLIfNeeded("browser/aboutLogins.ftl");
for (let ftl of extraFtl) {
MozXULElement.insertFTLIfNeeded(ftl);
}
// If there's already an existing notification bar, don't do anything.
let { gBrowser } = browser.ownerGlobal;
let notificationBox = gBrowser.getNotificationBox(browser);
let notification = notificationBox.getNotificationWithValue(id);
if (notification) {
continue;
}
let buttons = [];
for (let i = 0; i < buttonIds.length; i++) {
buttons[i] = {
"l10n-id": buttonIds[i],
popup: null,
callback: () => {
onClicks[i](browser);
},
};
}
notification = notificationBox.appendNotification(
id,
{
label: { "l10n-id": messageId },
image: iconURL,
priority: notificationBox[priority],
},
buttons
);
}
}
#removeNotifications(notificationId) {
for (let subscriber of this.#subscriberIterator()) {
let browser = subscriber.embedderElement;
let { gBrowser } = browser.ownerGlobal;
let notificationBox = gBrowser.getNotificationBox(browser);
let notification = notificationBox.getNotificationWithValue(
notificationId
);
if (!notification) {
continue;
}
notificationBox.removeNotification(notification);
}
}
*#subscriberIterator() {
let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(
this.subscribers
);
for (let subscriber of subscribers) {
let browser = subscriber.embedderElement;
if (
browser?.remoteType != EXPECTED_ABOUTLOGINS_REMOTE_TYPE ||
browser?.contentPrincipal?.originNoSuffix != ABOUT_LOGINS_ORIGIN
) {
this.subscribers.delete(subscriber);
continue;
}
yield subscriber;
}
}
#messageSubscribers(name, details) {
for (let subscriber of this.#subscriberIterator()) {
try {
if (subscriber.currentWindowGlobal) {
let actor = subscriber.currentWindowGlobal.getActor("AboutLogins");
actor.sendAsyncMessage(name, details);
}
} catch (ex) {
if (ex.result == Cr.NS_ERROR_NOT_INITIALIZED) {
// The actor may be destroyed before the message is sent.
lazy.log.debug(
"messageSubscribers: exception when calling sendAsyncMessage",
ex
);
} else {
throw ex;
}
}
}
}
async getAllLogins() {
try {
let logins = await lazy.LoginHelper.getAllUserFacingLogins();
return logins
.map(lazy.LoginHelper.loginToVanillaObject)
.map(augmentVanillaLoginObject);
} catch (e) {
if (e.result == Cr.NS_ERROR_ABORT) {
// If the user cancels the MP prompt then return no logins.
return [];
}
throw e;
}
}
async sendAllLoginRelatedObjects(logins, browsingContext) {
let sendMessageFn = (name, details) => {
if (browsingContext?.currentWindowGlobal) {
let actor = browsingContext.currentWindowGlobal.getActor("AboutLogins");
actor.sendAsyncMessage(name, details);
} else {
this.#messageSubscribers(name, details);
}
};
if (lazy.BREACH_ALERTS_ENABLED) {
sendMessageFn(
"AboutLogins:SetBreaches",
await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins)
);
if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
sendMessageFn(
"AboutLogins:SetVulnerableLogins",
await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
logins
)
);
}
}
}
getSyncState() {
const state = lazy.UIState.get();
// As long as Sync is configured, about:logins will treat it as
// authenticated. More diagnostics and error states can be handled
// by other more Sync-specific pages.
const loggedIn = state.status != lazy.UIState.STATUS_NOT_CONFIGURED;
const passwordSyncEnabled = state.syncEnabled && lazy.PASSWORD_SYNC_ENABLED;
return {
loggedIn,
email: state.email,
avatarURL: state.avatarURL,
fxAccountsEnabled: lazy.FXA_ENABLED,
passwordSyncEnabled,
};
}
onPasswordSyncEnabledPreferenceChange(data, previous, latest) {
this.#messageSubscribers("AboutLogins:SyncState", this.getSyncState());
}
#observedTopics = [
"passwordmgr-crypto-login",
"passwordmgr-crypto-loginCanceled",
"passwordmgr-storage-changed",
"passwordmgr-reload-all",
lazy.UIState.ON_UPDATE,
];
addObservers() {
if (!this.#observersAdded) {
for (const topic of this.#observedTopics) {
Services.obs.addObserver(this, topic);
}
this.#observersAdded = true;
}
}
#removeObservers() {
for (const topic of this.#observedTopics) {
Services.obs.removeObserver(this, topic);
}
this.#observersAdded = false;
}
}
let AboutLogins = new AboutLoginsInternal();
var _AboutLogins = AboutLogins;
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"PASSWORD_SYNC_ENABLED",
"services.sync.engine.passwords",
false,
AboutLogins.onPasswordSyncEnabledPreferenceChange
);