1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/**
6 * Handles serialization of the data and persistence into a file.
7 *
8 * The file is stored in JSON format, without indentation, using UTF-8 encoding.
9 * With indentation applied, the file would look like this:
10 *
11 * {
12 *   "logins": [
13 *     {
14 *       "id": 2,
15 *       "hostname": "http://www.example.com",
16 *       "httpRealm": null,
17 *       "formSubmitURL": "http://www.example.com",
18 *       "usernameField": "username_field",
19 *       "passwordField": "password_field",
20 *       "encryptedUsername": "...",
21 *       "encryptedPassword": "...",
22 *       "guid": "...",
23 *       "encType": 1,
24 *       "timeCreated": 1262304000000,
25 *       "timeLastUsed": 1262304000000,
26 *       "timePasswordChanged": 1262476800000,
27 *       "timesUsed": 1
28 *     },
29 *     {
30 *       "id": 4,
31 *       (...)
32 *     }
33 *   ],
34 *   "nextId": 10,
35 *   "version": 1
36 * }
37 */
38
39"use strict";
40
41const EXPORTED_SYMBOLS = ["LoginStore"];
42
43// Globals
44
45const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
46ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
47ChromeUtils.defineModuleGetter(
48  this,
49  "JSONFile",
50  "resource://gre/modules/JSONFile.jsm"
51);
52const { XPCOMUtils } = ChromeUtils.import(
53  "resource://gre/modules/XPCOMUtils.jsm"
54);
55
56XPCOMUtils.defineLazyModuleGetters(this, {
57  FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js",
58  FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js",
59});
60
61/**
62 * Current data version assigned by the code that last touched the data.
63 *
64 * This number should be updated only when it is important to understand whether
65 * an old version of the code has touched the data, for example to execute an
66 * update logic.  In most cases, this number should not be changed, in
67 * particular when no special one-time update logic is needed.
68 *
69 * For example, this number should NOT be changed when a new optional field is
70 * added to a login entry.
71 */
72const kDataVersion = 3;
73
74// The permission type we store in the permission manager.
75const PERMISSION_SAVE_LOGINS = "login-saving";
76
77const MAX_DATE_MS = 8640000000000000;
78
79// LoginStore
80
81/**
82 * Inherits from JSONFile and handles serialization of login-related data and
83 * persistence into a file.
84 *
85 * @param aPath
86 *        String containing the file path where data should be saved.
87 */
88function LoginStore(aPath, aBackupPath = "") {
89  JSONFile.call(this, {
90    path: aPath,
91    dataPostProcessor: this._dataPostProcessor.bind(this),
92    backupTo: aBackupPath,
93  });
94}
95
96LoginStore.prototype = Object.create(JSONFile.prototype);
97LoginStore.prototype.constructor = LoginStore;
98
99LoginStore.prototype._save = async function() {
100  await JSONFile.prototype._save.call(this);
101  // Notify tests that writes to the login store is complete.
102  Services.obs.notifyObservers(null, "password-storage-updated");
103
104  if (this._options.backupTo) {
105    await this._backupHandler();
106  }
107};
108
109/**
110 * Delete logins backup file if the last saved login was removed using
111 * removeLogin() or if all logins were removed at once using removeAllUserFacingLogins().
112 * Note that if the user has a fxa key stored as a login, we just update the
113 * backup to only store the key when the last saved user facing login is removed.
114 */
115LoginStore.prototype._backupHandler = async function() {
116  const logins = this._data.logins;
117  // Return early if more than one login is stored because the backup won't need
118  // updating in this case.
119  if (logins.length > 1) {
120    return;
121  }
122
123  // If one login is stored and it's a fxa sync key, we update the backup to store the
124  // key only.
125  if (
126    logins.length &&
127    logins[0].hostname == FXA_PWDMGR_HOST &&
128    logins[0].httpRealm == FXA_PWDMGR_REALM
129  ) {
130    try {
131      await OS.File.copy(this.path, this._options.backupTo);
132
133      // This notification is specifically sent out for a test.
134      Services.obs.notifyObservers(null, "logins-backup-updated");
135    } catch (ex) {
136      Cu.reportError(ex);
137    }
138  } else if (!logins.length) {
139    // If no logins are stored anymore, delete backup.
140    await OS.File.remove(this._options.backupTo, {
141      ignoreAbsent: true,
142    });
143  }
144};
145
146/**
147 * Synchronously work on the data just loaded into memory.
148 */
149LoginStore.prototype._dataPostProcessor = function(data) {
150  if (data.nextId === undefined) {
151    data.nextId = 1;
152  }
153
154  // Create any arrays that are not present in the saved file.
155  if (!data.logins) {
156    data.logins = [];
157  }
158
159  if (!data.potentiallyVulnerablePasswords) {
160    data.potentiallyVulnerablePasswords = [];
161  }
162
163  if (!data.dismissedBreachAlertsByLoginGUID) {
164    data.dismissedBreachAlertsByLoginGUID = {};
165  }
166
167  // sanitize dates in logins
168  if (!("version" in data) || data.version < 3) {
169    let dateProperties = ["timeCreated", "timeLastUsed", "timePasswordChanged"];
170    let now = Date.now();
171    function getEarliestDate(login, defaultDate) {
172      let earliestDate = dateProperties.reduce((earliest, pname) => {
173        let ts = login[pname];
174        return !ts ? earliest : Math.min(ts, earliest);
175      }, defaultDate);
176      return earliestDate;
177    }
178    for (let login of data.logins) {
179      for (let pname of dateProperties) {
180        let earliestDate;
181        if (!login[pname] || login[pname] > MAX_DATE_MS) {
182          login[pname] =
183            earliestDate || (earliestDate = getEarliestDate(login, now));
184        }
185      }
186    }
187  }
188
189  // Indicate that the current version of the code has touched the file.
190  data.version = kDataVersion;
191
192  return data;
193};
194