diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/passwordmgr/LoginStore.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/passwordmgr/LoginStore.sys.mjs')
-rw-r--r-- | toolkit/components/passwordmgr/LoginStore.sys.mjs | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/LoginStore.sys.mjs b/toolkit/components/passwordmgr/LoginStore.sys.mjs new file mode 100644 index 0000000000..acb5a6365c --- /dev/null +++ b/toolkit/components/passwordmgr/LoginStore.sys.mjs @@ -0,0 +1,182 @@ +/* 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/. */ + +/** + * Handles serialization of the data and persistence into a file. + * + * The file is stored in JSON format, without indentation, using UTF-8 encoding. + * With indentation applied, the file would look like this: + * + * { + * "logins": [ + * { + * "id": 2, + * "hostname": "http://www.example.com", + * "httpRealm": null, + * "formSubmitURL": "http://www.example.com", + * "usernameField": "username_field", + * "passwordField": "password_field", + * "encryptedUsername": "...", + * "encryptedPassword": "...", + * "guid": "...", + * "encType": 1, + * "timeCreated": 1262304000000, + * "timeLastUsed": 1262304000000, + * "timePasswordChanged": 1262476800000, + * "timesUsed": 1 + * // only present if other clients had fields we didn't know about + * "encryptedUnknownFields: "...", + * }, + * { + * "id": 4, + * (...) + * } + * ], + * "nextId": 10, + * "version": 1 + * } + */ + +// Globals + +import { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js", + FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js", +}); + +/** + * Current data version assigned by the code that last touched the data. + * + * This number should be updated only when it is important to understand whether + * an old version of the code has touched the data, for example to execute an + * update logic. In most cases, this number should not be changed, in + * particular when no special one-time update logic is needed. + * + * For example, this number should NOT be changed when a new optional field is + * added to a login entry. + */ +const kDataVersion = 3; + +const MAX_DATE_MS = 8640000000000000; + +// LoginStore + +/** + * Inherits from JSONFile and handles serialization of login-related data and + * persistence into a file. + * + * @param aPath + * String containing the file path where data should be saved. + */ +export function LoginStore(aPath, aBackupPath = "") { + JSONFile.call(this, { + path: aPath, + dataPostProcessor: this._dataPostProcessor.bind(this), + backupTo: aBackupPath, + }); +} + +LoginStore.prototype = Object.create(JSONFile.prototype); +LoginStore.prototype.constructor = LoginStore; + +LoginStore.prototype._save = async function () { + await JSONFile.prototype._save.call(this); + // Notify tests that writes to the login store is complete. + Services.obs.notifyObservers(null, "password-storage-updated"); + + if (this._options.backupTo) { + await this._backupHandler(); + } +}; + +/** + * Delete logins backup file if the last saved login was removed using + * removeLogin() or if all logins were removed at once using removeAllUserFacingLogins(). + * Note that if the user has a fxa key stored as a login, we just update the + * backup to only store the key when the last saved user facing login is removed. + */ +LoginStore.prototype._backupHandler = async function () { + const logins = this._data.logins; + // Return early if more than one login is stored because the backup won't need + // updating in this case. + if (logins.length > 1) { + return; + } + + // If one login is stored and it's a fxa sync key, we update the backup to store the + // key only. + if ( + logins.length && + logins[0].hostname == lazy.FXA_PWDMGR_HOST && + logins[0].httpRealm == lazy.FXA_PWDMGR_REALM + ) { + try { + await IOUtils.copy(this.path, this._options.backupTo); + + // This notification is specifically sent out for a test. + Services.obs.notifyObservers(null, "logins-backup-updated"); + } catch (ex) { + console.error(ex); + } + } else if (!logins.length) { + // If no logins are stored anymore, delete backup. + await IOUtils.remove(this._options.backupTo, { + ignoreAbsent: true, + }); + } +}; + +/** + * Synchronously work on the data just loaded into memory. + */ +LoginStore.prototype._dataPostProcessor = function (data) { + if (data.nextId === undefined) { + data.nextId = 1; + } + + // Create any arrays that are not present in the saved file. + if (!data.logins) { + data.logins = []; + } + + if (!data.potentiallyVulnerablePasswords) { + data.potentiallyVulnerablePasswords = []; + } + + if (!data.dismissedBreachAlertsByLoginGUID) { + data.dismissedBreachAlertsByLoginGUID = {}; + } + + // sanitize dates in logins + if (!("version" in data) || data.version < 3) { + let dateProperties = ["timeCreated", "timeLastUsed", "timePasswordChanged"]; + let now = Date.now(); + function getEarliestDate(login, defaultDate) { + let earliestDate = dateProperties.reduce((earliest, pname) => { + let ts = login[pname]; + return !ts ? earliest : Math.min(ts, earliest); + }, defaultDate); + return earliestDate; + } + for (let login of data.logins) { + for (let pname of dateProperties) { + let earliestDate; + if (!login[pname] || login[pname] > MAX_DATE_MS) { + login[pname] = + earliestDate || (earliestDate = getEarliestDate(login, now)); + } + } + } + } + + // Indicate that the current version of the code has touched the file. + data.version = kDataVersion; + + return data; +}; |