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