summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/LoginStore.sys.mjs
blob: acb5a6365cbcf95aeb9d203f640b1fd8e656f800 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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;
};