diff options
Diffstat (limited to 'bundles/XO/components/storage-Legacy.js')
-rwxr-xr-x | bundles/XO/components/storage-Legacy.js | 1478 |
1 files changed, 1478 insertions, 0 deletions
diff --git a/bundles/XO/components/storage-Legacy.js b/bundles/XO/components/storage-Legacy.js new file mode 100755 index 0000000..0d70f99 --- /dev/null +++ b/bundles/XO/components/storage-Legacy.js @@ -0,0 +1,1478 @@ +/* ***** 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 Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske <dolske@mozilla.com> (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"; + + 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); + } + } + + 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); + } + + 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)) + username = decodeURIComponent(username); + + 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) + entries = this._upgrade_entry_to_2E(entry); + + + 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); +} |