!import
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 *
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
8 *
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
13 *
14 * The Original Code is mozilla.org code.
15 *
16 * The Initial Developer of the Original Code is Mozilla Corporation.
17 * Portions created by the Initial Developer are Copyright (C) 2007
18 * the Initial Developer. All Rights Reserved.
19 *
20 * Contributor(s):
21 * Justin Dolske <dolske@mozilla.com> (original author)
22 *
23 * Alternatively, the contents of this file may be used under the terms of
24 * either the GNU General Public License Version 2 or later (the "GPL"), or
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 * in which case the provisions of the GPL or the LGPL are applicable instead
27 * of those above. If you wish to allow use of your version of this file only
28 * under the terms of either the GPL or the LGPL, and not to allow others to
29 * use your version of this file under the terms of the MPL, indicate your
30 * decision by deleting the provisions above and replace them with the notice
31 * and other provisions required by the GPL or the LGPL. If you do not delete
32 * the provisions above, a recipient may use your version of this file under
33 * the terms of any one of the MPL, the GPL or the LGPL.
34 *
35 * ***** END LICENSE BLOCK ***** */
36
37
38 const Cc = Components.classes;
39 const Ci = Components.interfaces;
40
41 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
42
LoginManagerStorage_legacy
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
43 function LoginManagerStorage_legacy() { };
44
45 LoginManagerStorage_legacy.prototype = {
46
47 classDescription : "LoginManagerStorage_legacy",
48 contractID : "@mozilla.org/login-manager/storage/legacy;1",
49 classID : Components.ID("{e09e4ca6-276b-4bb4-8b71-0635a3a2a007}"),
50 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
51 Ci.nsILoginManagerIEMigrationHelper]),
52
53 __logService : null, // Console logging service, used for debugging.
get__logService
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
54 get _logService() {
55 if (!this.__logService)
56 this.__logService = Cc["@mozilla.org/consoleservice;1"].
57 getService(Ci.nsIConsoleService);
58 return this.__logService;
59 },
60
61 __ioService: null, // IO service for string -> nsIURI conversion
get__ioService
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
62 get _ioService() {
63 if (!this.__ioService)
64 this.__ioService = Cc["@mozilla.org/network/io-service;1"].
65 getService(Ci.nsIIOService);
66 return this.__ioService;
67 },
68
69 __decoderRing : null, // nsSecretDecoderRing service
get__decoderRing
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
70 get _decoderRing() {
71 if (!this.__decoderRing)
72 this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
73 getService(Ci.nsISecretDecoderRing);
74 return this.__decoderRing;
75 },
76
77 __profileDir: null, // nsIFile for the user's profile dir
get__profileDir
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
78 get _profileDir() {
79 if (!this.__profileDir) {
80 var dirService = Cc["@mozilla.org/file/directory_service;1"].
81 getService(Ci.nsIProperties);
82 this.__profileDir = dirService.get("ProfD", Ci.nsIFile);
83 }
84 return this.__profileDir;
85 },
86
87 _prefBranch : null, // Preferences service
88
89 _signonsFile : null, // nsIFile for "signons3.txt" (or whatever pref is)
90 _debug : false, // mirrors signon.debug
91
92 /*
93 * A list of prefs that have been used to specify the filename for storing
94 * logins. (We've used a number over time due to compatibility issues.)
95 * This list is also used by _removeOldSignonsFile() to clean up old files.
96 */
97 _filenamePrefs : ["SignonFileName3", "SignonFileName2", "SignonFileName"],
98
99 /*
100 * Core datastructures
101 *
102 * EG: _logins["http://site.com"][0].password
103 * EG: _disabledHosts["never.site.com"]
104 */
105 _logins : null,
106 _disabledHosts : null,
107
108
109 /*
110 * log
111 *
112 * Internal function for logging debug messages to the Error Console.
113 */
log
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
114 log : function (message) {
115 if (!this._debug)
116 return;
117 dump("PwMgr Storage: " + message + "\n");
118 this._logService.logStringMessage("PwMgr Storage: " + message);
119 },
120
121
122
123
124 /* ==================== Public Methods ==================== */
125
126
127
128
initWithFile
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
129 initWithFile : function(aInputFile, aOutputFile) {
130 this._signonsFile = aInputFile;
131
132 this.init();
133
134 if (aOutputFile) {
135 this._signonsFile = aOutputFile;
136 this._writeFile();
137 }
138 },
139
140 /*
141 * init
142 *
143 * Initialize this storage component and load stored passwords from disk.
144 */
init
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
145 init : function () {
146 this._logins = {};
147 this._disabledHosts = {};
148
149 // Connect to the correct preferences branch.
150 this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
151 getService(Ci.nsIPrefService);
152 this._prefBranch = this._prefBranch.getBranch("signon.");
153 this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
154
155 this._debug = this._prefBranch.getBoolPref("debug");
156
157 // Check to see if the internal PKCS#11 token has been initialized.
158 // If not, set a blank password.
159 var tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
160 getService(Ci.nsIPK11TokenDB);
161
162 var token = tokenDB.getInternalKeyToken();
163 if (token.needsUserInit) {
164 this.log("Initializing key3.db with default blank password.");
165 token.initPassword("");
166 }
167
168 var importFile = null;
169 // If initWithFile is calling us, _signonsFile is already set.
170 if (!this._signonsFile)
171 [this._signonsFile, importFile] = this._getSignonsFile();
172
173 // If we have an import file, do a switcharoo before reading it.
174 if (importFile) {
175 this.log("Importing " + importFile.path);
176
177 var tmp = this._signonsFile;
178 this._signonsFile = importFile;
179 }
180
181 // Read in the stored login data.
182 this._readFile();
183
184 // If we were importing, write back to the normal file.
185 if (importFile) {
186 this._signonsFile = tmp;
187 this._writeFile();
188 }
189 },
190
191
192 /*
193 * addLogin
194 *
195 */
addLogin
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
196 addLogin : function (login) {
197 // Throws if there are bogus values.
198 this._checkLoginValues(login);
199
200 // We rely on using login.wrappedJSObject. addLogin is the
201 // only entry point where we might get a nsLoginInfo object
202 // that wasn't created by us (and so might not be a JS
203 // implementation being wrapped)
204 if (!login.wrappedJSObject) {
205 var clone = Cc["@mozilla.org/login-manager/loginInfo;1"].
206 createInstance(Ci.nsILoginInfo);
207 clone.init(login.hostname, login.formSubmitURL, login.httpRealm,
208 login.username, login.password,
209 login.usernameField, login.passwordField);
210 login = clone;
211 }
212
213 var key = login.hostname;
214
215 // If first entry for key, create an Array to hold it's logins.
216 var rollback;
217 if (!this._logins[key]) {
218 this._logins[key] = [];
219 rollback = null;
220 } else {
221 rollback = this._logins[key].concat(); // clone array
222 }
223
224 this._logins[key].push(login);
225
226 var ok = this._writeFile();
227
228 // If we failed, don't keep the added login in memory.
229 if (!ok) {
230 if (rollback)
231 this._logins[key] = rollback;
232 else
233 delete this._logins[key];
234
235 throw "Couldn't write to file, login not added.";
236 }
237 },
238
239
240 /*
241 * removeLogin
242 *
243 */
removeLogin
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
244 removeLogin : function (login) {
245 var key = login.hostname;
246 var logins = this._logins[key];
247
248 if (!logins)
249 throw "No logins found for hostname (" + key + ")";
250
251 var rollback = this._logins[key].concat(); // clone array
252
253 // The specified login isn't encrypted, so we need to ensure
254 // the logins we're comparing with are decrypted. We decrypt one entry
255 // at a time, lest _decryptLogins return fewer entries and screw up
256 // indices between the two.
257 for (var i = 0; i < logins.length; i++) {
258
259 var [[decryptedLogin], userCanceled] =
260 this._decryptLogins([logins[i]]);
261
262 if (userCanceled)
263 throw "User canceled master password entry, login not removed.";
264
265 if (!decryptedLogin)
266 continue;
267
268 if (decryptedLogin.equals(login)) {
269 logins.splice(i, 1); // delete that login from array.
270 break;
271 // Note that if there are duplicate entries, they'll
272 // have to be deleted one-by-one.
273 }
274 }
275
276 // Did we delete the last login for this host?
277 if (logins.length == 0)
278 delete this._logins[key];
279
280 var ok = this._writeFile();
281
282 // If we failed, don't actually remove the login.
283 if (!ok) {
284 this._logins[key] = rollback;
285 throw "Couldn't write to file, login not removed.";
286 }
287 },
288
289
290 /*
291 * modifyLogin
292 *
293 */
modifyLogin
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
294 modifyLogin : function (oldLogin, newLogin) {
295 // Throws if there are bogus values.
296 this._checkLoginValues(newLogin);
297
298 this.removeLogin(oldLogin);
299 this.addLogin(newLogin);
300 },
301
302
303 /*
304 * getAllLogins
305 *
306 * Returns an array of nsAccountInfo.
307 */
getAllLogins
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
308 getAllLogins : function (count) {
309 var result = [], userCanceled;
310
311 // Each entry is an array -- append the array entries to |result|.
312 for each (var hostLogins in this._logins) {
313 result = result.concat(hostLogins);
314 }
315
316 // decrypt entries for caller.
317 [result, userCanceled] = this._decryptLogins(result);
318
319 count.value = result.length; // needed for XPCOM
320 return result;
321 },
322
323
324 /*
325 * removeAllLogins
326 *
327 * Removes all logins from storage.
328 */
removeAllLogins
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
329 removeAllLogins : function () {
330 // Delete any old, unused files.
331 this._removeOldSignonsFiles();
332
333 // Disabled hosts kept, as one presumably doesn't want to erase those.
334 this._logins = {};
335 this._writeFile();
336 },
337
338
339 /*
340 * getAllDisabledHosts
341 *
342 */
getAllDisabledHosts
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
343 getAllDisabledHosts : function (count) {
344 var result = [];
345
346 for (var hostname in this._disabledHosts) {
347 result.push(hostname);
348 }
349
350 count.value = result.length; // needed for XPCOM
351 return result;
352 },
353
354
355 /*
356 * getLoginSavingEnabled
357 *
358 */
getLoginSavingEnabled
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
359 getLoginSavingEnabled : function (hostname) {
360 return !this._disabledHosts[hostname];
361 },
362
363
364 /*
365 * setLoginSavingEnabled
366 *
367 */
setLoginSavingEnabled
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
368 setLoginSavingEnabled : function (hostname, enabled) {
369 // File format prohibits certain values. Also, nulls
370 // won't round-trip with getAllDisabledHosts().
371 if (hostname == "." ||
372 hostname.indexOf("\r") != -1 ||
373 hostname.indexOf("\n") != -1 ||
374 hostname.indexOf("\0") != -1)
375 throw "Invalid hostname";
376
377 if (enabled)
378 delete this._disabledHosts[hostname];
379 else
380 this._disabledHosts[hostname] = true;
381
382 this._writeFile();
383 },
384
385
386 /*
387 * findLogins
388 *
389 */
findLogins
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
390 findLogins : function (count, hostname, formSubmitURL, httpRealm) {
391 var userCanceled;
392
393 var logins = this._searchLogins(hostname, formSubmitURL, httpRealm);
394
395 // Decrypt entries found for the caller.
396 [logins, userCanceled] = this._decryptLogins(logins);
397
398 // We want to throw in this case, so that the Login Manager
399 // knows to stop processing forms on the page so the user isn't
400 // prompted multiple times.
401 if (userCanceled)
402 throw "User canceled Master Password entry";
403
404 count.value = logins.length; // needed for XPCOM
405 return logins;
406 },
407
408
409 /*
410 * countLogins
411 *
412 */
countLogins
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
413 countLogins : function (aHostname, aFormSubmitURL, aHttpRealm) {
414 var logins;
415
416 // Normal case: return direct results for the specified host.
417 if (aHostname) {
418 logins = this._searchLogins(aHostname, aFormSubmitURL, aHttpRealm);
419 return logins.length
420 }
421
422 // For consistency with how aFormSubmitURL and aHttpRealm work
423 if (aHostname == null)
424 return 0;
425
426 // aHostname == "", so loop through each known host to match with each.
427 var count = 0;
428 for (var hostname in this._logins) {
429 logins = this._searchLogins(hostname, aFormSubmitURL, aHttpRealm);
430 count += logins.length;
431 }
432
433 return count;
434 },
435
436
437
438
439 /* ==================== Internal Methods ==================== */
440
441
442
443
444 /*
445 * _searchLogins
446 *
447 */
_searchLogins
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
448 _searchLogins : function (hostname, formSubmitURL, httpRealm) {
449 var hostLogins = this._logins[hostname];
450 if (hostLogins == null)
451 return [];
452
453 var result = [], userCanceled;
454
455 for each (var login in hostLogins) {
456
457 // If search arg is null, skip login unless it doesn't specify a
458 // httpRealm (ie, it's also null). If the search arg is an empty
459 // string, always match.
460 if (httpRealm == null) {
461 if (login.httpRealm != null)
462 continue;
463 } else if (httpRealm != "") {
464 // Make sure the realms match. If search arg is null,
465 // only match if login doesn't specify a realm (is null)
466 if (httpRealm != login.httpRealm)
467 continue;
468 }
469
470 // If search arg is null, skip login unless it doesn't specify a
471 // action URL (ie, it's also null). If the search arg is an empty
472 // string, always match.
473 if (formSubmitURL == null) {
474 if (login.formSubmitURL != null)
475 continue;
476 } else if (formSubmitURL != "") {
477 // If the stored login is blank (not null), that means the
478 // login was stored before we started keeping the action
479 // URL, so always match. Unless the search g
480 if (login.formSubmitURL != "" &&
481 formSubmitURL != login.formSubmitURL)
482 continue;
483 }
484
485 result.push(login);
486 }
487
488 return result;
489 },
490
491
492 /*
493 * _checkLoginValues
494 *
495 * Due to the way the signons2.txt file is formatted, we need to make
496 * sure certain field values or characters do not cause the file to
497 * be parse incorrectly. Reject logins that we can't store correctly.
498 */
_checkLoginValues
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
499 _checkLoginValues : function (aLogin) {
badCharacterPresent
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
500 function badCharacterPresent(l, c) {
501 return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
502 (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
503 l.hostname.indexOf(c) != -1 ||
504 l.usernameField.indexOf(c) != -1 ||
505 l.passwordField.indexOf(c) != -1);
506 }
507
508 // Nulls are invalid, as they don't round-trip well.
509 // Mostly not a formatting problem, although ".\0" can be quirky.
510 if (badCharacterPresent(aLogin, "\0"))
511 throw "login values can't contain nulls";
512
513 // Newlines are invalid for any field stored as plaintext.
514 if (badCharacterPresent(aLogin, "\r") ||
515 badCharacterPresent(aLogin, "\n"))
516 throw "login values can't contain newlines";
517
518 // A line with just a "." can have special meaning.
519 if (aLogin.usernameField == "." ||
520 aLogin.formSubmitURL == ".")
521 throw "login values can't be periods";
522
523 // A hostname with "\ \(" won't roundtrip.
524 // eg host="foo (", realm="bar" --> "foo ( (bar)"
525 // vs host="foo", realm=" (bar" --> "foo ( (bar)"
526 if (aLogin.hostname.indexOf(" (") != -1)
527 throw "bad parens in hostname";
528 },
529
530
531 /*
532 * _getSignonsFile
533 *
534 * Determines what file to use based on prefs. Returns it as a
535 * nsILocalFile, along with a file to import from first (if needed)
536 *
537 */
_getSignonsFile
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
538 _getSignonsFile : function() {
539 var destFile = null, importFile = null;
540
541 // We've used a number of prefs over time due to compatibility issues.
542 // Use the filename specified in the newest pref, but import from
543 // older files if needed.
544 for (var i = 0; i < this._filenamePrefs.length; i++) {
545 var prefname = this._filenamePrefs[i];
546 var filename = this._prefBranch.getCharPref(prefname);
547 var file = this._profileDir.clone();
548 file.append(filename);
549
550 this.log("Checking file " + filename + " (" + prefname + ")");
551
552 // First loop through, save the preferred filename.
553 if (!destFile)
554 destFile = file;
555 else
556 importFile = file;
557
558 if (file.exists())
559 return [destFile, importFile];
560 }
561
562 // If we can't find any existing file, use the preferred file.
563 return [destFile, null];
564 },
565
566
567 /*
568 * _removeOldSignonsFiles
569 *
570 * Deletes any storage files that we're not using any more.
571 */
_removeOldSignonsFiles
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
572 _removeOldSignonsFiles : function() {
573 // We've used a number of prefs over time due to compatibility issues.
574 // Skip the first entry (the newest) and delete the others.
575 for (var i = 1; i < this._filenamePrefs.length; i++) {
576 var prefname = this._filenamePrefs[i];
577 var filename = this._prefBranch.getCharPref(prefname);
578 var file = this._profileDir.clone();
579 file.append(filename);
580
581 if (file.exists()) {
582 this.log("Deleting old " + filename + " (" + prefname + ")");
583 try {
584 file.remove(false);
585 } catch (e) {
586 this.log("NOTICE: Couldn't delete " + filename + ": " + e);
587 }
588 }
589 }
590 },
591
592
593 /*
594 * _upgrade_entry_to_2E
595 *
596 * Updates the format of an entry from 2D to 2E. Returns an array of
597 * logins (1 or 2), as sometimes updating an entry requires creating an
598 * extra login.
599 */
_upgrade_entry_to_2E
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
600 _upgrade_entry_to_2E : function (aLogin) {
601 var upgradedLogins = [aLogin];
602
603 /*
604 * For logins stored from HTTP channels
605 * - scheme needs to be derived and prepended
606 * - blank or missing realm becomes same as hostname.
607 *
608 * "site.com:80" --> "http://site.com"
609 * "site.com:443" --> "https://site.com"
610 * "site.com:123" --> Who knows! (So add both)
611 *
612 * Note: For HTTP logins, the hostname never contained a username
613 * or password. EG "user@site.com:80" shouldn't ever happen.
614 *
615 * Note: Proxy logins are also stored in this format.
616 */
617 if (aLogin.hostname.indexOf("://") == -1) {
618 var oldHost = aLogin.hostname;
619
620 // Check for a trailing port number, EG "site.com:80". If there's
621 // no port, it wasn't saved by the browser and is probably some
622 // arbitrary string picked by an extension.
623 if (!/:\d+$/.test(aLogin.hostname)) {
624 this.log("2E upgrade: no port, skipping " + aLogin.hostname);
625 return upgradedLogins;
626 }
627
628 // Parse out "host:port".
629 try {
630 // Small hack: Need a scheme for nsIURI, so just prepend http.
631 // We'll check for a port == -1 in case nsIURI ever starts
632 // noticing that "http://foo:80" is using the default port.
633 var uri = this._ioService.newURI("http://" + aLogin.hostname,
634 null, null);
635 var host = uri.host;
636 var port = uri.port;
637 } catch (e) {
638 this.log("2E upgrade: Can't parse hostname " + aLogin.hostname);
639 return upgradedLogins;
640 }
641
642 if (port == 80 || port == -1)
643 aLogin.hostname = "http://" + host;
644 else if (port == 443)
645 aLogin.hostname = "https://" + host;
646 else {
647 // Not a standard port! Could be either http or https!
648 // (Or maybe it's a proxy login!) To try and avoid
649 // breaking logins, we'll add *both* http and https
650 // versions.
651 this.log("2E upgrade: Cloning login for " + aLogin.hostname);
652
653 aLogin.hostname = "http://" + host + ":" + port;
654
655 var extraLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
656 createInstance(Ci.nsILoginInfo);
657 extraLogin.init("https://" + host + ":" + port,
658 null, aLogin.httpRealm,
659 aLogin.username, aLogin.password, "", "");
660 // We don't have decrypted values, unless we're importing from IE,
661 // so clone the encrypted bits into the new entry.
662 extraLogin.wrappedJSObject.encryptedPassword =
663 aLogin.wrappedJSObject.encryptedPassword;
664 extraLogin.wrappedJSObject.encryptedUsername =
665 aLogin.wrappedJSObject.encryptedUsername;
666
667 if (extraLogin.httpRealm == "")
668 extraLogin.httpRealm = extraLogin.hostname;
669
670 upgradedLogins.push(extraLogin);
671 }
672
673 // If the server didn't send a realm (or it was blank), we
674 // previously didn't store anything.
675 if (aLogin.httpRealm == "")
676 aLogin.httpRealm = aLogin.hostname;
677
678 this.log("2E upgrade: " + oldHost + " ---> " + aLogin.hostname);
679
680 return upgradedLogins;
681 }
682
683
684 /*
685 * For form logins and non-HTTP channel logins (both were stored in
686 * the same format):
687 *
688 * Standardize URLs (.hostname and .actionURL)
689 * - remove default port numbers, if specified
690 * "http://site.com:80" --> "http://site.com"
691 * - remove usernames from URL (may move into aLogin.username)
692 * "ftp://user@site.com" --> "ftp://site.com"
693 *
694 * Note: Passwords in the URL ("foo://user:pass@site.com") were not
695 * stored in FF2, so no need to try to move the value into
696 * aLogin.password.
697 */
698
699 // closures in cleanupURL
700 var ioService = this._ioService;
701 var log = this.log;
702
cleanupURL
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
703 function cleanupURL(aURL, allowJS) {
704 var newURL, username = null, pathname = "";
705
706 try {
707 var uri = ioService.newURI(aURL, null, null);
708 var scheme = uri.scheme;
709
710 if (allowJS && scheme == "javascript")
711 return ["javascript:", null, ""];
712
713 newURL = scheme + "://" + uri.host;
714
715 // If the URL explicitly specified a port, only include it when
716 // it's not the default. (We never want "http://foo.com:80")
717 port = uri.port;
718 if (port != -1) {
719 var handler = ioService.getProtocolHandler(scheme);
720 if (port != handler.defaultPort)
721 newURL += ":" + port;
722 }
723
724 // Could be a channel login with a username.
725 if (scheme != "http" && scheme != "https" && uri.username)
726 username = uri.username;
727
728 if (uri.path != "/")
729 pathname = uri.path;
730
731 } catch (e) {
732 log("Can't cleanup URL: " + aURL + " e: " + e);
733 newURL = aURL;
734 }
735
736 if (newURL != aURL)
737 log("2E upgrade: " + aURL + " ---> " + newURL);
738
739 return [newURL, username, pathname];
740 }
741
742 const isMailNews = /^(ldaps?|smtp|imap|news|mailbox):\/\//;
743
744 // Old mailnews logins were protocol logins with a username/password
745 // field name set.
746 var isFormLogin = (aLogin.formSubmitURL ||
747 aLogin.usernameField ||
748 aLogin.passwordField) &&
749 !isMailNews.test(aLogin.hostname);
750
751 var [hostname, username, pathname] = cleanupURL(aLogin.hostname);
752 aLogin.hostname = hostname;
753
754 // If a non-HTTP URL contained a username, it wasn't stored in the
755 // encrypted username field (which contains an encrypted empty value)
756 // (Don't do this if it's a form login, though.)
757 if (username && !isFormLogin) {
758 var [encUsername, userCanceled] = this._encrypt(username);
759 if (!userCanceled)
760 aLogin.wrappedJSObject.encryptedUsername = encUsername;
761 }
762
763
764 if (aLogin.formSubmitURL) {
765 [hostname, username, pathname] = cleanupURL(aLogin.formSubmitURL,
766 true);
767 aLogin.formSubmitURL = hostname;
768 // username, if any, ignored.
769 }
770
771
772 /*
773 * For logins stored from non-HTTP channels
774 * - Set httpRealm so they don't look like form logins
775 * "ftp://site.com" --> "ftp://site.com (ftp://site.com)"
776 *
777 * Tricky: Form logins and non-HTTP channel logins are stored in the
778 * same format, and we don't want to add a realm to a form login.
779 * Form logins have field names, so only update the realm if there are
780 * no field names set. [Any login with a http[s]:// hostname is always
781 * a form login, so explicitly ignore those just to be safe.]
782 */
783 const isHTTP = /^https?:\/\//;
784 const isLDAP = /^ldaps?:\/\//;
785 if (!isHTTP.test(aLogin.hostname) && !isFormLogin) {
786 // LDAP logins need to keep the path.
787 if (isLDAP.test(aLogin.hostname))
788 aLogin.httpRealm = aLogin.hostname + pathname;
789 else
790 aLogin.httpRealm = aLogin.hostname;
791
792 aLogin.formSubmitURL = null;
793
794 // Null out the form items because mailnews will no longer treat
795 // or expect these as form logins
796 if (isMailNews.test(aLogin.hostname)) {
797 aLogin.usernameField = "";
798 aLogin.passwordField = "";
799 }
800
801 this.log("2E upgrade: set empty realm to " + aLogin.httpRealm);
802 }
803
804 return upgradedLogins;
805 },
806
807
808 /*
809 * _readFile
810 *
811 */
_readFile
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
812 _readFile : function () {
813 var formatVersion;
814
815 this.log("Reading passwords from " + this._signonsFile.path);
816
817 // If it doesn't exist, just create an empty file and bail out.
818 if (!this._signonsFile.exists()) {
819 this.log("Creating new signons file...");
820 this._writeFile();
821 return;
822 }
823
824 var inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
825 createInstance(Ci.nsIFileInputStream);
826 // init the stream as RD_ONLY, -1 == default permissions.
827 inputStream.init(this._signonsFile, 0x01, -1, null);
828 var lineStream = inputStream.QueryInterface(Ci.nsILineInputStream);
829 var line = { value: "" };
830
831 const STATE = { HEADER : 0, REJECT : 1, REALM : 2,
832 USERFIELD : 3, USERVALUE : 4,
833 PASSFIELD : 5, PASSVALUE : 6, ACTIONURL : 7,
834 FILLER : 8 };
835 var parseState = STATE.HEADER;
836
837 var nsLoginInfo = new Components.Constructor(
838 "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
839 var processEntry = false;
840
841 do {
842 var hasMore = lineStream.readLine(line);
843
844 switch (parseState) {
845 // Check file header
846 case STATE.HEADER:
847 if (line.value == "#2c") {
848 formatVersion = 0x2c;
849 } else if (line.value == "#2d") {
850 formatVersion = 0x2d;
851 } else if (line.value == "#2e") {
852 formatVersion = 0x2e;
853 } else {
854 this.log("invalid file header (" + line.value + ")");
855 throw "invalid file header in signons file";
856 // We could disable later writing to file, so we
857 // don't clobber whatever it is. ...however, that
858 // would mean corrupt files are not self-healing.
859 return;
860 }
861 parseState++;
862 break;
863
864 // Line is a hostname for which passwords should never be saved.
865 case STATE.REJECT:
866 if (line.value == ".") {
867 parseState++;
868 break;
869 }
870
871 this._disabledHosts[line.value] = true;
872
873 break;
874
875 // Line is a hostname, saved login(s) will follow
876 case STATE.REALM:
877 var hostrealm = line.value;
878
879 // Format is "http://site.com", with "(some realm)"
880 // appended if it's a HTTP-Auth login.
881 const realmFormat = /^(.+?)( \(.*\))?$/;
882 var matches = realmFormat.exec(hostrealm);
883 var hostname, httpRealm;
884 if (matches && matches.length == 3) {
885 hostname = matches[1];
886 httpRealm = matches[2] ?
887 matches[2].slice(2, -1) : null;
888 } else {
889 if (hostrealm != "") {
890 // Uhoh. This shouldn't happen, but try to deal.
891 this.log("Error parsing host/realm: " + hostrealm);
892 }
893 hostname = hostrealm;
894 httpRealm = null;
895 }
896
897 parseState++;
898 break;
899
900 // Line is the HTML 'name' attribute for the username field
901 // (or "." to indicate end of hostrealm)
902 case STATE.USERFIELD:
903 if (line.value == ".") {
904 parseState = STATE.REALM;
905 break;
906 }
907
908 var entry = new nsLoginInfo();
909 entry.hostname = hostname;
910 entry.httpRealm = httpRealm;
911
912 entry.usernameField = line.value;
913 parseState++;
914 break;
915
916 // Line is a username
917 case STATE.USERVALUE:
918 entry.wrappedJSObject.encryptedUsername = line.value;
919 parseState++;
920 break;
921
922 // Line is the HTML 'name' attribute for the password field,
923 // with a leading '*' character
924 case STATE.PASSFIELD:
925 entry.passwordField = line.value.substr(1);
926 parseState++;
927 break;
928
929 // Line is a password
930 case STATE.PASSVALUE:
931 entry.wrappedJSObject.encryptedPassword = line.value;
932
933 // Version 2C doesn't have an ACTIONURL line, so
934 // process entry now.
935 if (formatVersion < 0x2d)
936 processEntry = true;
937
938 parseState++;
939 break;
940
941 // Line is the action URL
942 case STATE.ACTIONURL:
943 var formSubmitURL = line.value;
944 if (!formSubmitURL && entry.httpRealm != null)
945 entry.formSubmitURL = null;
946 else
947 entry.formSubmitURL = formSubmitURL;
948
949 // Version 2D doesn't have a FILLER line, so
950 // process entry now.
951 if (formatVersion < 0x2e)
952 processEntry = true;
953
954 parseState++;
955 break;
956
957 // Line is unused filler for future use
958 case STATE.FILLER:
959 // Save the line's value (so we can dump it back out when
960 // we save the file next time) for forwards compatability.
961 entry.wrappedJSObject.filler = line.value;
962 processEntry = true;
963
964 parseState++;
965 break;
966 }
967
968 // If we've read all the lines for the current entry,
969 // process it and reset the parse state for the next entry.
970 if (processEntry) {
971 if (formatVersion < 0x2d) {
972 // A blank, non-null value is handled as a wildcard.
973 if (entry.httpRealm != null)
974 entry.formSubmitURL = null;
975 else
976 entry.formSubmitURL = "";
977 }
978
979 // Upgrading an entry to 2E can sometimes result in the need
980 // to create an extra login.
981 var entries = [entry];
982 if (formatVersion < 0x2e)
983 entries = this._upgrade_entry_to_2E(entry);
984
985
986 for each (var e in entries) {
987 if (!this._logins[e.hostname])
988 this._logins[e.hostname] = [];
989 this._logins[e.hostname].push(e);
990 }
991
992 entry = null;
993 processEntry = false;
994 parseState = STATE.USERFIELD;
995 }
996 } while (hasMore);
997
998 lineStream.close();
999
1000 return;
1001 },
1002
1003
1004 /*
1005 * _writeFile
1006 *
1007 * Returns true if the operation was successfully completed, or false
1008 * if there was an error (probably the user refusing to enter a
1009 * master password if prompted).
1010 */
_writeFile
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1011 _writeFile : function () {
writeLine
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1012 function writeLine(data) {
1013 data += "\r\n";
1014 outputStream.write(data, data.length);
1015 }
1016
1017 this.log("Writing passwords to " + this._signonsFile.path);
1018
1019 var safeStream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
1020 createInstance(Ci.nsIFileOutputStream);
1021 // WR_ONLY|CREAT|TRUNC
1022 safeStream.init(this._signonsFile, 0x02 | 0x08 | 0x20, 0600, null);
1023
1024 var outputStream = Cc["@mozilla.org/network/buffered-output-stream;1"].
1025 createInstance(Ci.nsIBufferedOutputStream);
1026 outputStream.init(safeStream, 8192);
1027 outputStream.QueryInterface(Ci.nsISafeOutputStream); // for .finish()
1028
1029
1030 // write file version header
1031 writeLine("#2e");
1032
1033 // write disabled logins list
1034 for (var hostname in this._disabledHosts) {
1035 writeLine(hostname);
1036 }
1037
1038 // write end-of-reject-list marker
1039 writeLine(".");
1040
1041 for (var hostname in this._logins) {
sortByRealm
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1042 function sortByRealm(a,b) {
1043 a = a.httpRealm;
1044 b = b.httpRealm;
1045
1046 if (!a && !b)
1047 return 0;
1048
1049 if (!a || a < b)
1050 return -1;
1051
1052 if (!b || b > a)
1053 return 1;
1054
1055 return 0; // a==b, neither is null
1056 }
1057
1058 // Sort logins by httpRealm. This allows us to group multiple
1059 // logins for the same realm together.
1060 this._logins[hostname].sort(sortByRealm);
1061
1062
1063 // write each login known for the host
1064 var lastRealm = null;
1065 var firstEntry = true;
1066 var userCanceled = false;
1067 for each (var login in this._logins[hostname]) {
1068
1069 // If this login is for a new realm, start a new entry.
1070 if (login.httpRealm != lastRealm || firstEntry) {
1071 // end previous entry, if needed.
1072 if (!firstEntry)
1073 writeLine(".");
1074
1075 var hostrealm = login.hostname;
1076 if (login.httpRealm)
1077 hostrealm += " (" + login.httpRealm + ")";
1078
1079 writeLine(hostrealm);
1080 }
1081
1082 firstEntry = false;
1083
1084 // Get the encrypted value of the username. Newly added
1085 // logins will need the plaintext value encrypted.
1086 var encUsername = login.wrappedJSObject.encryptedUsername;
1087 if (!encUsername) {
1088 [encUsername, userCanceled] = this._encrypt(login.username);
1089 login.wrappedJSObject.encryptedUsername = encUsername;
1090 }
1091
1092 if (userCanceled)
1093 break;
1094
1095 // Get the encrypted value of the password. Newly added
1096 // logins will need the plaintext value encrypted.
1097 var encPassword = login.wrappedJSObject.encryptedPassword;
1098 if (!encPassword) {
1099 [encPassword, userCanceled] = this._encrypt(login.password);
1100 login.wrappedJSObject.encryptedPassword = encPassword;
1101 }
1102
1103 if (userCanceled)
1104 break;
1105
1106
1107 writeLine((login.usernameField ? login.usernameField : ""));
1108 writeLine(encUsername);
1109 writeLine("*" +
1110 (login.passwordField ? login.passwordField : ""));
1111 writeLine(encPassword);
1112 writeLine((login.formSubmitURL ? login.formSubmitURL : ""));
1113 if (login.wrappedJSObject.filler)
1114 writeLine(login.wrappedJSObject.filler);
1115 else
1116 writeLine("---");
1117
1118 lastRealm = login.httpRealm;
1119 }
1120
1121 if (userCanceled) {
1122 this.log("User canceled Master Password, aborting write.");
1123 // .close will cause an abort w/o modifying original file
1124 outputStream.close();
1125 return false;
1126 }
1127
1128 // write end-of-host marker
1129 writeLine(".");
1130 }
1131
1132 // [if there were no hosts, no end-of-host marker (".") needed]
1133
1134 outputStream.finish();
1135 return true;
1136 },
1137
1138
1139 /*
1140 * _decryptLogins
1141 *
1142 * Decrypts username and password fields in the provided array of
1143 * logins. This is deferred from the _readFile() code, so that
1144 * the user is not prompted for a master password (if set) until
1145 * the entries are actually used.
1146 *
1147 * The entries specified by the array will be decrypted, if possible.
1148 * An array of successfully decrypted logins will be returned. The return
1149 * value should be given to external callers (since still-encrypted
1150 * entries are useless), whereas internal callers generally don't want
1151 * to lose unencrypted entries (eg, because the user clicked Cancel
1152 * instead of entering their master password)
1153 */
_decryptLogins
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1154 _decryptLogins : function (logins) {
1155 var result = [], userCanceled = false;
1156
1157 for each (var login in logins) {
1158 var username, password;
1159
1160 [username, userCanceled] =
1161 this._decrypt(login.wrappedJSObject.encryptedUsername);
1162
1163 if (userCanceled)
1164 break;
1165
1166 [password, userCanceled] =
1167 this._decrypt(login.wrappedJSObject.encryptedPassword);
1168
1169 // Probably can't hit this case, but for completeness...
1170 if (userCanceled)
1171 break;
1172
1173 // If decryption failed (corrupt entry?) skip it.
1174 // Note that we allow password-only logins, so username con be "".
1175 if (username == null || !password)
1176 continue;
1177
1178 // We could set the decrypted values on a copy of the object, to
1179 // try to prevent the decrypted values from sitting around in
1180 // memory if they're not needed. But thanks to GC that's happening
1181 // anyway, so meh.
1182 login.username = username;
1183 login.password = password;
1184
1185 // Old mime64-obscured entries need to be reencrypted in the new
1186 // format.
1187 if (login.wrappedJSObject.encryptedUsername &&
1188 login.wrappedJSObject.encryptedUsername.charAt(0) == '~') {
1189 [username, userCanceled] = this._encrypt(login.username);
1190
1191 if (userCanceled)
1192 break;
1193
1194 login.wrappedJSObject.encryptedUsername = username;
1195 }
1196
1197 if (login.wrappedJSObject.encryptedPassword &&
1198 login.wrappedJSObject.encryptedPassword.charAt(0) == '~') {
1199
1200 [password, userCanceled] = this._encrypt(login.password);
1201
1202 if (userCanceled)
1203 break;
1204
1205 login.wrappedJSObject.encryptedPassword = password;
1206 }
1207
1208 result.push(login);
1209 }
1210
1211 return [result, userCanceled];
1212 },
1213
1214
1215 /*
1216 * _encrypt
1217 *
1218 * Encrypts the specified string, using the SecretDecoderRing.
1219 *
1220 * Returns [cipherText, userCanceled] where:
1221 * cipherText -- the encrypted string, or null if it failed.
1222 * userCanceled -- if the encryption failed, this is true if the
1223 * user selected Cancel when prompted to enter their
1224 * Master Password. The caller should bail out, and not
1225 * not request that more things be encrypted (which
1226 * results in prompting the user for a Master Password
1227 * over and over.)
1228 */
_encrypt
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1229 _encrypt : function (plainText) {
1230 var cipherText = null, userCanceled = false;
1231
1232 try {
1233 var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
1234 createInstance(Ci.nsIScriptableUnicodeConverter);
1235 converter.charset = "UTF-8";
1236 var plainOctet = converter.ConvertFromUnicode(plainText);
1237 plainOctet += converter.Finish();
1238 cipherText = this._decoderRing.encryptString(plainOctet);
1239 } catch (e) {
1240 this.log("Failed to encrypt string. (" + e.name + ")");
1241 // If the user clicks Cancel, we get NS_ERROR_FAILURE.
1242 // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
1243 if (e.result == Components.results.NS_ERROR_FAILURE)
1244 userCanceled = true;
1245 }
1246
1247 return [cipherText, userCanceled];
1248 },
1249
1250
1251 /*
1252 * _decrypt
1253 *
1254 * Decrypts the specified string, using the SecretDecoderRing.
1255 *
1256 * Returns [plainText, userCanceled] where:
1257 * plainText -- the decrypted string, or null if it failed.
1258 * userCanceled -- if the decryption failed, this is true if the
1259 * user selected Cancel when prompted to enter their
1260 * Master Password. The caller should bail out, and not
1261 * not request that more things be decrypted (which
1262 * results in prompting the user for a Master Password
1263 * over and over.)
1264 */
_decrypt
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1265 _decrypt : function (cipherText) {
1266 var plainText = null, userCanceled = false;
1267
1268 try {
1269 var plainOctet;
1270 if (cipherText.charAt(0) == '~') {
1271 // The older file format obscured entries by
1272 // base64-encoding them. These entries are signaled by a
1273 // leading '~' character.
1274 plainOctet = atob(cipherText.substring(1));
1275 } else {
1276 plainOctet = this._decoderRing.decryptString(cipherText);
1277 }
1278 var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
1279 createInstance(Ci.nsIScriptableUnicodeConverter);
1280 converter.charset = "UTF-8";
1281 plainText = converter.ConvertToUnicode(plainOctet);
1282 } catch (e) {
1283 this.log("Failed to decrypt string: " + cipherText +
1284 " (" + e.name + ")");
1285
1286 // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
1287 // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
1288 // Wrong passwords are handled by the decoderRing reprompting;
1289 // we get no notification.
1290 if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE)
1291 userCanceled = true;
1292 }
1293
1294 return [plainText, userCanceled];
1295 },
1296
1297
1298
1299
1300 /* ================== nsILoginManagerIEMigratorHelper ================== */
1301
1302
1303
1304
1305 _migrationLoginManager : null,
1306
1307 /*
1308 * migrateAndAddLogin
1309 *
1310 * Given a login with IE6-formatted fields, migrates it to the new format
1311 * and adds it to the login manager.
1312 *
1313 * Experimentally derived format of IE6 logins, see:
1314 * https://bugzilla.mozilla.org/attachment.cgi?id=319346
1315 *
1316 * HTTP AUTH:
1317 * - hostname is always "example.com:123"
1318 * * "example.com", "http://example.com", "http://example.com:80" all
1319 * end up as just "example.com:80"
1320 * * Entering "example.com:80" in the URL bar isn't recognized as a
1321 * valid URL by IE6.
1322 * * "https://example.com" is saved as "example.com:443"
1323 * * "https://example.com:666" is saved as "example.com:666". Thus, for
1324 * non-standard ports we don't know the right scheme, so create both.
1325 *
1326 * - an empty or missing "realm" in the WWW-Authenticate reply is stored
1327 * as just an empty string by IE6.
1328 *
1329 * - IE6 will store logins where one or both (!) of the username/password
1330 * is left blank. We don't support logins without a password, so these
1331 * logins won't be added [addLogin() will throw].
1332 *
1333 * - IE6 won't recognize a URL with and embedded username/password (eg
1334 * http://user@example.com, http://user:pass@example.com), so these
1335 * shouldn't be encountered.
1336 *
1337 * - Our migration code doesn't extract non-HTTP logins (eg, FTP). So
1338 * they shouldn't be encountered here. (Verified by saving FTP logins
1339 * in IE and then importing in Firefox.)
1340 *
1341 *
1342 * FORM LOGINS:
1343 * - hostname is "http://site.com" or "https://site.com".
1344 * * scheme always included
1345 * * default port not included
1346 * - port numbers, even for non-standard posts, are never present!
1347 * unfortunately, this means logins will only work on the default
1348 * port, because we don't know what the original was (or even that
1349 * it wasn't originally stored for the original port).
1350 * - Logins are stored without a field name by IE, but we look one up
1351 * in the migrator for the username. The password field name will
1352 * always be empty-string.
1353 */
migrateAndAddLogin
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1354 migrateAndAddLogin : function (aLogin) {
1355 // Initialize outself on the first call
1356 if (!this._migrationLoginManager) {
1357 // Connect to the correct preferences branch.
1358 this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
1359 getService(Ci.nsIPrefService);
1360 this._prefBranch = this._prefBranch.getBranch("signon.");
1361 this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
1362
1363 this._debug = this._prefBranch.getBoolPref("debug");
1364
1365 this._migrationLoginManager = Cc["@mozilla.org/login-manager;1"].
1366 getService(Ci.nsILoginManager);
1367 }
1368
1369 this.log("Migrating login for " + aLogin.hostname);
1370
1371 // The IE login is in the same format as the old password
1372 // manager entries, so just reuse that code.
1373 var logins = this._upgrade_entry_to_2E(aLogin);
1374
1375 // Add logins via the login manager (and not this.addLogin),
1376 // lest an alternative storage module be in use.
1377 for each (var login in logins)
1378 this._migrationLoginManager.addLogin(login);
1379 }
1380 }; // end of nsLoginManagerStorage_legacy implementation
1381
1382 var component = [LoginManagerStorage_legacy];
NSGetModule
(0 calls, 0 incl. v-uS, 0 excl. v-uS)
1383 function NSGetModule(compMgr, fileSpec) {
1384 return XPCOMUtils.generateModule(component);
1385 }