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");
374
return this._storage.removeLogin(login);
375
},
376
377
/**
378
* Change the specified login to match the new login.
379
*/
380
modifyLogin(oldLogin, newLogin) {
381
log.debug("Modifying login");
382
return this._storage.modifyLogin(oldLogin, newLogin);
383
},
384
385
/**
386
* Get a dump of all stored logins. Used by the login manager UI.
387
*
388
* @return {nsILoginInfo[]} - If there are no logins, the array is empty.
389
*/
390
getAllLogins() {
391
log.debug("Getting a list of all logins");
392
return this._storage.getAllLogins();
393
},
394
395
/**
396
* Get a dump of all stored logins asynchronously. Used by the login manager UI.
397
*
398
* @return {nsILoginInfo[]} - If there are no logins, the array is empty.
399
*/
400
async getAllLoginsAsync() {
401
log.debug("Getting a list of all logins asynchronously");
402
return this._storage.getAllLoginsAsync();
403
},
404
405
/**
406
* Remove all stored logins.
407
*/
408
removeAllLogins() {
409
log.debug("Removing all logins");
410
this._storage.removeAllLogins();
411
},
412
413
/**
414
* Get a list of all origins for which logins are disabled.
415
*
416
* @param {Number} count - only needed for XPCOM.
417
*
418
* @return {String[]} of disabled origins. If there are no disabled origins,
419
* the array is empty.
420
*/
421
getAllDisabledHosts() {
422
log.debug("Getting a list of all disabled origins");
423
424
let disabledHosts = [];
425
for (let perm of Services.perms.all) {
426
if (
427
perm.type == PERMISSION_SAVE_LOGINS &&
428
perm.capability == Services.perms.DENY_ACTION
429
) {
430
disabledHosts.push(perm.principal.URI.displayPrePath);
431
}
432
}
433
434
log.debug(
435
"getAllDisabledHosts: returning",
436
disabledHosts.length,
437
"disabled hosts."
438
);
439
return disabledHosts;
440
},
441
442
/**
443
* Search for the known logins for entries matching the specified criteria.
444
*/
445
findLogins(origin, formActionOrigin, httpRealm) {
446
log.debug(
447
"Searching for logins matching origin:",
448
origin,
449
"formActionOrigin:",
450
formActionOrigin,
451
"httpRealm:",
452
httpRealm
453
);
454
455
return this._storage.findLogins(origin, formActionOrigin, httpRealm);
456
},
457
458
async searchLoginsAsync(matchData) {
459
log.debug("searchLoginsAsync:", matchData);
460
return this._storage.searchLoginsAsync(matchData);
461
},
462
463
/**
464
* @return {nsILoginInfo[]} which are decrypted.
465
*/
466
searchLogins(matchData) {
467
log.debug("Searching for logins");
468
469
matchData.QueryInterface(Ci.nsIPropertyBag2);
470
if (!matchData.hasKey("guid")) {
471
if (!matchData.hasKey("origin")) {
472
log.warn("searchLogins: An `origin` is recommended");
473
}
474
}
475
476
return this._storage.searchLogins(matchData);
477
},
478
479
/**
480
* Search for the known logins for entries matching the specified criteria,
481
* returns only the count.
482
*/
483
countLogins(origin, formActionOrigin, httpRealm) {
484
log.debug(
485
"Counting logins matching origin:",
486
origin,
487
"formActionOrigin:",
488
formActionOrigin,
489
"httpRealm:",
490
httpRealm
491
);
492
493
return this._storage.countLogins(origin, formActionOrigin, httpRealm);
494
},
495
496
get uiBusy() {
497
return this._storage.uiBusy;
498
},
499
500
get isLoggedIn() {
501
return this._storage.isLoggedIn;
502
},
503
504
/**
505
* Check to see if user has disabled saving logins for the origin.
506
*/
507
getLoginSavingEnabled(origin) {
508
log.debug("Checking if logins to", origin, "can be saved.");
509
if (!LoginHelper.enabled) {
510
return false;
511
}
512
513
let uri = Services.io.newURI(origin);
514
let principal = Services.scriptSecurityManager.createContentPrincipal(
515
uri,
516
{}
517
);
518
return (
519
Services.perms.testPermissionFromPrincipal(
520
principal,
521
PERMISSION_SAVE_LOGINS
522
) != Services.perms.DENY_ACTION
523
);
524
},
525
526
/**
527
* Enable or disable storing logins for the specified origin.
528
*/
529
setLoginSavingEnabled(origin, enabled) {
530
// Throws if there are bogus values.
531
LoginHelper.checkOriginValue(origin);
532
533
let uri = Services.io.newURI(origin);
534
let principal = Services.scriptSecurityManager.createContentPrincipal(
535
uri,
536
{}
537
);
538
if (enabled) {
539
Services.perms.removeFromPrincipal(principal, PERMISSION_SAVE_LOGINS);
540
} else {
541
Services.perms.addFromPrincipal(
542
principal,
543
PERMISSION_SAVE_LOGINS,
544
Services.perms.DENY_ACTION
545
);
546
}
547
548
log.debug("Login saving for", origin, "now enabled?", enabled);
549
LoginHelper.notifyStorageChanged(
550
enabled ? "hostSavingEnabled" : "hostSavingDisabled",
551
origin
552
);
553
},
554
}; // end of LoginManager implementation
555
556
const EXPORTED_SYMBOLS = ["LoginManager"];