2014-06-25 09:12:07 +04:00
|
|
|
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
2014-01-07 20:29:41 +04:00
|
|
|
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
|
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Contains functions shared by different Login Manager components.
|
|
|
|
*
|
|
|
|
* This JavaScript module exists in order to share code between the different
|
|
|
|
* XPCOM components that constitute the Login Manager, including implementations
|
|
|
|
* of nsILoginManager and nsILoginManagerStorage.
|
|
|
|
*/
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
|
|
"LoginHelper",
|
|
|
|
];
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//// Globals
|
|
|
|
|
|
|
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
|
|
|
|
2015-03-16 22:42:08 +03:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2014-01-07 20:29:41 +04:00
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//// LoginHelper
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Contains functions shared by different Login Manager components.
|
|
|
|
*/
|
|
|
|
this.LoginHelper = {
|
2015-03-16 22:42:08 +03:00
|
|
|
/**
|
|
|
|
* Warning: this only updates if a logger was created.
|
|
|
|
*/
|
|
|
|
debug: Services.prefs.getBoolPref("signon.debug"),
|
|
|
|
|
|
|
|
createLogger(aLogPrefix) {
|
|
|
|
let getMaxLogLevel = () => {
|
|
|
|
return this.debug ? "debug" : "error";
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
|
|
|
|
let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
|
|
|
|
let consoleOptions = {
|
|
|
|
maxLogLevel: getMaxLogLevel(),
|
|
|
|
prefix: aLogPrefix,
|
|
|
|
};
|
|
|
|
let logger = new ConsoleAPI(consoleOptions);
|
|
|
|
|
|
|
|
// Watch for pref changes and update this.debug and the maxLogLevel for created loggers
|
|
|
|
Services.prefs.addObserver("signon.", () => {
|
|
|
|
this.debug = Services.prefs.getBoolPref("signon.debug");
|
|
|
|
logger.maxLogLevel = getMaxLogLevel();
|
|
|
|
}, false);
|
|
|
|
|
|
|
|
return logger;
|
|
|
|
},
|
|
|
|
|
2014-01-07 20:29:41 +04:00
|
|
|
/**
|
|
|
|
* 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 parsed incorrectly. Reject hostnames that we can't store correctly.
|
|
|
|
*
|
|
|
|
* @throws String with English message in case validation failed.
|
|
|
|
*/
|
|
|
|
checkHostnameValue: function (aHostname)
|
|
|
|
{
|
|
|
|
// Nulls are invalid, as they don't round-trip well. Newlines are also
|
|
|
|
// invalid for any field stored as plaintext, and a hostname made of a
|
|
|
|
// single dot cannot be stored in the legacy format.
|
|
|
|
if (aHostname == "." ||
|
|
|
|
aHostname.indexOf("\r") != -1 ||
|
|
|
|
aHostname.indexOf("\n") != -1 ||
|
|
|
|
aHostname.indexOf("\0") != -1) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Invalid hostname");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 parsed incorrectly. Reject logins that we can't store correctly.
|
|
|
|
*
|
|
|
|
* @throws String with English message in case validation failed.
|
|
|
|
*/
|
|
|
|
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")) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("login values can't contain nulls");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("login values can't contain nulls");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Newlines are invalid for any field stored as plaintext.
|
|
|
|
if (badCharacterPresent(aLogin, "\r") ||
|
|
|
|
badCharacterPresent(aLogin, "\n")) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("login values can't contain newlines");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// A line with just a "." can have special meaning.
|
|
|
|
if (aLogin.usernameField == "." ||
|
|
|
|
aLogin.formSubmitURL == ".") {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("login values can't be periods");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("bad parens in hostname");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new login object that results by modifying the given object with
|
|
|
|
* the provided data.
|
|
|
|
*
|
|
|
|
* @param aOldStoredLogin
|
|
|
|
* Existing nsILoginInfo object to modify.
|
|
|
|
* @param aNewLoginData
|
|
|
|
* The new login values, either as nsILoginInfo or nsIProperyBag.
|
|
|
|
*
|
|
|
|
* @return The newly created nsILoginInfo object.
|
|
|
|
*
|
|
|
|
* @throws String with English message in case validation failed.
|
|
|
|
*/
|
|
|
|
buildModifiedLogin: function (aOldStoredLogin, aNewLoginData)
|
|
|
|
{
|
|
|
|
function bagHasProperty(aPropName)
|
|
|
|
{
|
|
|
|
try {
|
|
|
|
aNewLoginData.getProperty(aPropName);
|
|
|
|
return true;
|
|
|
|
} catch (ex) { }
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
aOldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
|
|
|
|
|
|
let newLogin;
|
|
|
|
if (aNewLoginData instanceof Ci.nsILoginInfo) {
|
|
|
|
// Clone the existing login to get its nsILoginMetaInfo, then init it
|
|
|
|
// with the replacement nsILoginInfo data from the new login.
|
|
|
|
newLogin = aOldStoredLogin.clone();
|
|
|
|
newLogin.init(aNewLoginData.hostname,
|
|
|
|
aNewLoginData.formSubmitURL, aNewLoginData.httpRealm,
|
|
|
|
aNewLoginData.username, aNewLoginData.password,
|
|
|
|
aNewLoginData.usernameField, aNewLoginData.passwordField);
|
|
|
|
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
|
|
|
|
|
|
// Automatically update metainfo when password is changed.
|
|
|
|
if (newLogin.password != aOldStoredLogin.password) {
|
|
|
|
newLogin.timePasswordChanged = Date.now();
|
|
|
|
}
|
|
|
|
} else if (aNewLoginData instanceof Ci.nsIPropertyBag) {
|
|
|
|
// Clone the existing login, along with all its properties.
|
|
|
|
newLogin = aOldStoredLogin.clone();
|
|
|
|
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
|
|
|
|
|
|
// Automatically update metainfo when password is changed.
|
|
|
|
// (Done before the main property updates, lest the caller be
|
|
|
|
// explicitly updating both .password and .timePasswordChanged)
|
|
|
|
if (bagHasProperty("password")) {
|
|
|
|
let newPassword = aNewLoginData.getProperty("password");
|
|
|
|
if (newPassword != aOldStoredLogin.password) {
|
|
|
|
newLogin.timePasswordChanged = Date.now();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let propEnum = aNewLoginData.enumerator;
|
|
|
|
while (propEnum.hasMoreElements()) {
|
|
|
|
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
|
|
|
switch (prop.name) {
|
|
|
|
// nsILoginInfo
|
|
|
|
case "hostname":
|
|
|
|
case "httpRealm":
|
|
|
|
case "formSubmitURL":
|
|
|
|
case "username":
|
|
|
|
case "password":
|
|
|
|
case "usernameField":
|
|
|
|
case "passwordField":
|
|
|
|
// nsILoginMetaInfo
|
|
|
|
case "guid":
|
|
|
|
case "timeCreated":
|
|
|
|
case "timeLastUsed":
|
|
|
|
case "timePasswordChanged":
|
|
|
|
case "timesUsed":
|
|
|
|
newLogin[prop.name] = prop.value;
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Fake property, allows easy incrementing.
|
|
|
|
case "timesUsedIncrement":
|
|
|
|
newLogin.timesUsed += prop.value;
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Fail if caller requests setting an unknown property.
|
|
|
|
default:
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Unexpected propertybag item: " + prop.name);
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("newLoginData needs an expected interface!");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sanity check the login
|
|
|
|
if (newLogin.hostname == null || newLogin.hostname.length == 0) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Can't add a login with a null or empty hostname.");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// For logins w/o a username, set to "", not null.
|
|
|
|
if (newLogin.username == null) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Can't add a login with a null username.");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (newLogin.password == null || newLogin.password.length == 0) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Can't add a login with a null or empty password.");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
|
|
|
// We have a form submit URL. Can't have a HTTP realm.
|
|
|
|
if (newLogin.httpRealm != null) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
} else if (newLogin.httpRealm) {
|
|
|
|
// We have a HTTP realm. Can't have a form submit URL.
|
|
|
|
if (newLogin.formSubmitURL != null) {
|
2015-02-24 05:45:00 +03:00
|
|
|
throw new Error("Can't add a login with both a httpRealm and formSubmitURL.");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Need one or the other!
|
2015-02-24 08:42:14 +03:00
|
|
|
throw new Error("Can't add a login without a httpRealm or formSubmitURL.");
|
2014-01-07 20:29:41 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Throws if there are bogus values.
|
|
|
|
this.checkLoginValues(newLogin);
|
|
|
|
|
|
|
|
return newLogin;
|
|
|
|
},
|
2015-07-27 08:25:46 +03:00
|
|
|
|
2015-07-30 10:46:06 +03:00
|
|
|
/**
|
|
|
|
* Removes duplicates from a list of logins.
|
|
|
|
*
|
|
|
|
* @param {nsILoginInfo[]} logins
|
|
|
|
* A list of logins we want to deduplicate.
|
|
|
|
*
|
|
|
|
* @param {string[] = ["username", "password"]} uniqueKeys
|
|
|
|
* A list of login attributes to use as unique keys for the deduplication.
|
|
|
|
*
|
|
|
|
* @returns {nsILoginInfo[]} list of unique logins.
|
|
|
|
*/
|
|
|
|
dedupeLogins(logins, uniqueKeys = ["username", "password"]) {
|
|
|
|
const KEY_DELIMITER = ":";
|
|
|
|
|
|
|
|
// Generate a unique key string from a login.
|
|
|
|
function getKey(login, uniqueKeys) {
|
|
|
|
return uniqueKeys.reduce((prev, key) => prev + KEY_DELIMITER + login[key], "");
|
|
|
|
}
|
|
|
|
|
|
|
|
// We use a Map to easily lookup logins by their unique keys.
|
|
|
|
let loginsByKeys = new Map();
|
|
|
|
for (let login of logins) {
|
|
|
|
let key = getKey(login, uniqueKeys);
|
|
|
|
// If we find a more recently used login for the same key, replace the existing one.
|
|
|
|
if (loginsByKeys.has(key)) {
|
|
|
|
let loginDate = login.QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
|
|
|
|
let storedLoginDate = loginsByKeys.get(key).QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed;
|
|
|
|
if (loginDate < storedLoginDate) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
loginsByKeys.set(key, login);
|
|
|
|
}
|
|
|
|
// Return the map values in the form of an array.
|
|
|
|
return [...loginsByKeys.values()];
|
|
|
|
},
|
|
|
|
|
2015-07-27 08:25:46 +03:00
|
|
|
/**
|
|
|
|
* Open the password manager window.
|
|
|
|
*
|
|
|
|
* @param {Window} window
|
|
|
|
* the window from where we want to open the dialog
|
|
|
|
*
|
|
|
|
* @param {string} [filterString=""]
|
|
|
|
* the filterString parameter to pass to the login manager dialog
|
|
|
|
*/
|
|
|
|
openPasswordManager(window, filterString = "") {
|
|
|
|
let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
|
|
|
|
if (win) {
|
|
|
|
win.setFilter(filterString);
|
|
|
|
win.focus();
|
|
|
|
} else {
|
|
|
|
window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
|
|
|
|
"Toolkit:PasswordManager", "",
|
|
|
|
{filterString : filterString});
|
|
|
|
}
|
|
|
|
},
|
2014-01-07 20:29:41 +04:00
|
|
|
};
|