summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/LoginStore.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/LoginStore.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/LoginStore.sys.mjs182
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;
+};