зеркало из https://github.com/mozilla/gecko-dev.git
546 строки
18 KiB
JavaScript
546 строки
18 KiB
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/. */
|
|
|
|
/*
|
|
* nsILoginManagerStorage implementation for the JSON back-end.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "LoginHelper",
|
|
"resource://gre/modules/LoginHelper.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "LoginImport",
|
|
"resource://gre/modules/LoginImport.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "LoginStore",
|
|
"resource://gre/modules/LoginStore.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "OS",
|
|
"resource://gre/modules/osfile.jsm");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
|
"@mozilla.org/uuid-generator;1",
|
|
"nsIUUIDGenerator");
|
|
|
|
this.LoginManagerStorage_json = function() {};
|
|
|
|
this.LoginManagerStorage_json.prototype = {
|
|
classID: Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsILoginManagerStorage]),
|
|
|
|
_xpcom_factory: XPCOMUtils.generateSingletonFactory(this.LoginManagerStorage_json),
|
|
|
|
__crypto: null, // nsILoginManagerCrypto service
|
|
get _crypto() {
|
|
if (!this.__crypto) {
|
|
this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
|
|
getService(Ci.nsILoginManagerCrypto);
|
|
}
|
|
return this.__crypto;
|
|
},
|
|
|
|
initialize() {
|
|
try {
|
|
// Force initialization of the crypto module.
|
|
// See bug 717490 comment 17.
|
|
this._crypto;
|
|
|
|
// Set the reference to LoginStore synchronously.
|
|
let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
|
|
"logins.json");
|
|
this._store = new LoginStore(jsonPath);
|
|
|
|
return (async () => {
|
|
// Load the data asynchronously.
|
|
this.log("Opening database at", this._store.path);
|
|
await this._store.load();
|
|
|
|
// The import from previous versions operates the first time
|
|
// that this built-in storage back-end is used. This may be
|
|
// later than expected, in case add-ons have registered an
|
|
// alternate storage that disabled the default one.
|
|
try {
|
|
if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
|
|
return;
|
|
}
|
|
} catch (ex) {
|
|
// If the preference does not exist, we need to import.
|
|
}
|
|
|
|
// Import only happens asynchronously.
|
|
let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
|
|
"signons.sqlite");
|
|
if (await OS.File.exists(sqlitePath)) {
|
|
let loginImport = new LoginImport(this._store, sqlitePath);
|
|
// Failures during import, for example due to a corrupt
|
|
// file or a schema version that is too old, will not
|
|
// prevent us from marking the operation as completed.
|
|
// At the next startup, we will not try the import again.
|
|
await loginImport.import().catch(Cu.reportError);
|
|
this._store.saveSoon();
|
|
}
|
|
|
|
// We won't attempt import again on next startup.
|
|
Services.prefs.setBoolPref("signon.importedFromSqlite", true);
|
|
})().catch(Cu.reportError);
|
|
} catch (e) {
|
|
this.log("Initialization failed:", e);
|
|
throw new Error("Initialization failed");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Internal method used by regression tests only. It is called before
|
|
* replacing this storage module with a new instance.
|
|
*/
|
|
terminate() {
|
|
this._store._saver.disarm();
|
|
return this._store._save();
|
|
},
|
|
|
|
addLogin(login, preEncrypted = false) {
|
|
this._store.ensureDataReady();
|
|
|
|
// Throws if there are bogus values.
|
|
LoginHelper.checkLoginValues(login);
|
|
|
|
let [encUsername, encPassword, encType] = preEncrypted ?
|
|
[login.username, login.password, this._crypto.defaultEncType] :
|
|
this._encryptLogin(login);
|
|
|
|
// Clone the login, so we don't modify the caller's object.
|
|
let loginClone = login.clone();
|
|
|
|
// Initialize the nsILoginMetaInfo fields, unless the caller gave us values
|
|
loginClone.QueryInterface(Ci.nsILoginMetaInfo);
|
|
if (loginClone.guid) {
|
|
let guid = loginClone.guid;
|
|
if (!this._isGuidUnique(guid)) {
|
|
// We have an existing GUID, but it's possible that entry is unable
|
|
// to be decrypted - if that's the case we remove the existing one
|
|
// and allow this one to be added.
|
|
let existing = this._searchLogins({guid})[0];
|
|
if (this._decryptLogins(existing).length) {
|
|
// Existing item is good, so it's an error to try and re-add it.
|
|
throw new Error("specified GUID already exists");
|
|
}
|
|
// find and remove the existing bad entry.
|
|
let foundIndex = this._store.data.logins.findIndex(l => l.guid == guid);
|
|
if (foundIndex == -1) {
|
|
throw new Error("can't find a matching GUID to remove");
|
|
}
|
|
this._store.data.logins.splice(foundIndex, 1);
|
|
}
|
|
} else {
|
|
loginClone.guid = gUUIDGenerator.generateUUID().toString();
|
|
}
|
|
|
|
// Set timestamps
|
|
let currentTime = Date.now();
|
|
if (!loginClone.timeCreated) {
|
|
loginClone.timeCreated = currentTime;
|
|
}
|
|
if (!loginClone.timeLastUsed) {
|
|
loginClone.timeLastUsed = currentTime;
|
|
}
|
|
if (!loginClone.timePasswordChanged) {
|
|
loginClone.timePasswordChanged = currentTime;
|
|
}
|
|
if (!loginClone.timesUsed) {
|
|
loginClone.timesUsed = 1;
|
|
}
|
|
|
|
this._store.data.logins.push({
|
|
id: this._store.data.nextId++,
|
|
hostname: loginClone.hostname,
|
|
httpRealm: loginClone.httpRealm,
|
|
formSubmitURL: loginClone.formSubmitURL,
|
|
usernameField: loginClone.usernameField,
|
|
passwordField: loginClone.passwordField,
|
|
encryptedUsername: encUsername,
|
|
encryptedPassword: encPassword,
|
|
guid: loginClone.guid,
|
|
encType,
|
|
timeCreated: loginClone.timeCreated,
|
|
timeLastUsed: loginClone.timeLastUsed,
|
|
timePasswordChanged: loginClone.timePasswordChanged,
|
|
timesUsed: loginClone.timesUsed,
|
|
});
|
|
this._store.saveSoon();
|
|
|
|
// Send a notification that a login was added.
|
|
LoginHelper.notifyStorageChanged("addLogin", loginClone);
|
|
return loginClone;
|
|
},
|
|
|
|
removeLogin(login) {
|
|
this._store.ensureDataReady();
|
|
|
|
let [idToDelete, storedLogin] = this._getIdForLogin(login);
|
|
if (!idToDelete) {
|
|
throw new Error("No matching logins");
|
|
}
|
|
|
|
let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
|
|
if (foundIndex != -1) {
|
|
this._store.data.logins.splice(foundIndex, 1);
|
|
this._store.saveSoon();
|
|
}
|
|
|
|
LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
|
|
},
|
|
|
|
modifyLogin(oldLogin, newLoginData) {
|
|
this._store.ensureDataReady();
|
|
|
|
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
|
if (!idToModify) {
|
|
throw new Error("No matching logins");
|
|
}
|
|
|
|
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
|
|
|
|
// Check if the new GUID is duplicate.
|
|
if (newLogin.guid != oldStoredLogin.guid &&
|
|
!this._isGuidUnique(newLogin.guid)) {
|
|
throw new Error("specified GUID already exists");
|
|
}
|
|
|
|
// Look for an existing entry in case key properties changed.
|
|
if (!newLogin.matches(oldLogin, true)) {
|
|
let logins = this.findLogins({}, newLogin.hostname,
|
|
newLogin.formSubmitURL,
|
|
newLogin.httpRealm);
|
|
|
|
if (logins.some(login => newLogin.matches(login, true))) {
|
|
throw new Error("This login already exists.");
|
|
}
|
|
}
|
|
|
|
// Get the encrypted value of the username and password.
|
|
let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
|
|
|
|
for (let loginItem of this._store.data.logins) {
|
|
if (loginItem.id == idToModify) {
|
|
loginItem.hostname = newLogin.hostname;
|
|
loginItem.httpRealm = newLogin.httpRealm;
|
|
loginItem.formSubmitURL = newLogin.formSubmitURL;
|
|
loginItem.usernameField = newLogin.usernameField;
|
|
loginItem.passwordField = newLogin.passwordField;
|
|
loginItem.encryptedUsername = encUsername;
|
|
loginItem.encryptedPassword = encPassword;
|
|
loginItem.guid = newLogin.guid;
|
|
loginItem.encType = encType;
|
|
loginItem.timeCreated = newLogin.timeCreated;
|
|
loginItem.timeLastUsed = newLogin.timeLastUsed;
|
|
loginItem.timePasswordChanged = newLogin.timePasswordChanged;
|
|
loginItem.timesUsed = newLogin.timesUsed;
|
|
this._store.saveSoon();
|
|
break;
|
|
}
|
|
}
|
|
|
|
LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
|
|
},
|
|
|
|
/**
|
|
* @return {nsILoginInfo[]}
|
|
*/
|
|
getAllLogins(count) {
|
|
let [logins, ids] = this._searchLogins({});
|
|
|
|
// decrypt entries for caller.
|
|
logins = this._decryptLogins(logins);
|
|
|
|
this.log("_getAllLogins: returning", logins.length, "logins.");
|
|
if (count) {
|
|
count.value = logins.length;
|
|
} // needed for XPCOM
|
|
return logins;
|
|
},
|
|
|
|
/**
|
|
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
|
|
* JavaScript object and decrypt the results.
|
|
*
|
|
* @return {nsILoginInfo[]} which are decrypted.
|
|
*/
|
|
searchLogins(count, matchData) {
|
|
let realMatchData = {};
|
|
let options = {};
|
|
// Convert nsIPropertyBag to normal JS object
|
|
for (let prop of matchData.enumerator) {
|
|
switch (prop.name) {
|
|
// Some property names aren't field names but are special options to affect the search.
|
|
case "schemeUpgrades": {
|
|
options[prop.name] = prop.value;
|
|
break;
|
|
}
|
|
default: {
|
|
realMatchData[prop.name] = prop.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let [logins, ids] = this._searchLogins(realMatchData, options);
|
|
|
|
// Decrypt entries found for the caller.
|
|
logins = this._decryptLogins(logins);
|
|
|
|
count.value = logins.length; // needed for XPCOM
|
|
return logins;
|
|
},
|
|
|
|
/**
|
|
* Private method to perform arbitrary searches on any field. Decryption is
|
|
* left to the caller.
|
|
*
|
|
* Returns [logins, ids] for logins that match the arguments, where logins
|
|
* is an array of encrypted nsLoginInfo and ids is an array of associated
|
|
* ids in the database.
|
|
*/
|
|
_searchLogins(matchData, aOptions = {
|
|
schemeUpgrades: false,
|
|
}) {
|
|
this._store.ensureDataReady();
|
|
|
|
function match(aLogin) {
|
|
for (let field in matchData) {
|
|
let wantedValue = matchData[field];
|
|
switch (field) {
|
|
case "formSubmitURL":
|
|
if (wantedValue != null) {
|
|
// Historical compatibility requires this special case
|
|
if (aLogin.formSubmitURL == "") {
|
|
break;
|
|
}
|
|
if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
// fall through
|
|
case "hostname":
|
|
if (wantedValue != null) { // needed for formSubmitURL fall through
|
|
if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
// fall through
|
|
// Normal cases.
|
|
case "httpRealm":
|
|
case "id":
|
|
case "usernameField":
|
|
case "passwordField":
|
|
case "encryptedUsername":
|
|
case "encryptedPassword":
|
|
case "guid":
|
|
case "encType":
|
|
case "timeCreated":
|
|
case "timeLastUsed":
|
|
case "timePasswordChanged":
|
|
case "timesUsed":
|
|
if (wantedValue == null && aLogin[field]) {
|
|
return false;
|
|
} else if (aLogin[field] != wantedValue) {
|
|
return false;
|
|
}
|
|
break;
|
|
// Fail if caller requests an unknown property.
|
|
default:
|
|
throw new Error("Unexpected field: " + field);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
let foundLogins = [], foundIds = [];
|
|
for (let loginItem of this._store.data.logins) {
|
|
if (match(loginItem)) {
|
|
// Create the new nsLoginInfo object, push to array
|
|
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
|
createInstance(Ci.nsILoginInfo);
|
|
login.init(loginItem.hostname, loginItem.formSubmitURL,
|
|
loginItem.httpRealm, loginItem.encryptedUsername,
|
|
loginItem.encryptedPassword, loginItem.usernameField,
|
|
loginItem.passwordField);
|
|
// set nsILoginMetaInfo values
|
|
login.QueryInterface(Ci.nsILoginMetaInfo);
|
|
login.guid = loginItem.guid;
|
|
login.timeCreated = loginItem.timeCreated;
|
|
login.timeLastUsed = loginItem.timeLastUsed;
|
|
login.timePasswordChanged = loginItem.timePasswordChanged;
|
|
login.timesUsed = loginItem.timesUsed;
|
|
foundLogins.push(login);
|
|
foundIds.push(loginItem.id);
|
|
}
|
|
}
|
|
|
|
this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData,
|
|
"with options", aOptions);
|
|
return [foundLogins, foundIds];
|
|
},
|
|
|
|
/**
|
|
* Removes all logins from storage.
|
|
*/
|
|
removeAllLogins() {
|
|
this._store.ensureDataReady();
|
|
|
|
this.log("Removing all logins");
|
|
this._store.data.logins = [];
|
|
this._store.saveSoon();
|
|
|
|
LoginHelper.notifyStorageChanged("removeAllLogins", null);
|
|
},
|
|
|
|
findLogins(count, hostname, formSubmitURL, httpRealm) {
|
|
let loginData = {
|
|
hostname,
|
|
formSubmitURL,
|
|
httpRealm,
|
|
};
|
|
let matchData = { };
|
|
for (let field of ["hostname", "formSubmitURL", "httpRealm"]) {
|
|
if (loginData[field] != "") {
|
|
matchData[field] = loginData[field];
|
|
}
|
|
}
|
|
let [logins, ids] = this._searchLogins(matchData);
|
|
|
|
// Decrypt entries found for the caller.
|
|
logins = this._decryptLogins(logins);
|
|
|
|
this.log("_findLogins: returning", logins.length, "logins");
|
|
count.value = logins.length; // needed for XPCOM
|
|
return logins;
|
|
},
|
|
|
|
countLogins(hostname, formSubmitURL, httpRealm) {
|
|
let loginData = {
|
|
hostname,
|
|
formSubmitURL,
|
|
httpRealm,
|
|
};
|
|
let matchData = { };
|
|
for (let field of ["hostname", "formSubmitURL", "httpRealm"]) {
|
|
if (loginData[field] != "") {
|
|
matchData[field] = loginData[field];
|
|
}
|
|
}
|
|
let [logins, ids] = this._searchLogins(matchData);
|
|
|
|
this.log("_countLogins: counted logins:", logins.length);
|
|
return logins.length;
|
|
},
|
|
|
|
get uiBusy() {
|
|
return this._crypto.uiBusy;
|
|
},
|
|
|
|
get isLoggedIn() {
|
|
return this._crypto.isLoggedIn;
|
|
},
|
|
|
|
/**
|
|
* Returns an array with two items: [id, login]. If the login was not
|
|
* found, both items will be null. The returned login contains the actual
|
|
* stored login (useful for looking at the actual nsILoginMetaInfo values).
|
|
*/
|
|
_getIdForLogin(login) {
|
|
let matchData = { };
|
|
for (let field of ["hostname", "formSubmitURL", "httpRealm"]) {
|
|
if (login[field] != "") {
|
|
matchData[field] = login[field];
|
|
}
|
|
}
|
|
let [logins, ids] = this._searchLogins(matchData);
|
|
|
|
let id = null;
|
|
let foundLogin = null;
|
|
|
|
// 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 (let i = 0; i < logins.length; i++) {
|
|
let [decryptedLogin] = this._decryptLogins([logins[i]]);
|
|
|
|
if (!decryptedLogin || !decryptedLogin.equals(login)) {
|
|
continue;
|
|
}
|
|
|
|
// We've found a match, set id and break
|
|
foundLogin = decryptedLogin;
|
|
id = ids[i];
|
|
break;
|
|
}
|
|
|
|
return [id, foundLogin];
|
|
},
|
|
|
|
/**
|
|
* Checks to see if the specified GUID already exists.
|
|
*/
|
|
_isGuidUnique(guid) {
|
|
this._store.ensureDataReady();
|
|
|
|
return this._store.data.logins.every(l => l.guid != guid);
|
|
},
|
|
|
|
/**
|
|
* Returns the encrypted username, password, and encrypton type for the specified
|
|
* login. Can throw if the user cancels a master password entry.
|
|
*/
|
|
_encryptLogin(login) {
|
|
let encUsername = this._crypto.encrypt(login.username);
|
|
let encPassword = this._crypto.encrypt(login.password);
|
|
let encType = this._crypto.defaultEncType;
|
|
|
|
return [encUsername, encPassword, encType];
|
|
},
|
|
|
|
/**
|
|
* Decrypts username and password fields in the provided array of
|
|
* logins.
|
|
*
|
|
* 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(logins) {
|
|
let result = [];
|
|
|
|
for (let login of logins) {
|
|
try {
|
|
login.username = this._crypto.decrypt(login.username);
|
|
login.password = this._crypto.decrypt(login.password);
|
|
} catch (e) {
|
|
// If decryption failed (corrupt entry?), just skip it.
|
|
// Rethrow other errors (like canceling entry of a master pw)
|
|
if (e.result == Cr.NS_ERROR_FAILURE) {
|
|
continue;
|
|
}
|
|
throw e;
|
|
}
|
|
result.push(login);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_json.prototype, "log", () => {
|
|
let logger = LoginHelper.createLogger("Login storage");
|
|
return logger.log.bind(logger);
|
|
});
|
|
|
|
var EXPORTED_SYMBOLS = ["LoginManagerStorage_json"];
|