/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2007 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Justin Dolske (original author) * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ const Cc = Components.classes; const Ci = Components.interfaces; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); function LoginManagerStorage_legacy() { }; LoginManagerStorage_legacy.prototype = { classDescription : "LoginManagerStorage_legacy", contractID : "@mozilla.org/login-manager/storage/legacy;1", classID : Components.ID("{e09e4ca6-276b-4bb4-8b71-0635a3a2a007}"), QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage, Ci.nsILoginManagerIEMigrationHelper]), __logService : null, // Console logging service, used for debugging. get _logService() { if (!this.__logService) this.__logService = Cc["@mozilla.org/consoleservice;1"]. getService(Ci.nsIConsoleService); return this.__logService; }, __ioService: null, // IO service for string -> nsIURI conversion get _ioService() { if (!this.__ioService) this.__ioService = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); return this.__ioService; }, __decoderRing : null, // nsSecretDecoderRing service get _decoderRing() { if (!this.__decoderRing) this.__decoderRing = Cc["@mozilla.org/security/sdr;1"]. getService(Ci.nsISecretDecoderRing); return this.__decoderRing; }, __utfConverter : null, // UCS2 <--> UTF8 string conversion get _utfConverter() { if (!this.__utfConverter) { this.__utfConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. createInstance(Ci.nsIScriptableUnicodeConverter); this.__utfConverter.charset = "UTF-8"; } return this.__utfConverter; }, _utfConverterReset : function() { this.__utfConverter = null; }, __profileDir: null, // nsIFile for the user's profile dir get _profileDir() { if (!this.__profileDir) { var dirService = Cc["@mozilla.org/file/directory_service;1"]. getService(Ci.nsIProperties); this.__profileDir = dirService.get("ProfD", Ci.nsIFile); } return this.__profileDir; }, __nsLoginInfo: null, // Constructor for nsILoginInfo implementation get _nsLoginInfo() { if (!this.__nsLoginInfo) this.__nsLoginInfo = new Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); return this.__nsLoginInfo; }, _prefBranch : null, // Preferences service _signonsFile : null, // nsIFile for "signons3.txt" (or whatever pref is) _debug : false, // mirrors signon.debug /* * A list of prefs that have been used to specify the filename for storing * logins. (We've used a number over time due to compatibility issues.) * This list is also used by _removeOldSignonsFile() to clean up old files. */ _filenamePrefs : ["SignonFileName3", "SignonFileName2", "SignonFileName"], /* * Core datastructures * * EG: _logins["http://site.com"][0].password * EG: _disabledHosts["never.site.com"] */ _logins : null, _disabledHosts : null, /* * log * * Internal function for logging debug messages to the Error Console. */ log : function (message) { if (!this._debug) return; dump("PwMgr Storage: " + message + "\n"); this._logService.logStringMessage("PwMgr Storage: " + message); }, /* ==================== Public Methods ==================== */ initWithFile : function(aInputFile, aOutputFile) { this._signonsFile = aInputFile; this.init(); if (aOutputFile) { this._signonsFile = aOutputFile; this._writeFile(); } }, /* * init * * Initialize this storage component and load stored passwords from disk. */ init : function () { this._logins = {}; this._disabledHosts = {}; // Connect to the correct preferences branch. this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); this._prefBranch = this._prefBranch.getBranch("signon."); this._prefBranch.QueryInterface(Ci.nsIPrefBranch2); this._debug = this._prefBranch.getBoolPref("debug"); // Check to see if the internal PKCS#11 token has been initialized. // If not, set a blank password. var tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"]. getService(Ci.nsIPK11TokenDB); var token = tokenDB.getInternalKeyToken(); if (token.needsUserInit) { this.log("Initializing key3.db with default blank password."); token.initPassword(""); } var importFile = null; // If initWithFile is calling us, _signonsFile is already set. if (!this._signonsFile) [this._signonsFile, importFile] = this._getSignonsFile(); // If we have an import file, do a switcharoo before reading it. if (importFile) { this.log("Importing " + importFile.path); var tmp = this._signonsFile; this._signonsFile = importFile; } // Read in the stored login data. this._readFile(); // If we were importing, write back to the normal file. if (importFile) { this._signonsFile = tmp; this._writeFile(); } }, /* * addLogin * */ addLogin : function (login) { // Throws if there are bogus values. this._checkLoginValues(login); // Clone the input. This ensures changes made by the caller to the // login (after calling addLogin) do no change the login we have. // Also, we rely on using login.wrappedJSObject, but can't rely on the // thing provided by the caller to support that. var clone = new this._nsLoginInfo(); clone.init(login.hostname, login.formSubmitURL, login.httpRealm, login.username, login.password, login.usernameField, login.passwordField); login = clone; var key = login.hostname; // If first entry for key, create an Array to hold its logins. var rollback; if (!this._logins[key]) { this._logins[key] = []; rollback = null; } else { rollback = this._logins[key].concat(); // clone array } this._logins[key].push(login); var ok = this._writeFile(); // If we failed, don't keep the added login in memory. if (!ok) { if (rollback) this._logins[key] = rollback; else delete this._logins[key]; throw "Couldn't write to file, login not added."; } }, /* * removeLogin * */ removeLogin : function (login) { var key = login.hostname; var logins = this._logins[key]; if (!logins) throw "No logins found for hostname (" + key + ")"; var rollback = this._logins[key].concat(); // clone array // The specified login isn't encrypted, so we need to ensure // the logins we're comparing with are decrypted. We decrypt one entry // at a time, lest _decryptLogins return fewer entries and screw up // indices between the two. for (var i = 0; i < logins.length; i++) { var [[decryptedLogin], userCanceled] = this._decryptLogins([logins[i]]); if (userCanceled) throw "User canceled master password entry, login not removed."; if (!decryptedLogin) continue; if (decryptedLogin.equals(login)) { logins.splice(i, 1); // delete that login from array. break; // Note that if there are duplicate entries, they'll // have to be deleted one-by-one. } } // Did we delete the last login for this host? if (logins.length == 0) delete this._logins[key]; var ok = this._writeFile(); // If we failed, don't actually remove the login. if (!ok) { this._logins[key] = rollback; throw "Couldn't write to file, login not removed."; } }, /* * modifyLogin * */ modifyLogin : function (oldLogin, newLogin) { if (newLogin instanceof Ci.nsIPropertyBag) throw "legacy modifyLogin with propertybag not implemented."; newLogin.QueryInterface(Ci.nsILoginInfo); // Throws if there are bogus values. this._checkLoginValues(newLogin); this.removeLogin(oldLogin); this.addLogin(newLogin); }, /* * getAllLogins * * Returns an array of nsAccountInfo. */ getAllLogins : function (count) { var result = [], userCanceled; // Each entry is an array -- append the array entries to |result|. for each (var hostLogins in this._logins) { result = result.concat(hostLogins); } // decrypt entries for caller. [result, userCanceled] = this._decryptLogins(result); if (userCanceled) throw "User canceled Master Password entry"; if (count) count.value = result.length; // needed for XPCOM return result; }, /* * getAllEncryptedLogins * * Returns an array of nsAccountInfo, each in the encrypted state. */ getAllEncryptedLogins : function (count) { var result = []; // Each entry is an array -- append the array entries to |result|. for each (var hostLogins in this._logins) { // Return copies to the caller. Prevents callers from modifying // our internal storage for each (var login in hostLogins) { var clone = new this._nsLoginInfo(); clone.init(login.hostname, login.formSubmitURL, login.httpRealm, login.wrappedJSObject.encryptedUsername, login.wrappedJSObject.encryptedPassword, login.usernameField, login.passwordField); result.push(clone); } } if (count) count.value = result.length; // needed for XPCOM return result; }, /* * searchLogins * * Not implemented. This interface was added to perform arbitrary searches. * Since the legacy storage module is no longer used, there is no need to * implement it here. */ searchLogins : function (count, matchData) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, /* * removeAllLogins * * Removes all logins from storage. */ removeAllLogins : function () { // Delete any old, unused files. this._removeOldSignonsFiles(); // Disabled hosts kept, as one presumably doesn't want to erase those. this._logins = {}; this._writeFile(); }, /* * getAllDisabledHosts * */ getAllDisabledHosts : function (count) { var result = []; for (var hostname in this._disabledHosts) { result.push(hostname); } if (count) count.value = result.length; // needed for XPCOM return result; }, /* * getLoginSavingEnabled * */ getLoginSavingEnabled : function (hostname) { return !this._disabledHosts[hostname]; }, /* * setLoginSavingEnabled * */ setLoginSavingEnabled : function (hostname, enabled) { // File format prohibits certain values. Also, nulls // won't round-trip with getAllDisabledHosts(). if (hostname == "." || hostname.indexOf("\r") != -1 || hostname.indexOf("\n") != -1 || hostname.indexOf("\0") != -1) throw "Invalid hostname"; if (enabled) delete this._disabledHosts[hostname]; else this._disabledHosts[hostname] = true; this._writeFile(); }, /* * findLogins * */ findLogins : function (count, hostname, formSubmitURL, httpRealm) { var userCanceled; var logins = this._searchLogins(hostname, formSubmitURL, httpRealm); // Decrypt entries found for the caller. [logins, userCanceled] = this._decryptLogins(logins); // We want to throw in this case, so that the Login Manager // knows to stop processing forms on the page so the user isn't // prompted multiple times. if (userCanceled) throw "User canceled Master Password entry"; count.value = logins.length; // needed for XPCOM return logins; }, /* * countLogins * */ countLogins : function (aHostname, aFormSubmitURL, aHttpRealm) { var logins; // Normal case: return direct results for the specified host. if (aHostname) { logins = this._searchLogins(aHostname, aFormSubmitURL, aHttpRealm); return logins.length } // For consistency with how aFormSubmitURL and aHttpRealm work if (aHostname == null) return 0; // aHostname == "", so loop through each known host to match with each. var count = 0; for (var hostname in this._logins) { logins = this._searchLogins(hostname, aFormSubmitURL, aHttpRealm); count += logins.length; } return count; }, /* ==================== Internal Methods ==================== */ /* * _searchLogins * */ _searchLogins : function (hostname, formSubmitURL, httpRealm) { var hostLogins = this._logins[hostname]; if (hostLogins == null) return []; var result = [], userCanceled; for each (var login in hostLogins) { // If search arg is null, skip login unless it doesn't specify a // httpRealm (ie, it's also null). If the search arg is an empty // string, always match. if (httpRealm == null) { if (login.httpRealm != null) continue; } else if (httpRealm != "") { // Make sure the realms match. If search arg is null, // only match if login doesn't specify a realm (is null) if (httpRealm != login.httpRealm) continue; } // If search arg is null, skip login unless it doesn't specify a // action URL (ie, it's also null). If the search arg is an empty // string, always match. if (formSubmitURL == null) { if (login.formSubmitURL != null) continue; } else if (formSubmitURL != "") { // If the stored login is blank (not null), that means the // login was stored before we started keeping the action // URL, so always match. Unless the search g if (login.formSubmitURL != "" && formSubmitURL != login.formSubmitURL) continue; } result.push(login); } return result; }, /* * _checkLoginValues * * Due to the way the signons2.txt file is formatted, we need to make * sure certain field values or characters do not cause the file to * be parse incorrectly. Reject logins that we can't store correctly. */ _checkLoginValues : function (aLogin) { function badCharacterPresent(l, c) { return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) || (l.httpRealm && l.httpRealm.indexOf(c) != -1) || l.hostname.indexOf(c) != -1 || l.usernameField.indexOf(c) != -1 || l.passwordField.indexOf(c) != -1); } // Nulls are invalid, as they don't round-trip well. // Mostly not a formatting problem, although ".\0" can be quirky. if (badCharacterPresent(aLogin, "\0")) throw "login values can't contain nulls"; // In theory these nulls should just be rolled up into the encrypted // values, but nsISecretDecoderRing doesn't use nsStrings, so the // nulls cause truncation. Check for them here just to avoid // unexpected round-trip surprises. if (aLogin.username.indexOf("\0") != -1 || aLogin.password.indexOf("\0") != -1) throw "login values can't contain nulls"; // Newlines are invalid for any field stored as plaintext. if (badCharacterPresent(aLogin, "\r") || badCharacterPresent(aLogin, "\n")) throw "login values can't contain newlines"; // A line with just a "." can have special meaning. if (aLogin.usernameField == "." || aLogin.formSubmitURL == ".") throw "login values can't be periods"; // A hostname with "\ \(" won't roundtrip. // eg host="foo (", realm="bar" --> "foo ( (bar)" // vs host="foo", realm=" (bar" --> "foo ( (bar)" if (aLogin.hostname.indexOf(" (") != -1) throw "bad parens in hostname"; }, /* * _getSignonsFile * * Determines what file to use based on prefs. Returns it as a * nsILocalFile, along with a file to import from first (if needed) * */ _getSignonsFile : function() { var destFile = null, importFile = null; // We've used a number of prefs over time due to compatibility issues. // Use the filename specified in the newest pref, but import from // older files if needed. for (var i = 0; i < this._filenamePrefs.length; i++) { var prefname = this._filenamePrefs[i]; var filename = this._prefBranch.getCharPref(prefname); var file = this._profileDir.clone(); file.append(filename); this.log("Checking file " + filename + " (" + prefname + ")"); // First loop through, save the preferred filename. if (!destFile) destFile = file; else importFile = file; if (file.exists()) return [destFile, importFile]; } // If we can't find any existing file, use the preferred file. return [destFile, null]; }, /* * _removeOldSignonsFiles * * Deletes any storage files that we're not using any more. */ _removeOldSignonsFiles : function() { // We've used a number of prefs over time due to compatibility issues. // Skip the first entry (the newest) and delete the others. for (var i = 1; i < this._filenamePrefs.length; i++) { var prefname = this._filenamePrefs[i]; var filename = this._prefBranch.getCharPref(prefname); var file = this._profileDir.clone(); file.append(filename); if (file.exists()) { this.log("Deleting old " + filename + " (" + prefname + ")"); try { file.remove(false); } catch (e) { this.log("NOTICE: Couldn't delete " + filename + ": " + e); } } } }, /* * _upgrade_entry_to_2E * * Updates the format of an entry from 2D to 2E. Returns an array of * logins (1 or 2), as sometimes updating an entry requires creating an * extra login. */ _upgrade_entry_to_2E : function (aLogin) { var upgradedLogins = [aLogin]; /* * For logins stored from HTTP channels * - scheme needs to be derived and prepended * - blank or missing realm becomes same as hostname. * * "site.com:80" --> "http://site.com" * "site.com:443" --> "https://site.com" * "site.com:123" --> Who knows! (So add both) * * Note: For HTTP logins, the hostname never contained a username * or password. EG "user@site.com:80" shouldn't ever happen. * * Note: Proxy logins are also stored in this format. */ if (aLogin.hostname.indexOf("://") == -1) { var oldHost = aLogin.hostname; // Check for a trailing port number, EG "site.com:80". If there's // no port, it wasn't saved by the browser and is probably some // arbitrary string picked by an extension. if (!/:\d+$/.test(aLogin.hostname)) { this.log("2E upgrade: no port, skipping " + aLogin.hostname); return upgradedLogins; } // Parse out "host:port". try { // Small hack: Need a scheme for nsIURI, so just prepend http. // We'll check for a port == -1 in case nsIURI ever starts // noticing that "http://foo:80" is using the default port. var uri = this._ioService.newURI("http://" + aLogin.hostname, null, null); var host = uri.host; var port = uri.port; } catch (e) { this.log("2E upgrade: Can't parse hostname " + aLogin.hostname); return upgradedLogins; } if (port == 80 || port == -1) aLogin.hostname = "http://" + host; else if (port == 443) aLogin.hostname = "https://" + host; else { // Not a standard port! Could be either http or https! // (Or maybe it's a proxy login!) To try and avoid // breaking logins, we'll add *both* http and https // versions. this.log("2E upgrade: Cloning login for " + aLogin.hostname); aLogin.hostname = "http://" + host + ":" + port; var extraLogin = new this._nsLoginInfo(); extraLogin.init("https://" + host + ":" + port, null, aLogin.httpRealm, aLogin.username, aLogin.password, "", ""); // We don't have decrypted values, unless we're importing from IE, // so clone the encrypted bits into the new entry. extraLogin.wrappedJSObject.encryptedPassword = aLogin.wrappedJSObject.encryptedPassword; extraLogin.wrappedJSObject.encryptedUsername = aLogin.wrappedJSObject.encryptedUsername; if (extraLogin.httpRealm == "") extraLogin.httpRealm = extraLogin.hostname; upgradedLogins.push(extraLogin); } // If the server didn't send a realm (or it was blank), we // previously didn't store anything. if (aLogin.httpRealm == "") aLogin.httpRealm = aLogin.hostname; this.log("2E upgrade: " + oldHost + " ---> " + aLogin.hostname); return upgradedLogins; } /* * For form logins and non-HTTP channel logins (both were stored in * the same format): * * Standardize URLs (.hostname and .actionURL) * - remove default port numbers, if specified * "http://site.com:80" --> "http://site.com" * - remove usernames from URL (may move into aLogin.username) * "ftp://user@site.com" --> "ftp://site.com" * * Note: Passwords in the URL ("foo://user:pass@site.com") were not * stored in FF2, so no need to try to move the value into * aLogin.password. */ // closures in cleanupURL var ioService = this._ioService; var log = this.log; function cleanupURL(aURL, allowJS) { var newURL, username = null, pathname = ""; try { var uri = ioService.newURI(aURL, null, null); var scheme = uri.scheme; if (allowJS && scheme == "javascript") return ["javascript:", null, ""]; newURL = scheme + "://" + uri.host; // If the URL explicitly specified a port, only include it when // it's not the default. (We never want "http://foo.com:80") port = uri.port; if (port != -1) { var handler = ioService.getProtocolHandler(scheme); if (port != handler.defaultPort) newURL += ":" + port; } // Could be a channel login with a username. if (scheme != "http" && scheme != "https" && uri.username) username = uri.username; if (uri.path != "/") pathname = uri.path; } catch (e) { log("Can't cleanup URL: " + aURL + " e: " + e); newURL = aURL; } if (newURL != aURL) log("2E upgrade: " + aURL + " ---> " + newURL); return [newURL, username, pathname]; } const isMailNews = /^(ldaps?|smtp|imap|news|mailbox):\/\//; // Old mailnews logins were protocol logins with a username/password // field name set. var isFormLogin = (aLogin.formSubmitURL || aLogin.usernameField || aLogin.passwordField) && !isMailNews.test(aLogin.hostname); var [hostname, username, pathname] = cleanupURL(aLogin.hostname); aLogin.hostname = hostname; // If a non-HTTP URL contained a username, it wasn't stored in the // encrypted username field (which contains an encrypted empty value) // (Don't do this if it's a form login, though.) if (username && !isFormLogin) { if (isMailNews.test(aLogin.hostname)) try { username = decodeURIComponent(username); } catch (ex) { // It has been seen that some usernames cannot be decoded // on upgrade, so if hit the case, log it and re-throw so // that we can handle it in the caller. this.log("Error decoding \"" + username + "\": " + ex); throw(ex); } var [encUsername, userCanceled] = this._encrypt(username); if (!userCanceled) aLogin.wrappedJSObject.encryptedUsername = encUsername; } if (aLogin.formSubmitURL) { [hostname, username, pathname] = cleanupURL(aLogin.formSubmitURL, true); aLogin.formSubmitURL = hostname; // username, if any, ignored. } /* * For logins stored from non-HTTP channels * - Set httpRealm so they don't look like form logins * "ftp://site.com" --> "ftp://site.com (ftp://site.com)" * * Tricky: Form logins and non-HTTP channel logins are stored in the * same format, and we don't want to add a realm to a form login. * Form logins have field names, so only update the realm if there are * no field names set. [Any login with a http[s]:// hostname is always * a form login, so explicitly ignore those just to be safe.] */ const isHTTP = /^https?:\/\//; const isLDAP = /^ldaps?:\/\//; const isNews = /^news?:\/\//; if (!isHTTP.test(aLogin.hostname) && !isFormLogin) { // LDAP and News logins need to keep the path. if (isLDAP.test(aLogin.hostname) || isNews.test(aLogin.hostname)) aLogin.httpRealm = aLogin.hostname + pathname; else aLogin.httpRealm = aLogin.hostname; aLogin.formSubmitURL = null; // Null out the form items because mailnews will no longer treat // or expect these as form logins if (isMailNews.test(aLogin.hostname)) { aLogin.usernameField = ""; aLogin.passwordField = ""; } this.log("2E upgrade: set empty realm to " + aLogin.httpRealm); } return upgradedLogins; }, /* * _readFile * */ _readFile : function () { var formatVersion; this.log("Reading passwords from " + this._signonsFile.path); // If it doesn't exist, just bail out. if (!this._signonsFile.exists()) { this.log("No existing signons file found."); return; } var inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. createInstance(Ci.nsIFileInputStream); // init the stream as RD_ONLY, -1 == default permissions. inputStream.init(this._signonsFile, 0x01, -1, null); var lineStream = inputStream.QueryInterface(Ci.nsILineInputStream); var line = { value: "" }; const STATE = { HEADER : 0, REJECT : 1, REALM : 2, USERFIELD : 3, USERVALUE : 4, PASSFIELD : 5, PASSVALUE : 6, ACTIONURL : 7, FILLER : 8 }; var parseState = STATE.HEADER; var processEntry = false; var discardEntry = false; do { var hasMore = lineStream.readLine(line); try { line.value = this._utfConverter.ConvertToUnicode(line.value); } catch (e) { this.log("Bad UTF8 conversion: " + line.value); this._utfConverterReset(); } switch (parseState) { // Check file header case STATE.HEADER: if (line.value == "#2c") { formatVersion = 0x2c; } else if (line.value == "#2d") { formatVersion = 0x2d; } else if (line.value == "#2e") { formatVersion = 0x2e; } else { this.log("invalid file header (" + line.value + ")"); throw "invalid file header in signons file"; // We could disable later writing to file, so we // don't clobber whatever it is. ...however, that // would mean corrupt files are not self-healing. return; } parseState++; break; // Line is a hostname for which passwords should never be saved. case STATE.REJECT: if (line.value == ".") { parseState++; break; } this._disabledHosts[line.value] = true; break; // Line is a hostname, saved login(s) will follow case STATE.REALM: var hostrealm = line.value; // Format is "http://site.com", with "(some realm)" // appended if it's a HTTP-Auth login. const realmFormat = /^(.+?)( \(.*\))?$/; var matches = realmFormat.exec(hostrealm); var hostname, httpRealm; if (matches && matches.length == 3) { hostname = matches[1]; httpRealm = matches[2] ? matches[2].slice(2, -1) : null; } else { if (hostrealm != "") { // Uhoh. This shouldn't happen, but try to deal. this.log("Error parsing host/realm: " + hostrealm); } hostname = hostrealm; httpRealm = null; } parseState++; break; // Line is the HTML 'name' attribute for the username field // (or "." to indicate end of hostrealm) case STATE.USERFIELD: if (line.value == ".") { discardEntry = false; parseState = STATE.REALM; break; } // If we're discarding the entry, keep looping in this // state until we hit the "." marking the end of the entry. if (discardEntry) break; var entry = new this._nsLoginInfo(); entry.hostname = hostname; entry.httpRealm = httpRealm; entry.usernameField = line.value; parseState++; break; // Line is a username case STATE.USERVALUE: entry.wrappedJSObject.encryptedUsername = line.value; parseState++; break; // Line is the HTML 'name' attribute for the password field, // with a leading '*' character case STATE.PASSFIELD: if (line.value.charAt(0) != '*') { discardEntry = true; entry = null; parseState = STATE.USERFIELD; break; } entry.passwordField = line.value.substr(1); parseState++; break; // Line is a password case STATE.PASSVALUE: entry.wrappedJSObject.encryptedPassword = line.value; // Version 2C doesn't have an ACTIONURL line, so // process entry now. if (formatVersion < 0x2d) processEntry = true; parseState++; break; // Line is the action URL case STATE.ACTIONURL: var formSubmitURL = line.value; if (!formSubmitURL && entry.httpRealm != null) entry.formSubmitURL = null; else entry.formSubmitURL = formSubmitURL; // Version 2D doesn't have a FILLER line, so // process entry now. if (formatVersion < 0x2e) processEntry = true; parseState++; break; // Line is unused filler for future use case STATE.FILLER: // Save the line's value (so we can dump it back out when // we save the file next time) for forwards compatability. entry.wrappedJSObject.filler = line.value; processEntry = true; parseState++; break; } // If we've read all the lines for the current entry, // process it and reset the parse state for the next entry. if (processEntry) { if (formatVersion < 0x2d) { // A blank, non-null value is handled as a wildcard. if (entry.httpRealm != null) entry.formSubmitURL = null; else entry.formSubmitURL = ""; } // Upgrading an entry to 2E can sometimes result in the need // to create an extra login. var entries = [entry]; if (formatVersion < 0x2e) { try { entries = this._upgrade_entry_to_2E(entry); } catch (ex) { // For some reason we couldn't decode this entry, // therefore, drop it and carry on so that we can // hopefully translate the other entries. entries = []; } } for each (var e in entries) { if (!this._logins[e.hostname]) this._logins[e.hostname] = []; this._logins[e.hostname].push(e); } entry = null; processEntry = false; parseState = STATE.USERFIELD; } } while (hasMore); lineStream.close(); return; }, /* * _writeFile * * Returns true if the operation was successfully completed, or false * if there was an error (probably the user refusing to enter a * master password if prompted). */ _writeFile : function () { var converter = this._utfConverter; function writeLine(data) { data = converter.ConvertFromUnicode(data); data += converter.Finish(); data += "\r\n"; outputStream.write(data, data.length); } this.log("Writing passwords to " + this._signonsFile.path); var safeStream = Cc["@mozilla.org/network/safe-file-output-stream;1"]. createInstance(Ci.nsIFileOutputStream); // WR_ONLY|CREAT|TRUNC safeStream.init(this._signonsFile, 0x02 | 0x08 | 0x20, 0600, null); var outputStream = Cc["@mozilla.org/network/buffered-output-stream;1"]. createInstance(Ci.nsIBufferedOutputStream); outputStream.init(safeStream, 8192); outputStream.QueryInterface(Ci.nsISafeOutputStream); // for .finish() // write file version header writeLine("#2e"); // write disabled logins list for (var hostname in this._disabledHosts) { writeLine(hostname); } // write end-of-reject-list marker writeLine("."); for (var hostname in this._logins) { function sortByRealm(a,b) { a = a.httpRealm; b = b.httpRealm; if (!a && !b) return 0; if (!a || a < b) return -1; if (!b || b > a) return 1; return 0; // a==b, neither is null } // Sort logins by httpRealm. This allows us to group multiple // logins for the same realm together. this._logins[hostname].sort(sortByRealm); // write each login known for the host var lastRealm = null; var firstEntry = true; var userCanceled = false; for each (var login in this._logins[hostname]) { // If this login is for a new realm, start a new entry. if (login.httpRealm != lastRealm || firstEntry) { // end previous entry, if needed. if (!firstEntry) writeLine("."); var hostrealm = login.hostname; if (login.httpRealm) hostrealm += " (" + login.httpRealm + ")"; writeLine(hostrealm); } firstEntry = false; // Get the encrypted value of the username. Newly added // logins will need the plaintext value encrypted. var encUsername = login.wrappedJSObject.encryptedUsername; if (!encUsername) { [encUsername, userCanceled] = this._encrypt(login.username); login.wrappedJSObject.encryptedUsername = encUsername; } if (userCanceled) break; // Get the encrypted value of the password. Newly added // logins will need the plaintext value encrypted. var encPassword = login.wrappedJSObject.encryptedPassword; if (!encPassword) { [encPassword, userCanceled] = this._encrypt(login.password); login.wrappedJSObject.encryptedPassword = encPassword; } if (userCanceled) break; writeLine((login.usernameField ? login.usernameField : "")); writeLine(encUsername); writeLine("*" + (login.passwordField ? login.passwordField : "")); writeLine(encPassword); writeLine((login.formSubmitURL ? login.formSubmitURL : "")); if (login.wrappedJSObject.filler) writeLine(login.wrappedJSObject.filler); else writeLine("---"); lastRealm = login.httpRealm; } if (userCanceled) { this.log("User canceled Master Password, aborting write."); // .close will cause an abort w/o modifying original file outputStream.close(); return false; } // write end-of-host marker writeLine("."); } // [if there were no hosts, no end-of-host marker (".") needed] outputStream.finish(); return true; }, /* * _decryptLogins * * Decrypts username and password fields in the provided array of * logins. This is deferred from the _readFile() code, so that * the user is not prompted for a master password (if set) until * the entries are actually used. * * The entries specified by the array will be decrypted, if possible. * An array of successfully decrypted logins will be returned. The return * value should be given to external callers (since still-encrypted * entries are useless), whereas internal callers generally don't want * to lose unencrypted entries (eg, because the user clicked Cancel * instead of entering their master password) */ _decryptLogins : function (logins) { var result = [], userCanceled = false; for each (var login in logins) { var decryptedUsername, decryptedPassword; [decryptedUsername, userCanceled] = this._decrypt(login.wrappedJSObject.encryptedUsername); if (userCanceled) break; [decryptedPassword, userCanceled] = this._decrypt(login.wrappedJSObject.encryptedPassword); // Probably can't hit this case, but for completeness... if (userCanceled) break; // If decryption failed (corrupt entry?) skip it. Note that we // allow password-only logins, so decryptedUsername can be "". if (decryptedUsername == null || !decryptedPassword) continue; // Return copies to the caller. Prevents callers from modifying // our internal stoage, and helps avoid keeping decrypted data in // memory (although this is fuzzy, because of GC issues). var clone = new this._nsLoginInfo(); clone.init(login.hostname, login.formSubmitURL, login.httpRealm, decryptedUsername, decryptedPassword, login.usernameField, login.passwordField); // Old mime64-obscured entries should be opportunistically // reencrypted in the new format. var recrypted; if (login.wrappedJSObject.encryptedUsername && login.wrappedJSObject.encryptedUsername.charAt(0) == '~') { [recrypted, userCanceled] = this._encrypt(decryptedUsername); if (userCanceled) break; login.wrappedJSObject.encryptedUsername = recrypted; } if (login.wrappedJSObject.encryptedPassword && login.wrappedJSObject.encryptedPassword.charAt(0) == '~') { [recrypted, userCanceled] = this._encrypt(decryptedPassword); if (userCanceled) break; login.wrappedJSObject.encryptedPassword = recrypted; } result.push(clone); } return [result, userCanceled]; }, /* * _encrypt * * Encrypts the specified string, using the SecretDecoderRing. * * Returns [cipherText, userCanceled] where: * cipherText -- the encrypted string, or null if it failed. * userCanceled -- if the encryption failed, this is true if the * user selected Cancel when prompted to enter their * Master Password. The caller should bail out, and not * not request that more things be encrypted (which * results in prompting the user for a Master Password * over and over.) */ _encrypt : function (plainText) { var cipherText = null, userCanceled = false; try { var plainOctet = this._utfConverter.ConvertFromUnicode(plainText); plainOctet += this._utfConverter.Finish(); cipherText = this._decoderRing.encryptString(plainOctet); } catch (e) { this.log("Failed to encrypt string. (" + e.name + ")"); // If the user clicks Cancel, we get NS_ERROR_FAILURE. // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE). if (e.result == Components.results.NS_ERROR_FAILURE) userCanceled = true; } return [cipherText, userCanceled]; }, /* * _decrypt * * Decrypts the specified string, using the SecretDecoderRing. * * Returns [plainText, userCanceled] where: * plainText -- the decrypted string, or null if it failed. * userCanceled -- if the decryption failed, this is true if the * user selected Cancel when prompted to enter their * Master Password. The caller should bail out, and not * not request that more things be decrypted (which * results in prompting the user for a Master Password * over and over.) */ _decrypt : function (cipherText) { var plainText = null, userCanceled = false; try { var plainOctet; if (cipherText.charAt(0) == '~') { // The older file format obscured entries by // base64-encoding them. These entries are signaled by a // leading '~' character. plainOctet = atob(cipherText.substring(1)); } else { plainOctet = this._decoderRing.decryptString(cipherText); } plainText = this._utfConverter.ConvertToUnicode(plainOctet); } catch (e) { this.log("Failed to decrypt string: " + cipherText + " (" + e.name + ")"); // In the unlikely event the converter threw, reset it. this._utfConverterReset(); // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE. // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE // Wrong passwords are handled by the decoderRing reprompting; // we get no notification. if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE) userCanceled = true; } return [plainText, userCanceled]; }, /* ================== nsILoginManagerIEMigratorHelper ================== */ _migrationLoginManager : null, /* * migrateAndAddLogin * * Given a login with IE6-formatted fields, migrates it to the new format * and adds it to the login manager. * * Experimentally derived format of IE6 logins, see: * https://bugzilla.mozilla.org/attachment.cgi?id=319346 * * HTTP AUTH: * - hostname is always "example.com:123" * * "example.com", "http://example.com", "http://example.com:80" all * end up as just "example.com:80" * * Entering "example.com:80" in the URL bar isn't recognized as a * valid URL by IE6. * * "https://example.com" is saved as "example.com:443" * * "https://example.com:666" is saved as "example.com:666". Thus, for * non-standard ports we don't know the right scheme, so create both. * * - an empty or missing "realm" in the WWW-Authenticate reply is stored * as just an empty string by IE6. * * - IE6 will store logins where one or both (!) of the username/password * is left blank. We don't support logins without a password, so these * logins won't be added [addLogin() will throw]. * * - IE6 won't recognize a URL with and embedded username/password (eg * http://user@example.com, http://user:pass@example.com), so these * shouldn't be encountered. * * - Our migration code doesn't extract non-HTTP logins (eg, FTP). So * they shouldn't be encountered here. (Verified by saving FTP logins * in IE and then importing in Firefox.) * * * FORM LOGINS: * - hostname is "http://site.com" or "https://site.com". * * scheme always included * * default port not included * - port numbers, even for non-standard posts, are never present! * unfortunately, this means logins will only work on the default * port, because we don't know what the original was (or even that * it wasn't originally stored for the original port). * - Logins are stored without a field name by IE, but we look one up * in the migrator for the username. The password field name will * always be empty-string. */ migrateAndAddLogin : function (aLogin) { // Initialize outself on the first call if (!this._migrationLoginManager) { // Connect to the correct preferences branch. this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService); this._prefBranch = this._prefBranch.getBranch("signon."); this._prefBranch.QueryInterface(Ci.nsIPrefBranch2); this._debug = this._prefBranch.getBoolPref("debug"); this._migrationLoginManager = Cc["@mozilla.org/login-manager;1"]. getService(Ci.nsILoginManager); } this.log("Migrating login for " + aLogin.hostname); // The IE login is in the same format as the old password // manager entries, so just reuse that code. var logins = this._upgrade_entry_to_2E(aLogin); // Add logins via the login manager (and not this.addLogin), // lest an alternative storage module be in use. for each (var login in logins) this._migrationLoginManager.addLogin(login); } }; // end of nsLoginManagerStorage_legacy implementation var component = [LoginManagerStorage_legacy]; function NSGetModule(compMgr, fileSpec) { return XPCOMUtils.generateModule(component); }