Source code

Revision control

Other Tools

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
"use strict";
6
7
const PERMISSION_SAVE_LOGINS = "login-saving";
8
const MAX_DATE_MS = 8640000000000000;
9
10
const { XPCOMUtils } = ChromeUtils.import(
12
);
13
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14
15
ChromeUtils.defineModuleGetter(
16
this,
17
"LoginHelper",
19
);
20
ChromeUtils.defineModuleGetter(
21
this,
22
"LoginFormFactory",
24
);
25
ChromeUtils.defineModuleGetter(
26
this,
27
"InsecurePasswordUtils",
29
);
30
31
XPCOMUtils.defineLazyGetter(this, "log", () => {
32
let logger = LoginHelper.createLogger("LoginManager");
33
return logger;
34
});
35
36
const MS_PER_DAY = 24 * 60 * 60 * 1000;
37
38
if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
39
throw new Error("LoginManager.jsm should only run in the parent process");
40
}
41
42
function LoginManager() {
43
this.init();
44
}
45
46
LoginManager.prototype = {
47
classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
48
QueryInterface: ChromeUtils.generateQI([
49
Ci.nsILoginManager,
50
Ci.nsISupportsWeakReference,
51
Ci.nsIInterfaceRequestor,
52
]),
53
getInterface(aIID) {
54
if (aIID.equals(Ci.mozIStorageConnection) && this._storage) {
55
let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor);
56
return ir.getInterface(aIID);
57
}
58
59
if (aIID.equals(Ci.nsIVariant)) {
60
// Allows unwrapping the JavaScript object for regression tests.
61
return this;
62
}
63
64
throw new Components.Exception(
65
"Interface not available",
66
Cr.NS_ERROR_NO_INTERFACE
67
);
68
},
69
70
/* ---------- private members ---------- */
71
72
_storage: null, // Storage component which contains the saved logins
73
74
/**
75
* Initialize the Login Manager. Automatically called when service
76
* is created.
77
*
78
* Note: Service created in BrowserGlue#_scheduleStartupIdleTasks()
79
*/
80
init() {
81
// Cache references to current |this| in utility objects
82
this._observer._pwmgr = this;
83
84
Services.obs.addObserver(this._observer, "xpcom-shutdown");
85
Services.obs.addObserver(this._observer, "passwordmgr-storage-replace");
86
87
// Initialize storage so that asynchronous data loading can start.
88
this._initStorage();
89
90
Services.obs.addObserver(this._observer, "gather-telemetry");
91
},
92
93
_initStorage() {
94
this._storage = Cc[
95
"@mozilla.org/login-manager/storage/default;1"
96
].createInstance(Ci.nsILoginManagerStorage);
97
this.initializationPromise = this._storage.initialize();
98
this.initializationPromise.then(() => {
99
log.debug(
100
"initializationPromise is resolved, updating isMasterPasswordSet in sharedData"
101
);
102
Services.ppmm.sharedData.set(
103
"isMasterPasswordSet",
104
LoginHelper.isMasterPasswordSet()
105
);
106
});
107
},
108
109
/* ---------- Utility objects ---------- */
110
111
/**
112
* Internal utility object, implements the nsIObserver interface.
113
* Used to receive notification for: form submission, preference changes.
114
*/
115
_observer: {
116
_pwmgr: null,
117
118
QueryInterface: ChromeUtils.generateQI([
119
Ci.nsIObserver,
120
Ci.nsISupportsWeakReference,
121
]),
122
123
// nsIObserver
124
observe(subject, topic, data) {
125
if (topic == "xpcom-shutdown") {
126
delete this._pwmgr._storage;
127
this._pwmgr = null;
128
} else if (topic == "passwordmgr-storage-replace") {
129
(async () => {
130
await this._pwmgr._storage.terminate();
131
this._pwmgr._initStorage();
132
await this._pwmgr.initializationPromise;
133
Services.obs.notifyObservers(
134
null,
135
"passwordmgr-storage-replace-complete"
136
);
137
})();
138
} else if (topic == "gather-telemetry") {
139
// When testing, the "data" parameter is a string containing the
140
// reference time in milliseconds for time-based statistics.
141
this._pwmgr._gatherTelemetry(
142
data ? parseInt(data) : new Date().getTime()
143
);
144
} else {
145
log.debug("Oops! Unexpected notification:", topic);
146
}
147
},
148
},
149
150
/**
151
* Collects statistics about the current logins and settings. The telemetry
152
* histograms used here are not accumulated, but are reset each time this
153
* function is called, since it can be called multiple times in a session.
154
*
155
* This function might also not be called at all in the current session.
156
*
157
* @param referenceTimeMs
158
* Current time used to calculate time-based statistics, expressed as
159
* the number of milliseconds since January 1, 1970, 00:00:00 UTC.
160
* This is set to a fake value during unit testing.
161
*/
162
_gatherTelemetry(referenceTimeMs) {
163
function clearAndGetHistogram(histogramId) {
164
let histogram = Services.telemetry.getHistogramById(histogramId);
165
histogram.clear();
166
return histogram;
167
}
168
169
clearAndGetHistogram("PWMGR_BLOCKLIST_NUM_SITES").add(
170
this.getAllDisabledHosts().length
171
);
172
clearAndGetHistogram("PWMGR_NUM_SAVED_PASSWORDS").add(
173
this.countLogins("", "", "")
174
);
175
clearAndGetHistogram("PWMGR_NUM_HTTPAUTH_PASSWORDS").add(
176
this.countLogins("", null, "")
177
);
178
Services.obs.notifyObservers(
179
null,
180
"weave:telemetry:histogram",
181
"PWMGR_BLOCKLIST_NUM_SITES"
182
);
183
Services.obs.notifyObservers(
184
null,
185
"weave:telemetry:histogram",
186
"PWMGR_NUM_SAVED_PASSWORDS"
187
);
188
189
// This is a boolean histogram, and not a flag, because we don't want to
190
// record any value if _gatherTelemetry is not called.
191
clearAndGetHistogram("PWMGR_SAVING_ENABLED").add(LoginHelper.enabled);
192
Services.obs.notifyObservers(
193
null,
194
"weave:telemetry:histogram",
195
"PWMGR_SAVING_ENABLED"
196
);
197
198
// Don't try to get logins if MP is enabled, since we don't want to show a MP prompt.
199
if (!this.isLoggedIn) {
200
return;
201
}
202
203
let logins = this.getAllLogins();
204
205
let usernamePresentHistogram = clearAndGetHistogram(
206
"PWMGR_USERNAME_PRESENT"
207
);
208
let loginLastUsedDaysHistogram = clearAndGetHistogram(
209
"PWMGR_LOGIN_LAST_USED_DAYS"
210
);
211
212
let originCount = new Map();
213
for (let login of logins) {
214
usernamePresentHistogram.add(!!login.username);
215
216
let origin = login.origin;
217
originCount.set(origin, (originCount.get(origin) || 0) + 1);
218
219
login.QueryInterface(Ci.nsILoginMetaInfo);
220
let timeLastUsedAgeMs = referenceTimeMs - login.timeLastUsed;
221
if (timeLastUsedAgeMs > 0) {
222
loginLastUsedDaysHistogram.add(
223
Math.floor(timeLastUsedAgeMs / MS_PER_DAY)
224
);
225
}
226
}
227
Services.obs.notifyObservers(
228
null,
229
"weave:telemetry:histogram",
230
"PWMGR_LOGIN_LAST_USED_DAYS"
231
);
232
233
let passwordsCountHistogram = clearAndGetHistogram(
234
"PWMGR_NUM_PASSWORDS_PER_HOSTNAME"
235
);
236
for (let count of originCount.values()) {
237
passwordsCountHistogram.add(count);
238
}
239
Services.obs.notifyObservers(
240
null,
241
"weave:telemetry:histogram",
242
"PWMGR_NUM_PASSWORDS_PER_HOSTNAME"
243
);
244
},
245
246
/**
247
* Ensures that a login isn't missing any necessary fields.
248
*
249
* @param login
250
* The login to check.
251
*/
252
_checkLogin(login) {
253
// Sanity check the login
254
if (login.origin == null || !login.origin.length) {
255
throw new Error("Can't add a login with a null or empty origin.");
256
}
257
258
// For logins w/o a username, set to "", not null.
259
if (login.username == null) {
260
throw new Error("Can't add a login with a null username.");
261
}
262
263
if (login.password == null || !login.password.length) {
264
throw new Error("Can't add a login with a null or empty password.");
265
}
266
267
if (login.formActionOrigin || login.formActionOrigin == "") {
268
// We have a form submit URL. Can't have a HTTP realm.
269
if (login.httpRealm != null) {
270
throw new Error(
271
"Can't add a login with both a httpRealm and formActionOrigin."
272
);
273
}
274
} else if (login.httpRealm) {
275
// We have a HTTP realm. Can't have a form submit URL.
276
if (login.formActionOrigin != null) {
277
throw new Error(
278
"Can't add a login with both a httpRealm and formActionOrigin."
279
);
280
}
281
} else {
282
// Need one or the other!
283
throw new Error(
284
"Can't add a login without a httpRealm or formActionOrigin."
285
);
286
}
287
288
login.QueryInterface(Ci.nsILoginMetaInfo);
289
for (let pname of ["timeCreated", "timeLastUsed", "timePasswordChanged"]) {
290
// Invalid dates
291
if (login[pname] > MAX_DATE_MS) {
292
throw new Error("Can't add a login with invalid date properties.");
293
}
294
}
295
},
296
297
/* ---------- Primary Public interfaces ---------- */
298
299
/**
300
* @type Promise
301
* This promise is resolved when initialization is complete, and is rejected
302
* in case the asynchronous part of initialization failed.
303
*/
304
initializationPromise: null,
305
306
/**
307
* Add a new login to login storage.
308
*/
309
addLogin(login) {
310
this._checkLogin(login);
311
312
// Look for an existing entry.
313
let logins = this.findLogins(
314
login.origin,
315
login.formActionOrigin,
316
login.httpRealm
317
);
318
319
let matchingLogin = logins.find(l => login.matches(l, true));
320
if (matchingLogin) {
321
throw LoginHelper.createLoginAlreadyExistsError(matchingLogin.guid);
322
}
323
324
log.debug("Adding login");
325
return this._storage.addLogin(login);
326
},
327
328
async addLogins(logins) {
329
let crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
330
Ci.nsILoginManagerCrypto
331
);
332
let plaintexts = logins
333
.map(l => l.username)
334
.concat(logins.map(l => l.password));
335
let ciphertexts = await crypto.encryptMany(plaintexts);
336
let usernames = ciphertexts.slice(0, logins.length);
337
let passwords = ciphertexts.slice(logins.length);
338
let resultLogins = [];
339
for (let i = 0; i < logins.length; i++) {
340
try {
341
this._checkLogin(logins[i]);
342
} catch (e) {
343
Cu.reportError(e);
344
continue;
345
}
346
347
let plaintextUsername = logins[i].username;
348
let plaintextPassword = logins[i].password;
349
logins[i].username = usernames[i];
350
logins[i].password = passwords[i];
351
log.debug("Adding login");
352
let resultLogin = this._storage.addLogin(
353
logins[i],
354
true,
355
plaintextUsername,
356
plaintextPassword
357
);
358
// Reset the username and password to keep the same guarantees as addLogin
359
logins[i].username = plaintextUsername;
360
logins[i].password = plaintextPassword;
361
362
resultLogin.username = plaintextUsername;
363
resultLogin.password = plaintextPassword;
364
resultLogins.push(resultLogin);
365
}
366
return resultLogins;
367
},
368
369
/**
370
* Remove the specified login from the stored logins.
371
*/
372
removeLogin(login) {
373
log.debug("Removing login", login.QueryInterface(Ci.nsILoginMetaInfo).guid);
374
return this._storage.removeLogin(login);
375
},
376
377
/**
378
* Change the specified login to match the new login or new properties.
379
*/
380
modifyLogin(oldLogin, newLogin) {
381
log.debug(
382
"Modifying login",
383
oldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid
384
);
385
return this._storage.modifyLogin(oldLogin, newLogin);
386
},
387
388
/**
389
* Record that the password of a saved login was used (e.g. submitted or copied).
390
*/
391
recordPasswordUse(login) {
392
log.debug(
393
"Recording password use",
394
login.QueryInterface(Ci.nsILoginMetaInfo).guid
395
);
396
this._storage.recordPasswordUse(login);
397
},
398
399
/**
400
* Get a dump of all stored logins. Used by the login manager UI.
401
*
402
* @return {nsILoginInfo[]} - If there are no logins, the array is empty.
403
*/
404
getAllLogins() {
405
log.debug("Getting a list of all logins");
406
return this._storage.getAllLogins();
407
},
408
409
/**
410
* Get a dump of all stored logins asynchronously. Used by the login manager UI.
411
*
412
* @return {nsILoginInfo[]} - If there are no logins, the array is empty.
413
*/
414
async getAllLoginsAsync() {
415
log.debug("Getting a list of all logins asynchronously");
416
return this._storage.getAllLoginsAsync();
417
},
418
419
/**
420
* Remove all stored logins.
421
*/
422
removeAllLogins() {
423
log.debug("Removing all logins");
424
this._storage.removeAllLogins();
425
},
426
427
/**
428
* Get a list of all origins for which logins are disabled.
429
*
430
* @param {Number} count - only needed for XPCOM.
431
*
432
* @return {String[]} of disabled origins. If there are no disabled origins,
433
* the array is empty.
434
*/
435
getAllDisabledHosts() {
436
log.debug("Getting a list of all disabled origins");
437
438
let disabledHosts = [];
439
for (let perm of Services.perms.all) {
440
if (
441
perm.type == PERMISSION_SAVE_LOGINS &&
442
perm.capability == Services.perms.DENY_ACTION
443
) {
444
disabledHosts.push(perm.principal.URI.displayPrePath);
445
}
446
}
447
448
log.debug(
449
"getAllDisabledHosts: returning",
450
disabledHosts.length,
451
"disabled hosts."
452
);
453
return disabledHosts;
454
},
455
456
/**
457
* Search for the known logins for entries matching the specified criteria.
458
*/
459
findLogins(origin, formActionOrigin, httpRealm) {
460
log.debug(
461
"Searching for logins matching origin:",
462
origin,
463
"formActionOrigin:",
464
formActionOrigin,
465
"httpRealm:",
466
httpRealm
467
);
468
469
return this._storage.findLogins(origin, formActionOrigin, httpRealm);
470
},
471
472
async searchLoginsAsync(matchData) {
473
log.debug("searchLoginsAsync:", matchData);
474
475
if (!matchData.origin) {
476
throw new Error("searchLoginsAsync: An `origin` is required");
477
}
478
479
return this._storage.searchLoginsAsync(matchData);
480
},
481
482
/**
483
* @return {nsILoginInfo[]} which are decrypted.
484
*/
485
searchLogins(matchData) {
486
log.debug("Searching for logins");
487
488
matchData.QueryInterface(Ci.nsIPropertyBag2);
489
if (!matchData.hasKey("guid")) {
490
if (!matchData.hasKey("origin")) {
491
log.warn("searchLogins: An `origin` is recommended");
492
}
493
}
494
495
return this._storage.searchLogins(matchData);
496
},
497
498
/**
499
* Search for the known logins for entries matching the specified criteria,
500
* returns only the count.
501
*/
502
countLogins(origin, formActionOrigin, httpRealm) {
503
log.debug(
504
"Counting logins matching origin:",
505
origin,
506
"formActionOrigin:",
507
formActionOrigin,
508
"httpRealm:",
509
httpRealm
510
);
511
512
return this._storage.countLogins(origin, formActionOrigin, httpRealm);
513
},
514
515
get uiBusy() {
516
return this._storage.uiBusy;
517
},
518
519
get isLoggedIn() {
520
return this._storage.isLoggedIn;
521
},
522
523
/**
524
* Check to see if user has disabled saving logins for the origin.
525
*/
526
getLoginSavingEnabled(origin) {
527
log.debug("Checking if logins to", origin, "can be saved.");
528
if (!LoginHelper.enabled) {
529
return false;
530
}
531
532
let uri = Services.io.newURI(origin);
533
let principal = Services.scriptSecurityManager.createContentPrincipal(
534
uri,
535
{}
536
);
537
return (
538
Services.perms.testPermissionFromPrincipal(
539
principal,
540
PERMISSION_SAVE_LOGINS
541
) != Services.perms.DENY_ACTION
542
);
543
},
544
545
/**
546
* Enable or disable storing logins for the specified origin.
547
*/
548
setLoginSavingEnabled(origin, enabled) {
549
// Throws if there are bogus values.
550
LoginHelper.checkOriginValue(origin);
551
552
let uri = Services.io.newURI(origin);
553
let principal = Services.scriptSecurityManager.createContentPrincipal(
554
uri,
555
{}
556
);
557
if (enabled) {
558
Services.perms.removeFromPrincipal(principal, PERMISSION_SAVE_LOGINS);
559
} else {
560
Services.perms.addFromPrincipal(
561
principal,
562
PERMISSION_SAVE_LOGINS,
563
Services.perms.DENY_ACTION
564
);
565
}
566
567
log.debug("Login saving for", origin, "now enabled?", enabled);
568
LoginHelper.notifyStorageChanged(
569
enabled ? "hostSavingEnabled" : "hostSavingDisabled",
570
origin
571
);
572
},
573
}; // end of LoginManager implementation
574
575
const EXPORTED_SYMBOLS = ["LoginManager"];