mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 14:22:01 +00:00
Bug 853549 - Use a JSON storage back-end in the Login Manager, except on Android. r=dolske
This commit is contained in:
parent
f227a9ce99
commit
e4ede54cb5
@ -388,7 +388,7 @@
|
|||||||
#endif
|
#endif
|
||||||
@BINPATH@/components/SiteSpecificUserAgent.js
|
@BINPATH@/components/SiteSpecificUserAgent.js
|
||||||
@BINPATH@/components/SiteSpecificUserAgent.manifest
|
@BINPATH@/components/SiteSpecificUserAgent.manifest
|
||||||
@BINPATH@/components/storage-mozStorage.js
|
@BINPATH@/components/storage-json.js
|
||||||
@BINPATH@/components/crypto-SDR.js
|
@BINPATH@/components/crypto-SDR.js
|
||||||
@BINPATH@/components/jsconsole-clhandler.manifest
|
@BINPATH@/components/jsconsole-clhandler.manifest
|
||||||
@BINPATH@/components/jsconsole-clhandler.js
|
@BINPATH@/components/jsconsole-clhandler.js
|
||||||
|
@ -1128,6 +1128,18 @@ var gBrowserInit = {
|
|||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
// Load the Login Manager data from disk off the main thread, some time
|
||||||
|
// after startup. If the data is required before the timeout, for example
|
||||||
|
// because a restored page contains a password field, it will be loaded on
|
||||||
|
// the main thread, and this initialization request will be ignored.
|
||||||
|
setTimeout(function() {
|
||||||
|
try {
|
||||||
|
Services.logins;
|
||||||
|
} catch (ex) {
|
||||||
|
Cu.reportError(ex);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
// The object handling the downloads indicator is also initialized here in the
|
// The object handling the downloads indicator is also initialized here in the
|
||||||
// delayed startup function, but the actual indicator element is not loaded
|
// delayed startup function, but the actual indicator element is not loaded
|
||||||
// unless there are downloads to be displayed.
|
// unless there are downloads to be displayed.
|
||||||
|
@ -390,7 +390,7 @@
|
|||||||
@BINPATH@/components/nsLoginInfo.js
|
@BINPATH@/components/nsLoginInfo.js
|
||||||
@BINPATH@/components/nsLoginManager.js
|
@BINPATH@/components/nsLoginManager.js
|
||||||
@BINPATH@/components/nsLoginManagerPrompter.js
|
@BINPATH@/components/nsLoginManagerPrompter.js
|
||||||
@BINPATH@/components/storage-mozStorage.js
|
@BINPATH@/components/storage-json.js
|
||||||
@BINPATH@/components/crypto-SDR.js
|
@BINPATH@/components/crypto-SDR.js
|
||||||
@BINPATH@/components/jsconsole-clhandler.manifest
|
@BINPATH@/components/jsconsole-clhandler.manifest
|
||||||
@BINPATH@/components/jsconsole-clhandler.js
|
@BINPATH@/components/jsconsole-clhandler.js
|
||||||
|
@ -113,11 +113,6 @@ function PasswordStore(name, engine) {
|
|||||||
Store.call(this, name, engine);
|
Store.call(this, name, engine);
|
||||||
this._nsLoginInfo = new Components.Constructor(
|
this._nsLoginInfo = new Components.Constructor(
|
||||||
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
|
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "DBConnection", function() {
|
|
||||||
return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
||||||
.getInterface(Ci.mozIStorageConnection);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
PasswordStore.prototype = {
|
PasswordStore.prototype = {
|
||||||
__proto__: Store.prototype,
|
__proto__: Store.prototype,
|
||||||
@ -162,21 +157,6 @@ PasswordStore.prototype = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
applyIncomingBatch: function applyIncomingBatch(records) {
|
|
||||||
if (!this.DBConnection) {
|
|
||||||
return Store.prototype.applyIncomingBatch.call(this, records);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Utils.runInTransaction(this.DBConnection, function() {
|
|
||||||
return Store.prototype.applyIncomingBatch.call(this, records);
|
|
||||||
}, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
applyIncoming: function applyIncoming(record) {
|
|
||||||
Store.prototype.applyIncoming.call(this, record);
|
|
||||||
this._sleep(0); // Yield back to main thread after synchronous operation.
|
|
||||||
},
|
|
||||||
|
|
||||||
getAllIDs: function PasswordStore__getAllIDs() {
|
getAllIDs: function PasswordStore__getAllIDs() {
|
||||||
let items = {};
|
let items = {};
|
||||||
let logins = Services.logins.getAllLogins({});
|
let logins = Services.logins.getAllLogins({});
|
||||||
|
@ -150,22 +150,6 @@ this.Utils = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
runInTransaction: function(db, callback, thisObj) {
|
|
||||||
let hasTransaction = false;
|
|
||||||
try {
|
|
||||||
db.beginTransaction();
|
|
||||||
hasTransaction = true;
|
|
||||||
} catch(e) { /* om nom nom exceptions */ }
|
|
||||||
|
|
||||||
try {
|
|
||||||
return callback.call(thisObj);
|
|
||||||
} finally {
|
|
||||||
if (hasTransaction) {
|
|
||||||
db.commitTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
|
* GUIDs are 9 random bytes encoded with base64url (RFC 4648).
|
||||||
* That makes them 12 characters long with 72 bits of entropy.
|
* That makes them 12 characters long with 72 bits of entropy.
|
||||||
|
233
toolkit/components/passwordmgr/LoginHelper.jsm
Normal file
233
toolkit/components/passwordmgr/LoginHelper.jsm
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* 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;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// LoginHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functions shared by different Login Manager components.
|
||||||
|
*/
|
||||||
|
this.LoginHelper = {
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
throw "Invalid hostname";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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")) {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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:
|
||||||
|
throw "Unexpected propertybag item: " + prop.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw "newLoginData needs an expected interface!";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check the login
|
||||||
|
if (newLogin.hostname == null || newLogin.hostname.length == 0) {
|
||||||
|
throw "Can't add a login with a null or empty hostname.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For logins w/o a username, set to "", not null.
|
||||||
|
if (newLogin.username == null) {
|
||||||
|
throw "Can't add a login with a null username.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLogin.password == null || newLogin.password.length == 0) {
|
||||||
|
throw "Can't add a login with a null or empty password.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
||||||
|
// We have a form submit URL. Can't have a HTTP realm.
|
||||||
|
if (newLogin.httpRealm != null) {
|
||||||
|
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||||
|
}
|
||||||
|
} else if (newLogin.httpRealm) {
|
||||||
|
// We have a HTTP realm. Can't have a form submit URL.
|
||||||
|
if (newLogin.formSubmitURL != null) {
|
||||||
|
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Need one or the other!
|
||||||
|
throw "Can't add a login without a httpRealm or formSubmitURL.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throws if there are bogus values.
|
||||||
|
this.checkLoginValues(newLogin);
|
||||||
|
|
||||||
|
return newLogin;
|
||||||
|
},
|
||||||
|
};
|
180
toolkit/components/passwordmgr/LoginImport.jsm
Normal file
180
toolkit/components/passwordmgr/LoginImport.jsm
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an object that has a method to import login-related data from the
|
||||||
|
* previous SQLite storage format.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = [
|
||||||
|
"LoginImport",
|
||||||
|
];
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Globals
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
const Cr = Components.results;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
|
"resource://gre/modules/osfile.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||||
|
"resource://gre/modules/Sqlite.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||||
|
"resource://gre/modules/NetUtil.jsm");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// LoginImport
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an object that has a method to import login-related data from the
|
||||||
|
* previous SQLite storage format.
|
||||||
|
*
|
||||||
|
* @param aStore
|
||||||
|
* LoginStore object where imported data will be added.
|
||||||
|
* @param aPath
|
||||||
|
* String containing the file path of the SQLite login database.
|
||||||
|
*/
|
||||||
|
this.LoginImport = function (aStore, aPath)
|
||||||
|
{
|
||||||
|
this.store = aStore;
|
||||||
|
this.path = aPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.LoginImport.prototype = {
|
||||||
|
/**
|
||||||
|
* LoginStore object where imported data will be added.
|
||||||
|
*/
|
||||||
|
store: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String containing the file path of the SQLite login database.
|
||||||
|
*/
|
||||||
|
path: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports login-related data from the previous SQLite storage format.
|
||||||
|
*/
|
||||||
|
import: Task.async(function* () {
|
||||||
|
// We currently migrate data directly from the database to the JSON store at
|
||||||
|
// first run, then we set a preference to prevent repeating the import.
|
||||||
|
// Thus, merging with existing data is not a use case we support. This
|
||||||
|
// restriction might be removed to support re-importing passwords set by an
|
||||||
|
// old version by flipping the import preference and restarting.
|
||||||
|
if (this.store.data.logins.length > 0 ||
|
||||||
|
this.store.data.disabledHosts.length > 0) {
|
||||||
|
throw new Error("Unable to import saved passwords because some data " +
|
||||||
|
"has already been imported or saved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// When a timestamp is not specified, we will use the same reference time.
|
||||||
|
let referenceTimeMs = Date.now();
|
||||||
|
|
||||||
|
let connection = yield Sqlite.openConnection({ path: this.path });
|
||||||
|
try {
|
||||||
|
let schemaVersion = yield connection.getSchemaVersion();
|
||||||
|
|
||||||
|
// We support importing database schema versions from 3 onwards.
|
||||||
|
// Version 3 was implemented in bug 316084 (Firefox 3.6, March 2009).
|
||||||
|
// Version 4 was implemented in bug 465636 (Firefox 4, March 2010).
|
||||||
|
// Version 5 was implemented in bug 718817 (Firefox 13, February 2012).
|
||||||
|
if (schemaVersion < 3) {
|
||||||
|
throw new Error("Unable to import saved passwords because " +
|
||||||
|
"the existing profile is too old.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = yield connection.execute("SELECT * FROM moz_logins");
|
||||||
|
for (let row of rows) {
|
||||||
|
try {
|
||||||
|
let hostname = row.getResultByName("hostname");
|
||||||
|
let httpRealm = row.getResultByName("httpRealm");
|
||||||
|
let formSubmitURL = row.getResultByName("formSubmitURL");
|
||||||
|
let usernameField = row.getResultByName("usernameField");
|
||||||
|
let passwordField = row.getResultByName("passwordField");
|
||||||
|
let encryptedUsername = row.getResultByName("encryptedUsername");
|
||||||
|
let encryptedPassword = row.getResultByName("encryptedPassword");
|
||||||
|
|
||||||
|
// The "guid" field was introduced in schema version 2, and the
|
||||||
|
// "enctype" field was introduced in schema version 3. We don't
|
||||||
|
// support upgrading from older versions of the database.
|
||||||
|
let guid = row.getResultByName("guid");
|
||||||
|
let encType = row.getResultByName("encType");
|
||||||
|
|
||||||
|
// The time and count fields were introduced in schema version 4.
|
||||||
|
let timeCreated = null;
|
||||||
|
let timeLastUsed = null;
|
||||||
|
let timePasswordChanged = null;
|
||||||
|
let timesUsed = null;
|
||||||
|
try {
|
||||||
|
timeCreated = row.getResultByName("timeCreated");
|
||||||
|
timeLastUsed = row.getResultByName("timeLastUsed");
|
||||||
|
timePasswordChanged = row.getResultByName("timePasswordChanged");
|
||||||
|
timesUsed = row.getResultByName("timesUsed");
|
||||||
|
} catch (ex) { }
|
||||||
|
|
||||||
|
// These columns may be null either because they were not present in
|
||||||
|
// the database or because the record was created on a new schema
|
||||||
|
// version by an old application version.
|
||||||
|
if (!timeCreated) {
|
||||||
|
timeCreated = referenceTimeMs;
|
||||||
|
}
|
||||||
|
if (!timeLastUsed) {
|
||||||
|
timeLastUsed = referenceTimeMs;
|
||||||
|
}
|
||||||
|
if (!timePasswordChanged) {
|
||||||
|
timePasswordChanged = referenceTimeMs;
|
||||||
|
}
|
||||||
|
if (!timesUsed) {
|
||||||
|
timesUsed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.data.logins.push({
|
||||||
|
id: this.store.data.nextId++,
|
||||||
|
hostname: hostname,
|
||||||
|
httpRealm: httpRealm,
|
||||||
|
formSubmitURL: formSubmitURL,
|
||||||
|
usernameField: usernameField,
|
||||||
|
passwordField: passwordField,
|
||||||
|
encryptedUsername: encryptedUsername,
|
||||||
|
encryptedPassword: encryptedPassword,
|
||||||
|
guid: guid,
|
||||||
|
encType: encType,
|
||||||
|
timeCreated: timeCreated,
|
||||||
|
timeLastUsed: timeLastUsed,
|
||||||
|
timePasswordChanged: timePasswordChanged,
|
||||||
|
timesUsed: timesUsed,
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
Cu.reportError("Error importing login: " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = yield connection.execute("SELECT * FROM moz_disabledHosts");
|
||||||
|
for (let row of rows) {
|
||||||
|
try {
|
||||||
|
let id = row.getResultByName("id");
|
||||||
|
let hostname = row.getResultByName("hostname");
|
||||||
|
|
||||||
|
this.store.data.disabledHosts.push({
|
||||||
|
id: this.store.data.nextId++,
|
||||||
|
hostname: hostname,
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
Cu.reportError("Error importing disabled host: " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
yield connection.close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
301
toolkit/components/passwordmgr/LoginStore.jsm
Normal file
301
toolkit/components/passwordmgr/LoginStore.jsm
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles serialization of login-related data and persistence into a file.
|
||||||
|
*
|
||||||
|
* This modules handles the raw data stored in JavaScript serializable objects,
|
||||||
|
* and contains no special validation or query logic, that is handled entirely
|
||||||
|
* by "storage.js" instead.
|
||||||
|
*
|
||||||
|
* The data can be manipulated only after it has been loaded from disk. The
|
||||||
|
* load process can happen asynchronously, through the "load" method, or
|
||||||
|
* synchronously, through "ensureDataReady". After any modification, the
|
||||||
|
* "saveSoon" method must be called to flush the data to disk asynchronously.
|
||||||
|
*
|
||||||
|
* The raw data should be manipulated synchronously, without waiting for the
|
||||||
|
* event loop or for promise resolution, so that the saved file is always
|
||||||
|
* consistent. This synchronous approach also simplifies the query and update
|
||||||
|
* logic. For example, it is possible to find an object and modify it
|
||||||
|
* immediately without caring whether other code modifies it in the meantime.
|
||||||
|
*
|
||||||
|
* An asynchronous shutdown observer makes sure that data is always saved before
|
||||||
|
* the browser is closed. The data cannot be modified during shutdown.
|
||||||
|
*
|
||||||
|
* The file is stored in JSON format, without indentation, using UTF-8 encoding.
|
||||||
|
* With indentation applied, the file would look like this:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* "logins": [
|
||||||
|
* {
|
||||||
|
* "id": 2,
|
||||||
|
* "hostname": "http://www.example.com",
|
||||||
|
* "httpRealm": null,
|
||||||
|
* "formSubmitURL": "http://www.example.com/submit-url",
|
||||||
|
* "usernameField": "username_field",
|
||||||
|
* "passwordField": "password_field",
|
||||||
|
* "encryptedUsername": "...",
|
||||||
|
* "encryptedPassword": "...",
|
||||||
|
* "guid": "...",
|
||||||
|
* "encType": 1,
|
||||||
|
* "timeCreated": 1262304000000,
|
||||||
|
* "timeLastUsed": 1262304000000,
|
||||||
|
* "timePasswordChanged": 1262476800000,
|
||||||
|
* "timesUsed": 1
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "id": 4,
|
||||||
|
* (...)
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* "disabledHosts": [
|
||||||
|
* "http://www.example.org",
|
||||||
|
* "http://www.example.net"
|
||||||
|
* ],
|
||||||
|
* "nextId": 10,
|
||||||
|
* "version": 1
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = [
|
||||||
|
"LoginStore",
|
||||||
|
];
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Globals
|
||||||
|
|
||||||
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||||
|
"resource://gre/modules/AsyncShutdown.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
|
||||||
|
"resource://gre/modules/DeferredTask.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||||
|
"resource://gre/modules/FileUtils.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
|
"resource://gre/modules/osfile.jsm")
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
|
||||||
|
return new TextDecoder();
|
||||||
|
});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
|
||||||
|
return new TextEncoder();
|
||||||
|
});
|
||||||
|
|
||||||
|
const FileInputStream =
|
||||||
|
Components.Constructor("@mozilla.org/network/file-input-stream;1",
|
||||||
|
"nsIFileInputStream", "init");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delay between a change to the login data and the related save operation.
|
||||||
|
*/
|
||||||
|
const kSaveDelayMs = 1500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current data version assigned by the code that last touched the data.
|
||||||
|
*
|
||||||
|
* This number should be updated only when it is important to understand whether
|
||||||
|
* an old version of the code has touched the data, for example to execute an
|
||||||
|
* update logic. In most cases, this number should not be changed, in
|
||||||
|
* particular when no special one-time update logic is needed.
|
||||||
|
*
|
||||||
|
* For example, this number should NOT be changed when a new optional field is
|
||||||
|
* added to a login entry.
|
||||||
|
*/
|
||||||
|
const kDataVersion = 1;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// LoginStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles serialization of login-related data and persistence into a file.
|
||||||
|
*
|
||||||
|
* @param aPath
|
||||||
|
* String containing the file path where data should be saved.
|
||||||
|
*/
|
||||||
|
function LoginStore(aPath)
|
||||||
|
{
|
||||||
|
this.path = aPath;
|
||||||
|
|
||||||
|
this._saver = new DeferredTask(() => this.save(), kSaveDelayMs);
|
||||||
|
AsyncShutdown.profileBeforeChange.addBlocker("Login store: writing data",
|
||||||
|
() => this._saver.finalize());
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginStore.prototype = {
|
||||||
|
/**
|
||||||
|
* String containing the file path where data should be saved.
|
||||||
|
*/
|
||||||
|
path: "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializable object containing the login-related data. This is populated
|
||||||
|
* directly with the data loaded from the file, and is saved without
|
||||||
|
* modifications.
|
||||||
|
*
|
||||||
|
* This contains one property for each list.
|
||||||
|
*/
|
||||||
|
data: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when data has been loaded.
|
||||||
|
*/
|
||||||
|
dataReady: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads persistent data from the file to memory.
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
* @resolves When the operation finished successfully.
|
||||||
|
* @rejects JavaScript exception.
|
||||||
|
*/
|
||||||
|
load: function ()
|
||||||
|
{
|
||||||
|
return Task.spawn(function () {
|
||||||
|
try {
|
||||||
|
let bytes = yield OS.File.read(this.path);
|
||||||
|
|
||||||
|
// If synchronous loading happened in the meantime, exit now.
|
||||||
|
if (this.dataReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data = JSON.parse(gTextDecoder.decode(bytes));
|
||||||
|
} catch (ex) {
|
||||||
|
// If an exception occurred because the file did not exist, we should
|
||||||
|
// just start with new data. Other errors may indicate that the file is
|
||||||
|
// corrupt, thus we move it to a backup location before allowing it to
|
||||||
|
// be overwritten by an empty file.
|
||||||
|
if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
|
||||||
|
Cu.reportError(ex);
|
||||||
|
|
||||||
|
// Move the original file to a backup location, ignoring errors.
|
||||||
|
try {
|
||||||
|
let openInfo = yield OS.File.openUnique(this.path + ".corrupt",
|
||||||
|
{ humanReadable: true });
|
||||||
|
yield openInfo.file.close();
|
||||||
|
yield OS.File.move(this.path, openInfo.path);
|
||||||
|
} catch (e2) {
|
||||||
|
Cu.reportError(e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any case, initialize a new object to host the data.
|
||||||
|
this.data = {
|
||||||
|
nextId: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._processLoadedData();
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads persistent data from the file to memory, synchronously.
|
||||||
|
*/
|
||||||
|
ensureDataReady: function ()
|
||||||
|
{
|
||||||
|
if (this.dataReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// This reads the file and automatically detects the UTF-8 encoding.
|
||||||
|
let inputStream = new FileInputStream(new FileUtils.File(this.path),
|
||||||
|
FileUtils.MODE_RDONLY,
|
||||||
|
FileUtils.PERMS_FILE, 0)
|
||||||
|
try {
|
||||||
|
let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
|
||||||
|
this.data = json.decodeFromStream(inputStream,
|
||||||
|
inputStream.available());
|
||||||
|
} finally {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
// If an exception occurred because the file did not exist, we should just
|
||||||
|
// start with new data. Other errors may indicate that the file is
|
||||||
|
// corrupt, thus we move it to a backup location before allowing it to be
|
||||||
|
// overwritten by an empty file.
|
||||||
|
if (!(ex instanceof Components.Exception &&
|
||||||
|
ex.result == Cr.NS_ERROR_FILE_NOT_FOUND)) {
|
||||||
|
Cu.reportError(ex);
|
||||||
|
// Move the original file to a backup location, ignoring errors.
|
||||||
|
try {
|
||||||
|
let originalFile = new FileUtils.File(this.path);
|
||||||
|
let backupFile = originalFile.clone();
|
||||||
|
backupFile.leafName += ".corrupt";
|
||||||
|
backupFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE,
|
||||||
|
FileUtils.PERMS_FILE);
|
||||||
|
backupFile.remove(false);
|
||||||
|
originalFile.moveTo(backupFile.parent, backupFile.leafName);
|
||||||
|
} catch (e2) {
|
||||||
|
Cu.reportError(e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any case, initialize a new object to host the data.
|
||||||
|
this.data = {
|
||||||
|
nextId: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._processLoadedData();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously work on the data just loaded into memory.
|
||||||
|
*/
|
||||||
|
_processLoadedData: function ()
|
||||||
|
{
|
||||||
|
// Create any arrays that are not present in the saved file.
|
||||||
|
if (!this.data.logins) {
|
||||||
|
this.data.logins = [];
|
||||||
|
}
|
||||||
|
if (!this.data.disabledHosts) {
|
||||||
|
this.data.disabledHosts = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicate that the current version of the code has touched the file.
|
||||||
|
this.data.version = kDataVersion;
|
||||||
|
|
||||||
|
this.dataReady = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the data changed, this triggers asynchronous serialization.
|
||||||
|
*/
|
||||||
|
saveSoon: function () this._saver.arm(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeferredTask that handles the save operation.
|
||||||
|
*/
|
||||||
|
_saver: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves persistent data from memory to the file.
|
||||||
|
*
|
||||||
|
* If an error occurs, the previous file is not deleted.
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
* @resolves When the operation finished successfully.
|
||||||
|
* @rejects JavaScript exception.
|
||||||
|
*/
|
||||||
|
save: function ()
|
||||||
|
{
|
||||||
|
return Task.spawn(function () {
|
||||||
|
// Create or overwrite the file.
|
||||||
|
let bytes = gTextEncoder.encode(JSON.stringify(this.data));
|
||||||
|
yield OS.File.writeAtomic(this.path, bytes,
|
||||||
|
{ tmpPath: this.path + ".tmp" });
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
};
|
@ -23,18 +23,31 @@ XPIDL_MODULE = 'loginmgr'
|
|||||||
EXTRA_COMPONENTS += [
|
EXTRA_COMPONENTS += [
|
||||||
'crypto-SDR.js',
|
'crypto-SDR.js',
|
||||||
'nsLoginInfo.js',
|
'nsLoginInfo.js',
|
||||||
'nsLoginManager.js',
|
|
||||||
'nsLoginManagerPrompter.js',
|
'nsLoginManagerPrompter.js',
|
||||||
'passwordmgr.manifest',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTRA_PP_COMPONENTS += [
|
EXTRA_PP_COMPONENTS += [
|
||||||
'storage-mozStorage.js',
|
'nsLoginManager.js',
|
||||||
|
'passwordmgr.manifest',
|
||||||
]
|
]
|
||||||
|
|
||||||
EXTRA_JS_MODULES += [
|
EXTRA_JS_MODULES += [
|
||||||
'InsecurePasswordUtils.jsm',
|
'InsecurePasswordUtils.jsm',
|
||||||
|
'LoginHelper.jsm',
|
||||||
'LoginManagerContent.jsm',
|
'LoginManagerContent.jsm',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if CONFIG['OS_TARGET'] == 'Android':
|
||||||
|
EXTRA_COMPONENTS += [
|
||||||
|
'storage-mozStorage.js',
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
EXTRA_COMPONENTS += [
|
||||||
|
'storage-json.js',
|
||||||
|
]
|
||||||
|
EXTRA_JS_MODULES += [
|
||||||
|
'LoginImport.jsm',
|
||||||
|
'LoginStore.jsm',
|
||||||
|
]
|
||||||
|
|
||||||
JAR_MANIFESTS += ['jar.mn']
|
JAR_MANIFESTS += ['jar.mn']
|
||||||
|
@ -125,7 +125,11 @@ LoginManager.prototype = {
|
|||||||
|
|
||||||
|
|
||||||
_initStorage : function () {
|
_initStorage : function () {
|
||||||
|
#ifdef ANDROID
|
||||||
var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
|
var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
|
||||||
|
#else
|
||||||
|
var contractID = "@mozilla.org/login-manager/storage/json;1";
|
||||||
|
#endif
|
||||||
try {
|
try {
|
||||||
var catMan = Cc["@mozilla.org/categorymanager;1"].
|
var catMan = Cc["@mozilla.org/categorymanager;1"].
|
||||||
getService(Ci.nsICategoryManager);
|
getService(Ci.nsICategoryManager);
|
||||||
|
@ -6,7 +6,12 @@ component {8aa66d77-1bbb-45a6-991e-b8f47751c291} nsLoginManagerPrompter.js
|
|||||||
contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
|
contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
|
||||||
component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
|
component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
|
||||||
contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
|
contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
|
||||||
|
#ifdef ANDROID
|
||||||
component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
|
component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
|
||||||
contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
|
contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
|
||||||
|
#else
|
||||||
|
component {c00c432d-a0c9-46d7-bef6-9c45b4d07341} storage-json.js
|
||||||
|
contract @mozilla.org/login-manager/storage/json;1 {c00c432d-a0c9-46d7-bef6-9c45b4d07341}
|
||||||
|
#endif
|
||||||
component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
|
component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
|
||||||
contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}
|
contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}
|
||||||
|
654
toolkit/components/passwordmgr/storage-json.js
Normal file
654
toolkit/components/passwordmgr/storage-json.js
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
||||||
|
/* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */
|
||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Globals
|
||||||
|
|
||||||
|
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||||
|
"resource://gre/modules/LoginHelper.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
|
||||||
|
"resource://gre/modules/LoginImport.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||||
|
"resource://gre/modules/LoginStore.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
|
"resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||||
|
"@mozilla.org/uuid-generator;1",
|
||||||
|
"nsIUUIDGenerator");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// LoginManagerStorage_json
|
||||||
|
|
||||||
|
this.LoginManagerStorage_json = function () {}
|
||||||
|
|
||||||
|
this.LoginManagerStorage_json.prototype = {
|
||||||
|
classID : Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
|
||||||
|
QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
|
||||||
|
|
||||||
|
__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;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* log
|
||||||
|
*
|
||||||
|
* Internal function for logging debug messages to the Error Console.
|
||||||
|
*/
|
||||||
|
log : function (message) {
|
||||||
|
if (!this._debug)
|
||||||
|
return;
|
||||||
|
dump("PwMgr json: " + message + "\n");
|
||||||
|
Services.console.logStringMessage("PwMgr json: " + message);
|
||||||
|
},
|
||||||
|
_debug : false,
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* initialize
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
initialize : function () {
|
||||||
|
this._debug = Services.prefs.getBoolPref("signon.debug");
|
||||||
|
|
||||||
|
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 Task.spawn(function () {
|
||||||
|
// Load the data asynchronously.
|
||||||
|
this.log("Opening database at " + this._store.path);
|
||||||
|
yield 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 (yield 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.
|
||||||
|
yield loginImport.import().catch(Cu.reportError);
|
||||||
|
this._store.saveSoon();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We won't attempt import again on next startup.
|
||||||
|
Services.prefs.setBoolPref("signon.importedFromSqlite", true);
|
||||||
|
}.bind(this)).catch(Cu.reportError);
|
||||||
|
} catch (e) {
|
||||||
|
this.log("Initialization failed: " + e);
|
||||||
|
throw "Initialization failed";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* terminate
|
||||||
|
*
|
||||||
|
* Internal method used by regression tests only. It is called before
|
||||||
|
* replacing this storage module with a new instance.
|
||||||
|
*/
|
||||||
|
terminate : function () {
|
||||||
|
this._store._saver.disarm();
|
||||||
|
return this._store.save();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* addLogin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
addLogin : function (login) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
let encUsername, encPassword;
|
||||||
|
|
||||||
|
// Throws if there are bogus values.
|
||||||
|
LoginHelper.checkLoginValues(login);
|
||||||
|
|
||||||
|
[encUsername, encPassword, encType] = 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) {
|
||||||
|
if (!this._isGuidUnique(loginClone.guid))
|
||||||
|
throw "specified GUID already exists";
|
||||||
|
} 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: encType,
|
||||||
|
timeCreated: loginClone.timeCreated,
|
||||||
|
timeLastUsed: loginClone.timeLastUsed,
|
||||||
|
timePasswordChanged: loginClone.timePasswordChanged,
|
||||||
|
timesUsed: loginClone.timesUsed
|
||||||
|
});
|
||||||
|
this._store.saveSoon();
|
||||||
|
|
||||||
|
// Send a notification that a login was added.
|
||||||
|
this._sendNotification("addLogin", loginClone);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* removeLogin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
removeLogin : function (login) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
let [idToDelete, storedLogin] = this._getIdForLogin(login);
|
||||||
|
if (!idToDelete)
|
||||||
|
throw "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();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendNotification("removeLogin", storedLogin);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* modifyLogin
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
modifyLogin : function (oldLogin, newLoginData) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
||||||
|
if (!idToModify)
|
||||||
|
throw "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 "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 "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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getAllLogins
|
||||||
|
*
|
||||||
|
* Returns an array of nsILoginInfo.
|
||||||
|
*/
|
||||||
|
getAllLogins : function (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;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* searchLogins
|
||||||
|
*
|
||||||
|
* Public wrapper around _searchLogins to convert the nsIPropertyBag to a
|
||||||
|
* JavaScript object and decrypt the results.
|
||||||
|
*
|
||||||
|
* Returns an array of decrypted nsILoginInfo.
|
||||||
|
*/
|
||||||
|
searchLogins : function(count, matchData) {
|
||||||
|
let realMatchData = {};
|
||||||
|
// Convert nsIPropertyBag to normal JS object
|
||||||
|
let propEnum = matchData.enumerator;
|
||||||
|
while (propEnum.hasMoreElements()) {
|
||||||
|
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
||||||
|
realMatchData[prop.name] = prop.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [logins, ids] = this._searchLogins(realMatchData);
|
||||||
|
|
||||||
|
// Decrypt entries found for the caller.
|
||||||
|
logins = this._decryptLogins(logins);
|
||||||
|
|
||||||
|
count.value = logins.length; // needed for XPCOM
|
||||||
|
return logins;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _searchLogins
|
||||||
|
*
|
||||||
|
* 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 : function (matchData) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
let conditions = [];
|
||||||
|
|
||||||
|
function match(aLogin) {
|
||||||
|
for (let field in matchData) {
|
||||||
|
let value = matchData[field];
|
||||||
|
switch (field) {
|
||||||
|
// Historical compatibility requires this special case
|
||||||
|
case "formSubmitURL":
|
||||||
|
if (value != null) {
|
||||||
|
if (aLogin.formSubmitURL != "" && aLogin.formSubmitURL != value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Normal cases.
|
||||||
|
case "hostname":
|
||||||
|
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 (value == null && aLogin[field]) {
|
||||||
|
return false;
|
||||||
|
} else if (aLogin[field] != value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Fail if caller requests an unknown property.
|
||||||
|
default:
|
||||||
|
throw "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");
|
||||||
|
return [foundLogins, foundIds];
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* removeAllLogins
|
||||||
|
*
|
||||||
|
* Removes all logins from storage.
|
||||||
|
*
|
||||||
|
* Disabled hosts are kept, as one presumably doesn't want to erase those.
|
||||||
|
*/
|
||||||
|
removeAllLogins : function () {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
this.log("Removing all logins");
|
||||||
|
this._store.data.logins = [];
|
||||||
|
this._store.saveSoon();
|
||||||
|
|
||||||
|
this._sendNotification("removeAllLogins", null);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getAllDisabledHosts
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getAllDisabledHosts : function (count) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
let disabledHosts = this._store.data.disabledHosts.slice(0);
|
||||||
|
|
||||||
|
this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
|
||||||
|
if (count)
|
||||||
|
count.value = disabledHosts.length; // needed for XPCOM
|
||||||
|
return disabledHosts;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* getLoginSavingEnabled
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getLoginSavingEnabled : function (hostname) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
this.log("Getting login saving is enabled for " + hostname);
|
||||||
|
return this._store.data.disabledHosts.indexOf(hostname) == -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* setLoginSavingEnabled
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
setLoginSavingEnabled : function (hostname, enabled) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
// Throws if there are bogus values.
|
||||||
|
LoginHelper.checkHostnameValue(hostname);
|
||||||
|
|
||||||
|
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
||||||
|
let foundIndex = this._store.data.disabledHosts.indexOf(hostname);
|
||||||
|
if (enabled) {
|
||||||
|
if (foundIndex != -1) {
|
||||||
|
this._store.data.disabledHosts.splice(foundIndex, 1);
|
||||||
|
this._store.saveSoon();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (foundIndex == -1) {
|
||||||
|
this._store.data.disabledHosts.push(hostname);
|
||||||
|
this._store.saveSoon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* findLogins
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
findLogins : function (count, hostname, formSubmitURL, httpRealm) {
|
||||||
|
let loginData = {
|
||||||
|
hostname: hostname,
|
||||||
|
formSubmitURL: formSubmitURL,
|
||||||
|
httpRealm: httpRealm
|
||||||
|
};
|
||||||
|
let matchData = { };
|
||||||
|
for each (let field in ["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
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
countLogins : function (hostname, formSubmitURL, httpRealm) {
|
||||||
|
let count = {};
|
||||||
|
let loginData = {
|
||||||
|
hostname: hostname,
|
||||||
|
formSubmitURL: formSubmitURL,
|
||||||
|
httpRealm: httpRealm
|
||||||
|
};
|
||||||
|
let matchData = { };
|
||||||
|
for each (let field in ["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;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* uiBusy
|
||||||
|
*/
|
||||||
|
get uiBusy() {
|
||||||
|
return this._crypto.uiBusy;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* isLoggedIn
|
||||||
|
*/
|
||||||
|
get isLoggedIn() {
|
||||||
|
return this._crypto.isLoggedIn;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _sendNotification
|
||||||
|
*
|
||||||
|
* Send a notification when stored data is changed.
|
||||||
|
*/
|
||||||
|
_sendNotification : function (changeType, data) {
|
||||||
|
let dataObject = data;
|
||||||
|
// Can't pass a raw JS string or array though notifyObservers(). :-(
|
||||||
|
if (data instanceof Array) {
|
||||||
|
dataObject = Cc["@mozilla.org/array;1"].
|
||||||
|
createInstance(Ci.nsIMutableArray);
|
||||||
|
for (let i = 0; i < data.length; i++)
|
||||||
|
dataObject.appendElement(data[i], false);
|
||||||
|
} else if (typeof(data) == "string") {
|
||||||
|
dataObject = Cc["@mozilla.org/supports-string;1"].
|
||||||
|
createInstance(Ci.nsISupportsString);
|
||||||
|
dataObject.data = data;
|
||||||
|
}
|
||||||
|
Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _getIdForLogin
|
||||||
|
*
|
||||||
|
* 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 : function (login) {
|
||||||
|
let matchData = { };
|
||||||
|
for each (let field in ["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];
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _isGuidUnique
|
||||||
|
*
|
||||||
|
* Checks to see if the specified GUID already exists.
|
||||||
|
*/
|
||||||
|
_isGuidUnique : function (guid) {
|
||||||
|
this._store.ensureDataReady();
|
||||||
|
|
||||||
|
return this._store.data.logins.every(l => l.guid != guid);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _encryptLogin
|
||||||
|
*
|
||||||
|
* Returns the encrypted username, password, and encrypton type for the specified
|
||||||
|
* login. Can throw if the user cancels a master password entry.
|
||||||
|
*/
|
||||||
|
_encryptLogin : function (login) {
|
||||||
|
let encUsername = this._crypto.encrypt(login.username);
|
||||||
|
let encPassword = this._crypto.encrypt(login.password);
|
||||||
|
let encType = this._crypto.defaultEncType;
|
||||||
|
|
||||||
|
return [encUsername, encPassword, encType];
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* _decryptLogins
|
||||||
|
*
|
||||||
|
* 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 : function (logins) {
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
for each (let login in 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);
|
@ -13,9 +13,10 @@ const DB_VERSION = 5; // The database schema version
|
|||||||
|
|
||||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||||
|
Components.utils.import("resource://gre/modules/Promise.jsm");
|
||||||
|
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||||
"resource://gre/modules/Promise.jsm");
|
"resource://gre/modules/LoginHelper.jsm");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object that manages a database transaction properly so consumers don't have
|
* Object that manages a database transaction properly so consumers don't have
|
||||||
@ -243,7 +244,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
let encUsername, encPassword;
|
let encUsername, encPassword;
|
||||||
|
|
||||||
// Throws if there are bogus values.
|
// Throws if there are bogus values.
|
||||||
this._checkLoginValues(login);
|
LoginHelper.checkLoginValues(login);
|
||||||
|
|
||||||
[encUsername, encPassword, encType] = this._encryptLogin(login);
|
[encUsername, encPassword, encType] = this._encryptLogin(login);
|
||||||
|
|
||||||
@ -355,109 +356,16 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
||||||
if (!idToModify)
|
if (!idToModify)
|
||||||
throw "No matching logins";
|
throw "No matching logins";
|
||||||
oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
||||||
|
|
||||||
let newLogin;
|
let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
|
||||||
if (newLoginData instanceof Ci.nsILoginInfo) {
|
|
||||||
// Clone the existing login to get its nsILoginMetaInfo, then init it
|
|
||||||
// with the replacement nsILoginInfo data from the new login.
|
|
||||||
newLogin = oldStoredLogin.clone();
|
|
||||||
newLogin.init(newLoginData.hostname,
|
|
||||||
newLoginData.formSubmitURL, newLoginData.httpRealm,
|
|
||||||
newLoginData.username, newLoginData.password,
|
|
||||||
newLoginData.usernameField, newLoginData.passwordField);
|
|
||||||
newLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
||||||
|
|
||||||
// Automatically update metainfo when password is changed.
|
// Check if the new GUID is duplicate.
|
||||||
if (newLogin.password != oldLogin.password)
|
if (newLogin.guid != oldStoredLogin.guid &&
|
||||||
newLogin.timePasswordChanged = Date.now();
|
!this._isGuidUnique(newLogin.guid))
|
||||||
} else if (newLoginData instanceof Ci.nsIPropertyBag) {
|
{
|
||||||
function _bagHasProperty(aPropName) {
|
throw "specified GUID already exists";
|
||||||
try {
|
|
||||||
newLoginData.getProperty(aPropName);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone the existing login, along with all its properties.
|
|
||||||
newLogin = oldStoredLogin.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 = newLoginData.getProperty("password");
|
|
||||||
if (newPassword != oldLogin.password)
|
|
||||||
newLogin.timePasswordChanged = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
let propEnum = newLoginData.enumerator;
|
|
||||||
while (propEnum.hasMoreElements()) {
|
|
||||||
let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
|
|
||||||
switch (prop.name) {
|
|
||||||
// nsILoginInfo properties...
|
|
||||||
case "hostname":
|
|
||||||
case "httpRealm":
|
|
||||||
case "formSubmitURL":
|
|
||||||
case "username":
|
|
||||||
case "password":
|
|
||||||
case "usernameField":
|
|
||||||
case "passwordField":
|
|
||||||
// nsILoginMetaInfo properties...
|
|
||||||
case "guid":
|
|
||||||
case "timeCreated":
|
|
||||||
case "timeLastUsed":
|
|
||||||
case "timePasswordChanged":
|
|
||||||
case "timesUsed":
|
|
||||||
newLogin[prop.name] = prop.value;
|
|
||||||
if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid))
|
|
||||||
throw "specified GUID already exists";
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Fake property, allows easy incrementing.
|
|
||||||
case "timesUsedIncrement":
|
|
||||||
newLogin.timesUsed += prop.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Fail if caller requests setting an unknown property.
|
|
||||||
default:
|
|
||||||
throw "Unexpected propertybag item: " + prop.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw "newLoginData needs an expected interface!";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check the login
|
|
||||||
if (newLogin.hostname == null || newLogin.hostname.length == 0)
|
|
||||||
throw "Can't add a login with a null or empty hostname.";
|
|
||||||
|
|
||||||
// For logins w/o a username, set to "", not null.
|
|
||||||
if (newLogin.username == null)
|
|
||||||
throw "Can't add a login with a null username.";
|
|
||||||
|
|
||||||
if (newLogin.password == null || newLogin.password.length == 0)
|
|
||||||
throw "Can't add a login with a null or empty password.";
|
|
||||||
|
|
||||||
if (newLogin.formSubmitURL || newLogin.formSubmitURL == "") {
|
|
||||||
// We have a form submit URL. Can't have a HTTP realm.
|
|
||||||
if (newLogin.httpRealm != null)
|
|
||||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
|
||||||
} else if (newLogin.httpRealm) {
|
|
||||||
// We have a HTTP realm. Can't have a form submit URL.
|
|
||||||
if (newLogin.formSubmitURL != null)
|
|
||||||
throw "Can't add a login with both a httpRealm and formSubmitURL.";
|
|
||||||
} else {
|
|
||||||
// Need one or the other!
|
|
||||||
throw "Can't add a login without a httpRealm or formSubmitURL.";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throws if there are bogus values.
|
|
||||||
this._checkLoginValues(newLogin);
|
|
||||||
|
|
||||||
// Look for an existing entry in case key properties changed.
|
// Look for an existing entry in case key properties changed.
|
||||||
if (!newLogin.matches(oldLogin, true)) {
|
if (!newLogin.matches(oldLogin, true)) {
|
||||||
let logins = this.findLogins({}, newLogin.hostname,
|
let logins = this.findLogins({}, newLogin.hostname,
|
||||||
@ -665,7 +573,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
storeDeletedLogin : function(aLogin) {
|
storeDeletedLogin : function(aLogin) {
|
||||||
#ifdef ANDROID
|
|
||||||
let stmt = null;
|
let stmt = null;
|
||||||
try {
|
try {
|
||||||
this.log("Storing " + aLogin.guid + " in deleted passwords\n");
|
this.log("Storing " + aLogin.guid + " in deleted passwords\n");
|
||||||
@ -680,7 +587,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
if (stmt)
|
if (stmt)
|
||||||
stmt.reset();
|
stmt.reset();
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@ -714,7 +620,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._sendNotification("removeAllLogins", null);
|
this._sendNotification("removeAllLogins", null);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -747,7 +653,7 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
*/
|
*/
|
||||||
setLoginSavingEnabled : function (hostname, enabled) {
|
setLoginSavingEnabled : function (hostname, enabled) {
|
||||||
// Throws if there are bogus values.
|
// Throws if there are bogus values.
|
||||||
this._checkHostnameValue(hostname);
|
LoginHelper.checkHostnameValue(hostname);
|
||||||
|
|
||||||
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
||||||
let query;
|
let query;
|
||||||
@ -788,8 +694,8 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
};
|
};
|
||||||
let matchData = { };
|
let matchData = { };
|
||||||
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
|
||||||
if (loginData[field] != '')
|
if (loginData[field] != '')
|
||||||
matchData[field] = loginData[field];
|
matchData[field] = loginData[field];
|
||||||
let [logins, ids] = this._searchLogins(matchData);
|
let [logins, ids] = this._searchLogins(matchData);
|
||||||
|
|
||||||
// Decrypt entries found for the caller.
|
// Decrypt entries found for the caller.
|
||||||
@ -978,70 +884,6 @@ LoginManagerStorage_mozStorage.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* _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";
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* _checkHostnameValue
|
|
||||||
*
|
|
||||||
* Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
|
|
||||||
* that standard here. Throws on illegal format.
|
|
||||||
*/
|
|
||||||
_checkHostnameValue : function (hostname) {
|
|
||||||
// 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";
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* _isGuidUnique
|
* _isGuidUnique
|
||||||
*
|
*
|
||||||
|
@ -17,12 +17,14 @@ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
|||||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
|
||||||
|
"resource://gre/modules/DownloadPaths.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||||
"resource://gre/modules/FileUtils.jsm");
|
"resource://gre/modules/FileUtils.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
XPCOMUtils.defineLazyModuleGetter(this, "OS",
|
||||||
"resource://gre/modules/osfile.jsm");
|
"resource://gre/modules/osfile.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||||
"resource://gre/modules/commonjs/sdk/core/promise.js");
|
"resource://gre/modules/Promise.jsm");
|
||||||
|
|
||||||
const LoginInfo =
|
const LoginInfo =
|
||||||
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
|
||||||
@ -43,6 +45,46 @@ function run_test()
|
|||||||
// Some of these functions are already implemented in other parts of the source
|
// Some of these functions are already implemented in other parts of the source
|
||||||
// tree, see bug 946708 about sharing more code.
|
// tree, see bug 946708 about sharing more code.
|
||||||
|
|
||||||
|
// While the previous test file should have deleted all the temporary files it
|
||||||
|
// used, on Windows these might still be pending deletion on the physical file
|
||||||
|
// system. Thus, start from a new base number every time, to make a collision
|
||||||
|
// with a file that is still pending deletion highly unlikely.
|
||||||
|
let gFileCounter = Math.floor(Math.random() * 1000000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a reference to a temporary file, that is guaranteed not to exist, and
|
||||||
|
* to have never been created before.
|
||||||
|
*
|
||||||
|
* @param aLeafName
|
||||||
|
* Suggested leaf name for the file to be created.
|
||||||
|
*
|
||||||
|
* @return nsIFile pointing to a non-existent file in a temporary directory.
|
||||||
|
*
|
||||||
|
* @note It is not enough to delete the file if it exists, or to delete the file
|
||||||
|
* after calling nsIFile.createUnique, because on Windows the delete
|
||||||
|
* operation in the file system may still be pending, preventing a new
|
||||||
|
* file with the same name to be created.
|
||||||
|
*/
|
||||||
|
function getTempFile(aLeafName)
|
||||||
|
{
|
||||||
|
// Prepend a serial number to the extension in the suggested leaf name.
|
||||||
|
let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
|
||||||
|
let leafName = base + "-" + gFileCounter + ext;
|
||||||
|
gFileCounter++;
|
||||||
|
|
||||||
|
// Get a file reference under the temporary directory for this test file.
|
||||||
|
let file = FileUtils.getFile("TmpD", [leafName]);
|
||||||
|
do_check_false(file.exists());
|
||||||
|
|
||||||
|
do_register_cleanup(function () {
|
||||||
|
if (file.exists()) {
|
||||||
|
file.remove(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows waiting for an observer notification once.
|
* Allows waiting for an observer notification once.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,248 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* vim: set ts=2 et sw=2 tw=80: */
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the LoginImport object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Globals
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||||
|
"resource://gre/modules/LoginHelper.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
|
||||||
|
"resource://gre/modules/LoginImport.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||||
|
"resource://gre/modules/LoginStore.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
|
||||||
|
"resource://gre/modules/Sqlite.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(this, "gLoginManagerCrypto",
|
||||||
|
"@mozilla.org/login-manager/crypto/SDR;1",
|
||||||
|
"nsILoginManagerCrypto");
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
|
||||||
|
"@mozilla.org/uuid-generator;1",
|
||||||
|
"nsIUUIDGenerator");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates empty login data tables in the given SQLite connection, resembling
|
||||||
|
* the most recent schema version (excluding indices).
|
||||||
|
*/
|
||||||
|
function promiseCreateDatabaseSchema(aConnection)
|
||||||
|
{
|
||||||
|
return Task.spawn(function () {
|
||||||
|
yield aConnection.setSchemaVersion(5);
|
||||||
|
yield aConnection.execute("CREATE TABLE moz_logins (" +
|
||||||
|
"id INTEGER PRIMARY KEY," +
|
||||||
|
"hostname TEXT NOT NULL," +
|
||||||
|
"httpRealm TEXT," +
|
||||||
|
"formSubmitURL TEXT," +
|
||||||
|
"usernameField TEXT NOT NULL," +
|
||||||
|
"passwordField TEXT NOT NULL," +
|
||||||
|
"encryptedUsername TEXT NOT NULL," +
|
||||||
|
"encryptedPassword TEXT NOT NULL," +
|
||||||
|
"guid TEXT," +
|
||||||
|
"encType INTEGER," +
|
||||||
|
"timeCreated INTEGER," +
|
||||||
|
"timeLastUsed INTEGER," +
|
||||||
|
"timePasswordChanged INTEGER," +
|
||||||
|
"timesUsed INTEGER)");
|
||||||
|
yield aConnection.execute("CREATE TABLE moz_disabledHosts (" +
|
||||||
|
"id INTEGER PRIMARY KEY," +
|
||||||
|
"hostname TEXT UNIQUE)");
|
||||||
|
yield aConnection.execute("CREATE TABLE moz_deleted_logins (" +
|
||||||
|
"id INTEGER PRIMARY KEY," +
|
||||||
|
"guid TEXT," +
|
||||||
|
"timeDeleted INTEGER)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new entry in the database resembling the given nsILoginInfo object.
|
||||||
|
*/
|
||||||
|
function promiseInsertLoginInfo(aConnection, aLoginInfo)
|
||||||
|
{
|
||||||
|
aLoginInfo.QueryInterface(Ci.nsILoginMetaInfo);
|
||||||
|
|
||||||
|
// We can't use the aLoginInfo object directly in the execute statement
|
||||||
|
// because the bind code in Sqlite.jsm doesn't allow objects with extra
|
||||||
|
// properties beyond those being binded. So we might as well use an array as
|
||||||
|
// it is simpler.
|
||||||
|
let values = [
|
||||||
|
aLoginInfo.hostname,
|
||||||
|
aLoginInfo.httpRealm,
|
||||||
|
aLoginInfo.formSubmitURL,
|
||||||
|
aLoginInfo.usernameField,
|
||||||
|
aLoginInfo.passwordField,
|
||||||
|
gLoginManagerCrypto.encrypt(aLoginInfo.username),
|
||||||
|
gLoginManagerCrypto.encrypt(aLoginInfo.password),
|
||||||
|
aLoginInfo.guid,
|
||||||
|
aLoginInfo.encType,
|
||||||
|
aLoginInfo.timeCreated,
|
||||||
|
aLoginInfo.timeLastUsed,
|
||||||
|
aLoginInfo.timePasswordChanged,
|
||||||
|
aLoginInfo.timesUsed,
|
||||||
|
];
|
||||||
|
|
||||||
|
return aConnection.execute("INSERT INTO moz_logins (hostname, " +
|
||||||
|
"httpRealm, formSubmitURL, usernameField, " +
|
||||||
|
"passwordField, encryptedUsername, " +
|
||||||
|
"encryptedPassword, guid, encType, timeCreated, " +
|
||||||
|
"timeLastUsed, timePasswordChanged, timesUsed) " +
|
||||||
|
"VALUES (?" + ",?".repeat(12) + ")", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new disabled host entry in the database.
|
||||||
|
*/
|
||||||
|
function promiseInsertDisabledHost(aConnection, aHostname)
|
||||||
|
{
|
||||||
|
return aConnection.execute("INSERT INTO moz_disabledHosts (hostname) " +
|
||||||
|
"VALUES (?)", [aHostname]);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Tests
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports login data from a SQLite file constructed using the test data.
|
||||||
|
*/
|
||||||
|
add_task(function test_import()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile("test-import.json").path);
|
||||||
|
let loginsSqlite = getTempFile("test-logins.sqlite").path;
|
||||||
|
|
||||||
|
// Prepare the logins to be imported, including the nsILoginMetaInfo data.
|
||||||
|
let loginList = TestData.loginList();
|
||||||
|
for (let loginInfo of loginList) {
|
||||||
|
loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
|
||||||
|
loginInfo.guid = gUUIDGenerator.generateUUID().toString();
|
||||||
|
loginInfo.timeCreated = Date.now();
|
||||||
|
loginInfo.timeLastUsed = Date.now();
|
||||||
|
loginInfo.timePasswordChanged = Date.now();
|
||||||
|
loginInfo.timesUsed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and populate the SQLite database first.
|
||||||
|
let connection = yield Sqlite.openConnection({ path: loginsSqlite });
|
||||||
|
try {
|
||||||
|
yield promiseCreateDatabaseSchema(connection);
|
||||||
|
for (let loginInfo of loginList) {
|
||||||
|
yield promiseInsertLoginInfo(connection, loginInfo);
|
||||||
|
}
|
||||||
|
yield promiseInsertDisabledHost(connection, "http://www.example.com");
|
||||||
|
yield promiseInsertDisabledHost(connection, "https://www.example.org");
|
||||||
|
} finally {
|
||||||
|
yield connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "load" method must be called before importing data.
|
||||||
|
yield store.load();
|
||||||
|
yield new LoginImport(store, loginsSqlite).import();
|
||||||
|
|
||||||
|
// Verify that every login in the test data has a matching imported row.
|
||||||
|
do_check_eq(loginList.length, store.data.logins.length);
|
||||||
|
do_check_true(loginList.every(function (loginInfo) {
|
||||||
|
return store.data.logins.some(function (loginDataItem) {
|
||||||
|
let username = gLoginManagerCrypto.decrypt(loginDataItem.encryptedUsername);
|
||||||
|
let password = gLoginManagerCrypto.decrypt(loginDataItem.encryptedPassword);
|
||||||
|
return loginDataItem.hostname == loginInfo.hostname &&
|
||||||
|
loginDataItem.httpRealm == loginInfo.httpRealm &&
|
||||||
|
loginDataItem.formSubmitURL == loginInfo.formSubmitURL &&
|
||||||
|
loginDataItem.usernameField == loginInfo.usernameField &&
|
||||||
|
loginDataItem.passwordField == loginInfo.passwordField &&
|
||||||
|
username == loginInfo.username &&
|
||||||
|
password == loginInfo.password &&
|
||||||
|
loginDataItem.guid == loginInfo.guid &&
|
||||||
|
loginDataItem.encType == loginInfo.encType &&
|
||||||
|
loginDataItem.timeCreated == loginInfo.timeCreated &&
|
||||||
|
loginDataItem.timeLastUsed == loginInfo.timeLastUsed &&
|
||||||
|
loginDataItem.timePasswordChanged == loginInfo.timePasswordChanged &&
|
||||||
|
loginDataItem.timesUsed == loginInfo.timesUsed;
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify that disabled hosts have been imported.
|
||||||
|
do_check_eq(store.data.disabledHosts.length, 2);
|
||||||
|
do_check_true(store.data.disabledHosts.some(
|
||||||
|
dataItem => dataItem.hostname == "http://www.example.com"));
|
||||||
|
do_check_true(store.data.disabledHosts.some(
|
||||||
|
dataItem => dataItem.hostname == "https://www.example.org"));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests imports of NULL values due to a downgraded database.
|
||||||
|
*/
|
||||||
|
add_task(function test_import_downgraded()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile("test-import-downgraded.json").path);
|
||||||
|
let loginsSqlite = getTempFile("test-logins-downgraded.sqlite").path;
|
||||||
|
let loginList = TestData.loginList();
|
||||||
|
|
||||||
|
// Create and populate the SQLite database first.
|
||||||
|
let connection = yield Sqlite.openConnection({ path: loginsSqlite });
|
||||||
|
try {
|
||||||
|
yield promiseCreateDatabaseSchema(connection);
|
||||||
|
yield connection.setSchemaVersion(3);
|
||||||
|
yield promiseInsertLoginInfo(connection, TestData.formLogin({
|
||||||
|
guid: gUUIDGenerator.generateUUID().toString(),
|
||||||
|
timeCreated: null,
|
||||||
|
timeLastUsed: null,
|
||||||
|
timePasswordChanged: null,
|
||||||
|
timesUsed: 0,
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
yield connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "load" method must be called before importing data.
|
||||||
|
yield store.load();
|
||||||
|
yield new LoginImport(store, loginsSqlite).import();
|
||||||
|
|
||||||
|
// Verify that the missing metadata was generated correctly.
|
||||||
|
let loginItem = store.data.logins[0];
|
||||||
|
let creationTime = loginItem.timeCreated;
|
||||||
|
LoginTest.assertTimeIsAboutNow(creationTime);
|
||||||
|
do_check_eq(loginItem.timeLastUsed, creationTime);
|
||||||
|
do_check_eq(loginItem.timePasswordChanged, creationTime);
|
||||||
|
do_check_eq(loginItem.timesUsed, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that importing from a SQLite file with database version 2 fails.
|
||||||
|
*/
|
||||||
|
add_task(function test_import_v2()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile("test-import-v2.json").path);
|
||||||
|
let loginsSqlite = do_get_file("data/signons-v2.sqlite").path;
|
||||||
|
|
||||||
|
// The "load" method must be called before importing data.
|
||||||
|
yield store.load();
|
||||||
|
try {
|
||||||
|
yield new LoginImport(store, loginsSqlite).import();
|
||||||
|
do_throw("The operation should have failed.");
|
||||||
|
} catch (ex) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports login data from a SQLite file, with database version 3.
|
||||||
|
*/
|
||||||
|
add_task(function test_import_v3()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile("test-import-v3.json").path);
|
||||||
|
let loginsSqlite = do_get_file("data/signons-v3.sqlite").path;
|
||||||
|
|
||||||
|
// The "load" method must be called before importing data.
|
||||||
|
yield store.load();
|
||||||
|
yield new LoginImport(store, loginsSqlite).import();
|
||||||
|
|
||||||
|
// We only execute basic integrity checks.
|
||||||
|
do_check_eq(store.data.logins[0].usernameField, "u1");
|
||||||
|
do_check_eq(store.data.disabledHosts.length, 0);
|
||||||
|
});
|
@ -0,0 +1,208 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* vim: set ts=2 et sw=2 tw=80: */
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the LoginStore object.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Globals
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
|
||||||
|
"resource://gre/modules/LoginStore.jsm");
|
||||||
|
|
||||||
|
const TEST_STORE_FILE_NAME = "test-logins.json";
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//// Tests
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves login data to a file, then reloads it.
|
||||||
|
*/
|
||||||
|
add_task(function test_save_reload()
|
||||||
|
{
|
||||||
|
let storeForSave = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||||
|
|
||||||
|
// The "load" method must be called before preparing the data to be saved.
|
||||||
|
yield storeForSave.load();
|
||||||
|
|
||||||
|
let rawLoginData = {
|
||||||
|
id: storeForSave.data.nextId++,
|
||||||
|
hostname: "http://www.example.com",
|
||||||
|
httpRealm: null,
|
||||||
|
formSubmitURL: "http://www.example.com/submit-url",
|
||||||
|
usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
|
||||||
|
passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
|
||||||
|
encryptedUsername: "(test)",
|
||||||
|
encryptedPassword: "(test)",
|
||||||
|
guid: "(test)",
|
||||||
|
encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
|
||||||
|
timeCreated: Date.now(),
|
||||||
|
timeLastUsed: Date.now(),
|
||||||
|
timePasswordChanged: Date.now(),
|
||||||
|
timesUsed: 1,
|
||||||
|
};
|
||||||
|
storeForSave.data.logins.push(rawLoginData);
|
||||||
|
|
||||||
|
storeForSave.data.disabledHosts.push("http://www.example.org");
|
||||||
|
|
||||||
|
yield storeForSave.save();
|
||||||
|
|
||||||
|
// Test the asynchronous initialization path.
|
||||||
|
let storeForLoad = new LoginStore(storeForSave.path);
|
||||||
|
yield storeForLoad.load();
|
||||||
|
|
||||||
|
do_check_eq(storeForLoad.data.logins.length, 1);
|
||||||
|
do_check_matches(storeForLoad.data.logins[0], rawLoginData);
|
||||||
|
do_check_eq(storeForLoad.data.disabledHosts.length, 1);
|
||||||
|
do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
|
||||||
|
|
||||||
|
// Test the synchronous initialization path.
|
||||||
|
storeForLoad = new LoginStore(storeForSave.path);
|
||||||
|
storeForLoad.ensureDataReady();
|
||||||
|
|
||||||
|
do_check_eq(storeForLoad.data.logins.length, 1);
|
||||||
|
do_check_matches(storeForLoad.data.logins[0], rawLoginData);
|
||||||
|
do_check_eq(storeForLoad.data.disabledHosts.length, 1);
|
||||||
|
do_check_eq(storeForLoad.data.disabledHosts[0], "http://www.example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that loading from a missing file results in empty arrays.
|
||||||
|
*/
|
||||||
|
add_task(function test_load_empty()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||||
|
|
||||||
|
do_check_false(yield OS.File.exists(store.path));
|
||||||
|
|
||||||
|
yield store.load();
|
||||||
|
|
||||||
|
do_check_false(yield OS.File.exists(store.path));
|
||||||
|
|
||||||
|
do_check_eq(store.data.logins.length, 0);
|
||||||
|
do_check_eq(store.data.disabledHosts.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that saving empty data still overwrites any existing file.
|
||||||
|
*/
|
||||||
|
add_task(function test_save_empty()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||||
|
|
||||||
|
yield store.load();
|
||||||
|
|
||||||
|
let createdFile = yield OS.File.open(store.path, { create: true });
|
||||||
|
yield createdFile.close();
|
||||||
|
|
||||||
|
yield store.save();
|
||||||
|
|
||||||
|
do_check_true(yield OS.File.exists(store.path));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads data from a string in a predefined format. The purpose of this test is
|
||||||
|
* to verify that the JSON format used in previous versions can be loaded.
|
||||||
|
*/
|
||||||
|
add_task(function test_load_string_predefined()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||||
|
|
||||||
|
let string = "{\"logins\":[{" +
|
||||||
|
"\"id\":1," +
|
||||||
|
"\"hostname\":\"http://www.example.com\"," +
|
||||||
|
"\"httpRealm\":null," +
|
||||||
|
"\"formSubmitURL\":\"http://www.example.com/submit-url\"," +
|
||||||
|
"\"usernameField\":\"usernameField\"," +
|
||||||
|
"\"passwordField\":\"passwordField\"," +
|
||||||
|
"\"encryptedUsername\":\"(test)\"," +
|
||||||
|
"\"encryptedPassword\":\"(test)\"," +
|
||||||
|
"\"guid\":\"(test)\"," +
|
||||||
|
"\"encType\":1," +
|
||||||
|
"\"timeCreated\":1262304000000," +
|
||||||
|
"\"timeLastUsed\":1262390400000," +
|
||||||
|
"\"timePasswordChanged\":1262476800000," +
|
||||||
|
"\"timesUsed\":1}],\"disabledHosts\":[" +
|
||||||
|
"\"http://www.example.org\"]}";
|
||||||
|
|
||||||
|
yield OS.File.writeAtomic(store.path,
|
||||||
|
new TextEncoder().encode(string),
|
||||||
|
{ tmpPath: store.path + ".tmp" });
|
||||||
|
|
||||||
|
yield store.load();
|
||||||
|
|
||||||
|
do_check_eq(store.data.logins.length, 1);
|
||||||
|
do_check_matches(store.data.logins[0], {
|
||||||
|
id: 1,
|
||||||
|
hostname: "http://www.example.com",
|
||||||
|
httpRealm: null,
|
||||||
|
formSubmitURL: "http://www.example.com/submit-url",
|
||||||
|
usernameField: "usernameField",
|
||||||
|
passwordField: "passwordField",
|
||||||
|
encryptedUsername: "(test)",
|
||||||
|
encryptedPassword: "(test)",
|
||||||
|
guid: "(test)",
|
||||||
|
encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
|
||||||
|
timeCreated: 1262304000000,
|
||||||
|
timeLastUsed: 1262390400000,
|
||||||
|
timePasswordChanged: 1262476800000,
|
||||||
|
timesUsed: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
do_check_eq(store.data.disabledHosts.length, 1);
|
||||||
|
do_check_eq(store.data.disabledHosts[0], "http://www.example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads login data from a malformed JSON string.
|
||||||
|
*/
|
||||||
|
add_task(function test_load_string_malformed()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||||
|
|
||||||
|
let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
|
||||||
|
"\"id\":1,";
|
||||||
|
|
||||||
|
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
|
||||||
|
{ tmpPath: store.path + ".tmp" });
|
||||||
|
|
||||||
|
yield store.load();
|
||||||
|
|
||||||
|
// A backup file should have been created.
|
||||||
|
do_check_true(yield OS.File.exists(store.path + ".corrupt"));
|
||||||
|
yield OS.File.remove(store.path + ".corrupt");
|
||||||
|
|
||||||
|
// The store should be ready to accept new data.
|
||||||
|
do_check_eq(store.data.logins.length, 0);
|
||||||
|
do_check_eq(store.data.disabledHosts.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads login data from a malformed JSON string, using the synchronous
|
||||||
|
* initialization path.
|
||||||
|
*/
|
||||||
|
add_task(function test_load_string_malformed_sync()
|
||||||
|
{
|
||||||
|
let store = new LoginStore(getTempFile(TEST_STORE_FILE_NAME).path);
|
||||||
|
|
||||||
|
let string = "{\"logins\":[{\"hostname\":\"http://www.example.com\"," +
|
||||||
|
"\"id\":1,";
|
||||||
|
|
||||||
|
yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
|
||||||
|
{ tmpPath: store.path + ".tmp" });
|
||||||
|
|
||||||
|
store.ensureDataReady();
|
||||||
|
|
||||||
|
// A backup file should have been created.
|
||||||
|
do_check_true(yield OS.File.exists(store.path + ".corrupt"));
|
||||||
|
yield OS.File.remove(store.path + ".corrupt");
|
||||||
|
|
||||||
|
// The store should be ready to accept new data.
|
||||||
|
do_check_eq(store.data.logins.length, 0);
|
||||||
|
do_check_eq(store.data.disabledHosts.length, 0);
|
||||||
|
});
|
@ -3,6 +3,17 @@ head = head.js
|
|||||||
tail =
|
tail =
|
||||||
support-files = data/**
|
support-files = data/**
|
||||||
|
|
||||||
|
# Test JSON file access and import from SQLite, not applicable to Android.
|
||||||
|
[test_module_LoginImport.js]
|
||||||
|
skip-if = os == "android"
|
||||||
|
[test_module_LoginStore.js]
|
||||||
|
skip-if = os == "android"
|
||||||
|
|
||||||
|
# Test SQLite database backup and migration, applicable to Android only.
|
||||||
|
[test_storage_mozStorage.js]
|
||||||
|
skip-if = os != "android"
|
||||||
|
|
||||||
|
# The following tests apply to any storage back-end.
|
||||||
[test_disabled_hosts.js]
|
[test_disabled_hosts.js]
|
||||||
[test_legacy_empty_formSubmitURL.js]
|
[test_legacy_empty_formSubmitURL.js]
|
||||||
[test_legacy_validation.js]
|
[test_legacy_validation.js]
|
||||||
@ -12,4 +23,3 @@ support-files = data/**
|
|||||||
[test_logins_search.js]
|
[test_logins_search.js]
|
||||||
[test_notifications.js]
|
[test_notifications.js]
|
||||||
[test_storage.js]
|
[test_storage.js]
|
||||||
[test_storage_mozStorage.js]
|
|
||||||
|
Loading…
Reference in New Issue
Block a user