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
/**
6
* Contains functions shared by different Login Manager components.
7
*
8
* This JavaScript module exists in order to share code between the different
9
* XPCOM components that constitute the Login Manager, including implementations
10
* of nsILoginManager and nsILoginManagerStorage.
11
*/
12
13
"use strict";
14
15
const EXPORTED_SYMBOLS = ["LoginHelper"];
16
17
// Globals
18
19
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
20
const { XPCOMUtils } = ChromeUtils.import(
22
);
23
24
/**
25
* Contains functions shared by different Login Manager components.
26
*/
27
this.LoginHelper = {
28
debug: null,
29
enabled: null,
30
formlessCaptureEnabled: null,
31
generationAvailable: null,
32
generationEnabled: null,
33
insecureAutofill: null,
34
managementURI: null,
35
privateBrowsingCaptureEnabled: null,
36
schemeUpgrades: null,
37
showAutoCompleteFooter: null,
38
39
init() {
40
// Watch for pref changes to update cached pref values.
41
Services.prefs.addObserver("signon.", () => this.updateSignonPrefs());
42
this.updateSignonPrefs();
43
Services.telemetry.setEventRecordingEnabled("pwmgr", true);
44
},
45
46
updateSignonPrefs() {
47
this.autofillForms = Services.prefs.getBoolPref("signon.autofillForms");
48
this.autofillAutocompleteOff = Services.prefs.getBoolPref(
49
"signon.autofillForms.autocompleteOff"
50
);
51
this.debug = Services.prefs.getBoolPref("signon.debug");
52
this.enabled = Services.prefs.getBoolPref("signon.rememberSignons");
53
this.formlessCaptureEnabled = Services.prefs.getBoolPref(
54
"signon.formlessCapture.enabled"
55
);
56
this.generationAvailable = Services.prefs.getBoolPref(
57
"signon.generation.available"
58
);
59
this.generationEnabled = Services.prefs.getBoolPref(
60
"signon.generation.enabled"
61
);
62
this.insecureAutofill = Services.prefs.getBoolPref(
63
"signon.autofillForms.http"
64
);
65
this.managementURI = Services.prefs.getStringPref(
66
"signon.management.overrideURI",
67
null
68
);
69
this.privateBrowsingCaptureEnabled = Services.prefs.getBoolPref(
70
"signon.privateBrowsingCapture.enabled"
71
);
72
this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
73
this.showAutoCompleteFooter = Services.prefs.getBoolPref(
74
"signon.showAutoCompleteFooter"
75
);
76
this.storeWhenAutocompleteOff = Services.prefs.getBoolPref(
77
"signon.storeWhenAutocompleteOff"
78
);
79
},
80
81
createLogger(aLogPrefix) {
82
let getMaxLogLevel = () => {
83
return this.debug ? "Debug" : "Warn";
84
};
85
86
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
87
let consoleOptions = {
88
maxLogLevel: getMaxLogLevel(),
89
prefix: aLogPrefix,
90
};
91
let logger = console.createInstance(consoleOptions);
92
93
// Watch for pref changes and update this.debug and the maxLogLevel for created loggers
94
Services.prefs.addObserver("signon.debug", () => {
95
this.debug = Services.prefs.getBoolPref("signon.debug");
96
if (logger) {
97
logger.maxLogLevel = getMaxLogLevel();
98
}
99
});
100
101
return logger;
102
},
103
104
/**
105
* Due to the way the signons2.txt file is formatted, we need to make
106
* sure certain field values or characters do not cause the file to
107
* be parsed incorrectly. Reject origins that we can't store correctly.
108
*
109
* @throws String with English message in case validation failed.
110
*/
111
checkOriginValue(aOrigin) {
112
// Nulls are invalid, as they don't round-trip well. Newlines are also
113
// invalid for any field stored as plaintext, and an origin made of a
114
// single dot cannot be stored in the legacy format.
115
if (
116
aOrigin == "." ||
117
aOrigin.includes("\r") ||
118
aOrigin.includes("\n") ||
119
aOrigin.includes("\0")
120
) {
121
throw new Error("Invalid origin");
122
}
123
},
124
125
/**
126
* Due to the way the signons2.txt file is formatted, we need to make
127
* sure certain field values or characters do not cause the file to
128
* be parsed incorrectly. Reject logins that we can't store correctly.
129
*
130
* @throws String with English message in case validation failed.
131
*/
132
checkLoginValues(aLogin) {
133
function badCharacterPresent(l, c) {
134
return (
135
(l.formActionOrigin && l.formActionOrigin.includes(c)) ||
136
(l.httpRealm && l.httpRealm.includes(c)) ||
137
l.origin.includes(c) ||
138
l.usernameField.includes(c) ||
139
l.passwordField.includes(c)
140
);
141
}
142
143
// Nulls are invalid, as they don't round-trip well.
144
// Mostly not a formatting problem, although ".\0" can be quirky.
145
if (badCharacterPresent(aLogin, "\0")) {
146
throw new Error("login values can't contain nulls");
147
}
148
149
// In theory these nulls should just be rolled up into the encrypted
150
// values, but nsISecretDecoderRing doesn't use nsStrings, so the
151
// nulls cause truncation. Check for them here just to avoid
152
// unexpected round-trip surprises.
153
if (aLogin.username.includes("\0") || aLogin.password.includes("\0")) {
154
throw new Error("login values can't contain nulls");
155
}
156
157
// Newlines are invalid for any field stored as plaintext.
158
if (
159
badCharacterPresent(aLogin, "\r") ||
160
badCharacterPresent(aLogin, "\n")
161
) {
162
throw new Error("login values can't contain newlines");
163
}
164
165
// A line with just a "." can have special meaning.
166
if (aLogin.usernameField == "." || aLogin.formActionOrigin == ".") {
167
throw new Error("login values can't be periods");
168
}
169
170
// An origin with "\ \(" won't roundtrip.
171
// eg host="foo (", realm="bar" --> "foo ( (bar)"
172
// vs host="foo", realm=" (bar" --> "foo ( (bar)"
173
if (aLogin.origin.includes(" (")) {
174
throw new Error("bad parens in origin");
175
}
176
},
177
178
/**
179
* Returns a new XPCOM property bag with the provided properties.
180
*
181
* @param {Object} aProperties
182
* Each property of this object is copied to the property bag. This
183
* parameter can be omitted to return an empty property bag.
184
*
185
* @return A new property bag, that is an instance of nsIWritablePropertyBag,
186
* nsIWritablePropertyBag2, nsIPropertyBag, and nsIPropertyBag2.
187
*/
188
newPropertyBag(aProperties) {
189
let propertyBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
190
Ci.nsIWritablePropertyBag
191
);
192
if (aProperties) {
193
for (let [name, value] of Object.entries(aProperties)) {
194
propertyBag.setProperty(name, value);
195
}
196
}
197
return propertyBag
198
.QueryInterface(Ci.nsIPropertyBag)
199
.QueryInterface(Ci.nsIPropertyBag2)
200
.QueryInterface(Ci.nsIWritablePropertyBag2);
201
},
202
203
/**
204
* Helper to avoid the property bags when calling
205
* Services.logins.searchLogins from JS.
206
*
207
* @param {Object} aSearchOptions - A regular JS object to copy to a property bag before searching
208
* @return {nsILoginInfo[]} - The result of calling searchLogins.
209
*/
210
searchLoginsWithObject(aSearchOptions) {
211
return Services.logins.searchLogins(this.newPropertyBag(aSearchOptions));
212
},
213
214
/**
215
* @param {string} aURL
216
* @returns {string} which is the hostPort of aURL if supported by the scheme
217
* otherwise, returns the original aURL.
218
*/
219
maybeGetHostPortForURL(aURL) {
220
try {
221
let uri = Services.io.newURI(aURL);
222
return uri.hostPort;
223
} catch (ex) {
224
// No need to warn for javascript:/data:/about:/chrome:/etc.
225
}
226
return aURL;
227
},
228
229
/**
230
* Get the parts of the URL we want for identification.
231
* Strip out things like the userPass portion and handle javascript:.
232
*/
233
getLoginOrigin(uriString, allowJS) {
234
let realm = "";
235
try {
236
let uri = Services.io.newURI(uriString);
237
238
if (allowJS && uri.scheme == "javascript") {
239
return "javascript:";
240
}
241
// TODO: Bug 1559205 - Add support for moz-proxy
242
243
// Build this manually instead of using prePath to avoid including the userPass portion.
244
realm = uri.scheme + "://" + uri.displayHostPort;
245
} catch (e) {
246
// bug 159484 - disallow url types that don't support a hostPort.
247
// (although we handle "javascript:..." as a special case above.)
248
log.warn("Couldn't parse origin for", uriString, e);
249
realm = null;
250
}
251
252
return realm;
253
},
254
255
getFormActionOrigin(form) {
256
let uriString = form.action;
257
258
// A blank or missing action submits to where it came from.
259
if (uriString == "") {
260
// ala bug 297761
261
uriString = form.baseURI;
262
}
263
264
return this.getLoginOrigin(uriString, true);
265
},
266
267
/**
268
* @param {String} aLoginOrigin - An origin value from a stored login's
269
* origin or formActionOrigin properties.
270
* @param {String} aSearchOrigin - The origin that was are looking to match
271
* with aLoginOrigin. This would normally come
272
* from a form or page that we are considering.
273
* @param {nsILoginFindOptions} aOptions - Options to affect whether the origin
274
* from the login (aLoginOrigin) is a
275
* match for the origin we're looking
276
* for (aSearchOrigin).
277
*/
278
isOriginMatching(
279
aLoginOrigin,
280
aSearchOrigin,
281
aOptions = {
282
schemeUpgrades: false,
283
acceptWildcardMatch: false,
284
acceptDifferentSubdomains: false,
285
}
286
) {
287
if (aLoginOrigin == aSearchOrigin) {
288
return true;
289
}
290
291
if (!aOptions) {
292
return false;
293
}
294
295
if (aOptions.acceptWildcardMatch && aLoginOrigin == "") {
296
return true;
297
}
298
299
try {
300
let loginURI = Services.io.newURI(aLoginOrigin);
301
let searchURI = Services.io.newURI(aSearchOrigin);
302
let schemeMatches =
303
loginURI.scheme == "http" && searchURI.scheme == "https";
304
305
if (aOptions.acceptDifferentSubdomains) {
306
let loginBaseDomain = Services.eTLD.getBaseDomain(loginURI);
307
let searchBaseDomain = Services.eTLD.getBaseDomain(searchURI);
308
if (
309
loginBaseDomain == searchBaseDomain &&
310
(loginURI.scheme == searchURI.scheme ||
311
(aOptions.schemeUpgrades && schemeMatches))
312
) {
313
return true;
314
}
315
}
316
317
if (
318
aOptions.schemeUpgrades &&
319
loginURI.host == searchURI.host &&
320
schemeMatches &&
321
loginURI.port == searchURI.port
322
) {
323
return true;
324
}
325
} catch (ex) {
326
// newURI will throw for some values e.g. chrome://FirefoxAccounts
327
// uri.host and uri.port will throw for some values e.g. javascript:
328
return false;
329
}
330
331
return false;
332
},
333
334
doLoginsMatch(
335
aLogin1,
336
aLogin2,
337
{ ignorePassword = false, ignoreSchemes = false }
338
) {
339
if (
340
aLogin1.httpRealm != aLogin2.httpRealm ||
341
aLogin1.username != aLogin2.username
342
) {
343
return false;
344
}
345
346
if (!ignorePassword && aLogin1.password != aLogin2.password) {
347
return false;
348
}
349
350
if (ignoreSchemes) {
351
let login1HostPort = this.maybeGetHostPortForURL(aLogin1.origin);
352
let login2HostPort = this.maybeGetHostPortForURL(aLogin2.origin);
353
if (login1HostPort != login2HostPort) {
354
return false;
355
}
356
357
if (
358
aLogin1.formActionOrigin != "" &&
359
aLogin2.formActionOrigin != "" &&
360
this.maybeGetHostPortForURL(aLogin1.formActionOrigin) !=
361
this.maybeGetHostPortForURL(aLogin2.formActionOrigin)
362
) {
363
return false;
364
}
365
} else {
366
if (aLogin1.origin != aLogin2.origin) {
367
return false;
368
}
369
370
// If either formActionOrigin is blank (but not null), then match.
371
if (
372
aLogin1.formActionOrigin != "" &&
373
aLogin2.formActionOrigin != "" &&
374
aLogin1.formActionOrigin != aLogin2.formActionOrigin
375
) {
376
return false;
377
}
378
}
379
380
// The .usernameField and .passwordField values are ignored.
381
382
return true;
383
},
384
385
/**
386
* Creates a new login object that results by modifying the given object with
387
* the provided data.
388
*
389
* @param aOldStoredLogin
390
* Existing nsILoginInfo object to modify.
391
* @param aNewLoginData
392
* The new login values, either as nsILoginInfo or nsIProperyBag.
393
*
394
* @return The newly created nsILoginInfo object.
395
*
396
* @throws String with English message in case validation failed.
397
*/
398
buildModifiedLogin(aOldStoredLogin, aNewLoginData) {
399
function bagHasProperty(aPropName) {
400
try {
401
aNewLoginData.getProperty(aPropName);
402
return true;
403
} catch (ex) {}
404
return false;
405
}
406
407
aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
408
409
let newLogin;
410
if (aNewLoginData instanceof Ci.nsILoginInfo) {
411
// Clone the existing login to get its nsILoginMetaInfo, then init it
412
// with the replacement nsILoginInfo data from the new login.
413
newLogin = aOldStoredLogin.clone();
414
newLogin.init(
415
aNewLoginData.origin,
416
aNewLoginData.formActionOrigin,
417
aNewLoginData.httpRealm,
418
aNewLoginData.username,
419
aNewLoginData.password,
420
aNewLoginData.usernameField,
421
aNewLoginData.passwordField
422
);
423
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
424
425
// Automatically update metainfo when password is changed.
426
if (newLogin.password != aOldStoredLogin.password) {
427
newLogin.timePasswordChanged = Date.now();
428
}
429
} else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
430
// Clone the existing login, along with all its properties.
431
newLogin = aOldStoredLogin.clone();
432
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
433
434
// Automatically update metainfo when password is changed.
435
// (Done before the main property updates, lest the caller be
436
// explicitly updating both .password and .timePasswordChanged)
437
if (bagHasProperty("password")) {
438
let newPassword = aNewLoginData.getProperty("password");
439
if (newPassword != aOldStoredLogin.password) {
440
newLogin.timePasswordChanged = Date.now();
441
}
442
}
443
444
for (let prop of aNewLoginData.enumerator) {
445
switch (prop.name) {
446
// nsILoginInfo (fall through)
447
case "origin":
448
case "httpRealm":
449
case "formActionOrigin":
450
case "username":
451
case "password":
452
case "usernameField":
453
case "passwordField":
454
// nsILoginMetaInfo (fall through)
455
case "guid":
456
case "timeCreated":
457
case "timeLastUsed":
458
case "timePasswordChanged":
459
case "timesUsed":
460
newLogin[prop.name] = prop.value;
461
break;
462
463
// Fake property, allows easy incrementing.
464
case "timesUsedIncrement":
465
newLogin.timesUsed += prop.value;
466
break;
467
468
// Fail if caller requests setting an unknown property.
469
default:
470
throw new Error("Unexpected propertybag item: " + prop.name);
471
}
472
}
473
} else {
474
throw new Error("newLoginData needs an expected interface!");
475
}
476
477
// Sanity check the login
478
if (newLogin.origin == null || !newLogin.origin.length) {
479
throw new Error("Can't add a login with a null or empty origin.");
480
}
481
482
// For logins w/o a username, set to "", not null.
483
if (newLogin.username == null) {
484
throw new Error("Can't add a login with a null username.");
485
}
486
487
if (newLogin.password == null || !newLogin.password.length) {
488
throw new Error("Can't add a login with a null or empty password.");
489
}
490
491
if (newLogin.formActionOrigin || newLogin.formActionOrigin == "") {
492
// We have a form submit URL. Can't have a HTTP realm.
493
if (newLogin.httpRealm != null) {
494
throw new Error(
495
"Can't add a login with both a httpRealm and formActionOrigin."
496
);
497
}
498
} else if (newLogin.httpRealm) {
499
// We have a HTTP realm. Can't have a form submit URL.
500
if (newLogin.formActionOrigin != null) {
501
throw new Error(
502
"Can't add a login with both a httpRealm and formActionOrigin."
503
);
504
}
505
} else {
506
// Need one or the other!
507
throw new Error(
508
"Can't add a login without a httpRealm or formActionOrigin."
509
);
510
}
511
512
// Throws if there are bogus values.
513
this.checkLoginValues(newLogin);
514
515
return newLogin;
516
},
517
518
/**
519
* Remove http: logins when there is an https: login with the same username and hostPort.
520
* Sort order is preserved.
521
*
522
* @param {nsILoginInfo[]} logins
523
* A list of logins we want to process for shadowing.
524
* @returns {nsILoginInfo[]} A subset of of the passed logins.
525
*/
526
shadowHTTPLogins(logins) {
527
/**
528
* Map a (hostPort, username) to a boolean indicating whether `logins`
529
* contains an https: login for that combo.
530
*/
531
let hasHTTPSByHostPortUsername = new Map();
532
for (let login of logins) {
533
let key = this.getUniqueKeyForLogin(login, ["hostPort", "username"]);
534
let hasHTTPSlogin = hasHTTPSByHostPortUsername.get(key) || false;
535
let loginURI = Services.io.newURI(login.origin);
536
hasHTTPSByHostPortUsername.set(
537
key,
538
loginURI.scheme == "https" || hasHTTPSlogin
539
);
540
}
541
542
return logins.filter(login => {
543
let key = this.getUniqueKeyForLogin(login, ["hostPort", "username"]);
544
let loginURI = Services.io.newURI(login.origin);
545
if (loginURI.scheme == "http" && hasHTTPSByHostPortUsername.get(key)) {
546
// If this is an http: login and we have an https: login for the
547
// (hostPort, username) combo then remove it.
548
return false;
549
}
550
return true;
551
});
552
},
553
554
/**
555
* Generate a unique key string from a login.
556
* @param {nsILoginInfo} login
557
* @param {string[]} uniqueKeys containing nsILoginInfo attribute names or "hostPort"
558
* @returns {string} to use as a key in a Map
559
*/
560
getUniqueKeyForLogin(login, uniqueKeys) {
561
const KEY_DELIMITER = ":";
562
return uniqueKeys.reduce((prev, key) => {
563
let val = null;
564
if (key == "hostPort") {
565
val = Services.io.newURI(login.origin).hostPort;
566
} else {
567
val = login[key];
568
}
569
570
return prev + KEY_DELIMITER + val;
571
}, "");
572
},
573
574
/**
575
* Removes duplicates from a list of logins while preserving the sort order.
576
*
577
* @param {nsILoginInfo[]} logins
578
* A list of logins we want to deduplicate.
579
* @param {string[]} [uniqueKeys = ["username", "password"]]
580
* A list of login attributes to use as unique keys for the deduplication.
581
* @param {string[]} [resolveBy = ["timeLastUsed"]]
582
* Ordered array of keyword strings used to decide which of the
583
* duplicates should be used. "scheme" would prefer the login that has
584
* a scheme matching `preferredOrigin`'s if there are two logins with
585
* the same `uniqueKeys`. The default preference to distinguish two
586
* logins is `timeLastUsed`. If there is no preference between two
587
* logins, the first one found wins.
588
* @param {string} [preferredOrigin = undefined]
589
* String representing the origin to use for preferring one login over
590
* another when they are dupes. This is used with "scheme" for
591
* `resolveBy` so the scheme from this origin will be preferred.
592
* @param {string} [preferredFormActionOrigin = undefined]
593
* String representing the action origin to use for preferring one login over
594
* another when they are dupes. This is used with "actionOrigin" for
595
* `resolveBy` so the scheme from this action origin will be preferred.
596
*
597
* @returns {nsILoginInfo[]} list of unique logins.
598
*/
599
dedupeLogins(
600
logins,
601
uniqueKeys = ["username", "password"],
602
resolveBy = ["timeLastUsed"],
603
preferredOrigin = undefined,
604
preferredFormActionOrigin = undefined
605
) {
606
if (!preferredOrigin) {
607
if (resolveBy.includes("scheme")) {
608
throw new Error(
609
"dedupeLogins: `preferredOrigin` is required in order to " +
610
"prefer schemes which match it."
611
);
612
}
613
if (resolveBy.includes("subdomain")) {
614
throw new Error(
615
"dedupeLogins: `preferredOrigin` is required in order to " +
616
"prefer subdomains which match it."
617
);
618
}
619
}
620
621
let preferredOriginScheme;
622
if (preferredOrigin) {
623
try {
624
preferredOriginScheme = Services.io.newURI(preferredOrigin).scheme;
625
} catch (ex) {
626
// Handle strings that aren't valid URIs e.g. chrome://FirefoxAccounts
627
}
628
}
629
630
if (!preferredOriginScheme && resolveBy.includes("scheme")) {
631
log.warn(
632
"dedupeLogins: Deduping with a scheme preference but couldn't " +
633
"get the preferred origin scheme."
634
);
635
}
636
637
// We use a Map to easily lookup logins by their unique keys.
638
let loginsByKeys = new Map();
639
640
/**
641
* @return {bool} whether `login` is preferred over its duplicate (considering `uniqueKeys`)
642
* `existingLogin`.
643
*
644
* `resolveBy` is a sorted array so we can return true the first time `login` is preferred
645
* over the existingLogin.
646
*/
647
function isLoginPreferred(existingLogin, login) {
648
if (!resolveBy || !resolveBy.length) {
649
// If there is no preference, prefer the existing login.
650
return false;
651
}
652
653
for (let preference of resolveBy) {
654
switch (preference) {
655
case "actionOrigin": {
656
if (!preferredFormActionOrigin) {
657
break;
658
}
659
if (
660
LoginHelper.isOriginMatching(
661
existingLogin.formActionOrigin,
662
preferredFormActionOrigin,
663
{ schemeUpgrades: LoginHelper.schemeUpgrades }
664
) &&
665
!LoginHelper.isOriginMatching(
666
login.formActionOrigin,
667
preferredFormActionOrigin,
668
{ schemeUpgrades: LoginHelper.schemeUpgrades }
669
)
670
) {
671
return false;
672
}
673
break;
674
}
675
case "scheme": {
676
if (!preferredOriginScheme) {
677
break;
678
}
679
680
try {
681
// Only `origin` is currently considered
682
let existingLoginURI = Services.io.newURI(existingLogin.origin);
683
let loginURI = Services.io.newURI(login.origin);
684
// If the schemes of the two logins are the same or neither match the
685
// preferredOriginScheme then we have no preference and look at the next resolveBy.
686
if (
687
loginURI.scheme == existingLoginURI.scheme ||
688
(loginURI.scheme != preferredOriginScheme &&
689
existingLoginURI.scheme != preferredOriginScheme)
690
) {
691
break;
692
}
693
694
return loginURI.scheme == preferredOriginScheme;
695
} catch (ex) {
696
// Some URLs aren't valid nsIURI (e.g. chrome://FirefoxAccounts)
697
log.debug(
698
"dedupeLogins/shouldReplaceExisting: Error comparing schemes:",
699
existingLogin.origin,
700
login.origin,
701
"preferredOrigin:",
702
preferredOrigin,
703
ex
704
);
705
}
706
break;
707
}
708
case "subdomain": {
709
// Replace the existing login only if the new login is an exact match on the host.
710
let existingLoginURI = Services.io.newURI(existingLogin.origin);
711
let newLoginURI = Services.io.newURI(login.origin);
712
let preferredOriginURI = Services.io.newURI(preferredOrigin);
713
if (
714
existingLoginURI.hostPort != preferredOriginURI.hostPort &&
715
newLoginURI.hostPort == preferredOriginURI.hostPort
716
) {
717
return true;
718
}
719
if (
720
existingLoginURI.host != preferredOriginURI.host &&
721
newLoginURI.host == preferredOriginURI.host
722
) {
723
return true;
724
}
725
break;
726
}
727
case "timeLastUsed":
728
case "timePasswordChanged": {
729
// If we find a more recent login for the same key, replace the existing one.
730
let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo)[
731
preference
732
];
733
let storedLoginDate = existingLogin.QueryInterface(
734
Ci.nsILoginMetaInfo
735
)[preference];
736
if (loginDate == storedLoginDate) {
737
break;
738
}
739
740
return loginDate > storedLoginDate;
741
}
742
default: {
743
throw new Error(
744
"dedupeLogins: Invalid resolveBy preference: " + preference
745
);
746
}
747
}
748
}
749
750
return false;
751
}
752
753
for (let login of logins) {
754
let key = this.getUniqueKeyForLogin(login, uniqueKeys);
755
756
if (loginsByKeys.has(key)) {
757
if (!isLoginPreferred(loginsByKeys.get(key), login)) {
758
// If there is no preference for the new login, use the existing one.
759
continue;
760
}
761
}
762
loginsByKeys.set(key, login);
763
}
764
765
// Return the map values in the form of an array.
766
return [...loginsByKeys.values()];
767
},
768
769
/**
770
* Open the password manager window.
771
*
772
* @param {Window} window
773
* the window from where we want to open the dialog
774
*
775
* @param {object?} args
776
* params for opening the password manager
777
* @param {string} [args.filterString=""]
778
* the domain (not origin) to pass to the login manager dialog
779
* to pre-filter the results
780
* @param {string} args.entryPoint
781
* The name of the entry point, used for telemetry
782
*/
783
openPasswordManager(window, { filterString = "", entryPoint = "" } = {}) {
784
Services.telemetry.recordEvent("pwmgr", "open_management", entryPoint);
785
if (this.managementURI && window.openTrustedLinkIn) {
786
let managementURL = this.managementURI.replace(
787
"%DOMAIN%",
788
window.encodeURIComponent(filterString)
789
);
790
window.openTrustedLinkIn(managementURL, "tab");
791
return;
792
}
793
let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
794
if (win) {
795
win.setFilter(filterString);
796
win.focus();
797
} else {
798
window.openDialog(
800
"Toolkit:PasswordManager",
801
"",
802
{ filterString }
803
);
804
}
805
},
806
807
/**
808
* Checks if a field type is username compatible.
809
*
810
* @param {Element} element
811
* the field we want to check.
812
*
813
* @returns {Boolean} true if the field type is one
814
* of the username types.
815
*/
816
isUsernameFieldType(element) {
817
if (ChromeUtils.getClassName(element) !== "HTMLInputElement") {
818
return false;
819
}
820
821
if (!element.isConnected) {
822
// If the element isn't connected then it isn't visible to the user so
823
// shouldn't be considered. It must have been connected in the past.
824
return false;
825
}
826
827
let fieldType = element.hasAttribute("type")
828
? element.getAttribute("type").toLowerCase()
829
: element.type;
830
if (
831
!(
832
fieldType == "text" ||
833
fieldType == "email" ||
834
fieldType == "url" ||
835
fieldType == "tel" ||
836
fieldType == "number"
837
)
838
) {
839
return false;
840
}
841
842
let acFieldName = element.getAutocompleteInfo().fieldName;
843
if (
844
!(
845
acFieldName == "username" ||
846
// Bug 1540154: Some sites use tel/email on their username fields.
847
acFieldName == "email" ||
848
acFieldName == "tel" ||
849
acFieldName == "tel-national" ||
850
acFieldName == "off" ||
851
acFieldName == "on" ||
852
acFieldName == ""
853
)
854
) {
855
return false;
856
}
857
return true;
858
},
859
860
/**
861
* For each login, add the login to the password manager if a similar one
862
* doesn't already exist. Merge it otherwise with the similar existing ones.
863
*
864
* @param {Object[]} loginDatas - For each login, the data that needs to be added.
865
* @returns {nsILoginInfo[]} the newly added logins, filtered if no login was added.
866
*/
867
async maybeImportLogins(loginDatas) {
868
let loginsToAdd = [];
869
let loginMap = new Map();
870
for (let loginData of loginDatas) {
871
// create a new login
872
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
873
Ci.nsILoginInfo
874
);
875
login.init(
876
loginData.origin,
877
loginData.formActionOrigin ||
878
(typeof loginData.httpRealm == "string" ? null : ""),
879
typeof loginData.httpRealm == "string" ? loginData.httpRealm : null,
880
loginData.username,
881
loginData.password,
882
loginData.usernameElement || "",
883
loginData.passwordElement || ""
884
);
885
886
login.QueryInterface(Ci.nsILoginMetaInfo);
887
login.timeCreated = loginData.timeCreated;
888
login.timeLastUsed = loginData.timeLastUsed || loginData.timeCreated;
889
login.timePasswordChanged =
890
loginData.timePasswordChanged || loginData.timeCreated;
891
login.timesUsed = loginData.timesUsed || 1;
892
893
try {
894
// Ensure we only send checked logins through, since the validation is optimized
895
// out from the bulk APIs below us.
896
this.checkLoginValues(login);
897
} catch (e) {
898
Cu.reportError(e);
899
continue;
900
}
901
902
// First, we need to check the logins that we've already decided to add, to
903
// see if this is a duplicate. This should mirror the logic below for
904
// existingLogins, but only for the array of logins we're adding.
905
let newLogins = loginMap.get(login.origin) || [];
906
if (!newLogins) {
907
loginMap.set(login.origin, newLogins);
908
} else {
909
if (newLogins.some(l => login.matches(l, false /* ignorePassword */))) {
910
continue;
911
}
912
let foundMatchingNewLogin = false;
913
for (let newLogin of newLogins) {
914
if (login.username == newLogin.username) {
915
foundMatchingNewLogin = true;
916
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
917
if (
918
(login.password != newLogin.password) &
919
(login.timePasswordChanged > newLogin.timePasswordChanged)
920
) {
921
// if a login with the same username and different password already exists and it's older
922
// than the current one, update its password and timestamp.
923
newLogin.password = login.password;
924
newLogin.timePasswordChanged = login.timePasswordChanged;
925
}
926
}
927
}
928
929
if (foundMatchingNewLogin) {
930
continue;
931
}
932
}
933
934
// While here we're passing formActionOrigin and httpRealm, they could be empty/null and get
935
// ignored in that case, leading to multiple logins for the same username.
936
let existingLogins = Services.logins.findLogins(
937
login.origin,
938
login.formActionOrigin,
939
login.httpRealm
940
);
941
// Check for an existing login that matches *including* the password.
942
// If such a login exists, we do not need to add a new login.
943
if (
944
existingLogins.some(l => login.matches(l, false /* ignorePassword */))
945
) {
946
continue;
947
}
948
// Now check for a login with the same username, where it may be that we have an
949
// updated password.
950
let foundMatchingLogin = false;
951
for (let existingLogin of existingLogins) {
952
if (login.username == existingLogin.username) {
953
foundMatchingLogin = true;
954
existingLogin.QueryInterface(Ci.nsILoginMetaInfo);
955
if (
956
(login.password != existingLogin.password) &
957
(login.timePasswordChanged > existingLogin.timePasswordChanged)
958
) {
959
// if a login with the same username and different password already exists and it's older
960
// than the current one, update its password and timestamp.
961
let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
962
Ci.nsIWritablePropertyBag
963
);
964
propBag.setProperty("password", login.password);
965
propBag.setProperty(
966
"timePasswordChanged",
967
login.timePasswordChanged
968
);
969
Services.logins.modifyLogin(existingLogin, propBag);
970
}
971
}
972
}
973
// if the new login is an update or is older than an exiting login, don't add it.
974
if (foundMatchingLogin) {
975
continue;
976
}
977
978
newLogins.push(login);
979
loginsToAdd.push(login);
980
}
981
if (!loginsToAdd.length) {
982
return [];
983
}
984
return Services.logins.addLogins(loginsToAdd);
985
},
986
987
/**
988
* Convert an array of nsILoginInfo to vanilla JS objects suitable for
989
* sending over IPC.
990
*
991
* NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
992
*/
993
loginsToVanillaObjects(logins) {
994
return logins.map(this.loginToVanillaObject);
995
},
996
997
/**
998
* Same as above, but for a single login.
999
*/
1000
loginToVanillaObject(login) {
1001
let obj = {};
1002
for (let i in login.QueryInterface(Ci.nsILoginMetaInfo)) {
1003
if (typeof login[i] !== "function") {
1004
obj[i] = login[i];
1005
}
1006
}
1007
1008
return obj;
1009
},
1010
1011
/**
1012
* Convert an object received from IPC into an nsILoginInfo (with guid).
1013
*/
1014
vanillaObjectToLogin(login) {
1015
let formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
1016
Ci.nsILoginInfo
1017
);
1018
// For compatibility for the Lockwise extension we fallback to hostname/formSubmitURL
1019
formLogin.init(
1020
login.origin || login.hostname,
1021
"formSubmitURL" in login ? login.formSubmitURL : login.formActionOrigin,
1022
login.httpRealm,
1023
login.username,
1024
login.password,
1025
login.usernameField,
1026
login.passwordField
1027
);
1028
1029
formLogin.QueryInterface(Ci.nsILoginMetaInfo);
1030
for (let prop of [
1031
"guid",
1032
"timeCreated",
1033
"timeLastUsed",
1034
"timePasswordChanged",
1035
"timesUsed",
1036
]) {
1037
formLogin[prop] = login[prop];
1038
}
1039
return formLogin;
1040
},
1041
1042
/**
1043
* As above, but for an array of objects.
1044
*/
1045
vanillaObjectsToLogins(logins) {
1046
return logins.map(this.vanillaObjectToLogin);
1047
},
1048
1049
/**
1050
* Returns true if the user has a master password set and false otherwise.
1051
*/
1052
isMasterPasswordSet() {
1053
let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
1054
Ci.nsIPK11TokenDB
1055
);
1056
let token = tokenDB.getInternalKeyToken();
1057
return token.hasPassword;
1058
},
1059
1060
/**
1061
* Send a notification when stored data is changed.
1062
*/
1063
notifyStorageChanged(changeType, data) {
1064
let dataObject = data;
1065
// Can't pass a raw JS string or array though notifyObservers(). :-(
1066
if (Array.isArray(data)) {
1067
dataObject = Cc["@mozilla.org/array;1"].createInstance(
1068
Ci.nsIMutableArray
1069
);
1070
for (let i = 0; i < data.length; i++) {
1071
dataObject.appendElement(data[i]);
1072
}
1073
} else if (typeof data == "string") {
1074
dataObject = Cc["@mozilla.org/supports-string;1"].createInstance(
1075
Ci.nsISupportsString
1076
);
1077
dataObject.data = data;
1078
}
1079
Services.obs.notifyObservers(
1080
dataObject,
1081
"passwordmgr-storage-changed",
1082
changeType
1083
);
1084
},
1085
1086
isUserFacingLogin(login) {
1087
return !login.origin.startsWith("chrome://");
1088
},
1089
1090
async getAllUserFacingLogins() {
1091
try {
1092
let logins = await Services.logins.getAllLoginsAsync();
1093
return logins.filter(this.isUserFacingLogin);
1094
} catch (e) {
1095
if (e.result == Cr.NS_ERROR_ABORT) {
1096
// If the user cancels the MP prompt then return no logins.
1097
return [];
1098
}
1099
throw e;
1100
}
1101
},
1102
};
1103
1104
LoginHelper.init();
1105
1106
XPCOMUtils.defineLazyPreferenceGetter(
1107
LoginHelper,
1108
"showInsecureFieldWarning",
1109
"security.insecure_field_warning.contextual.enabled"
1110
);
1111
1112
XPCOMUtils.defineLazyGetter(this, "log", () => {
1113
return LoginHelper.createLogger("LoginHelper");
1114
});