mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-30 05:35:31 +00:00
1685 lines
57 KiB
JavaScript
1685 lines
57 KiB
JavaScript
/* -*- 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/. */
|
|
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
|
|
const DB_VERSION = 5; // The database schema version
|
|
|
|
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
|
|
/**
|
|
* Object that manages a database transaction properly so consumers don't have
|
|
* to worry about it throwing.
|
|
*
|
|
* @param aDatabase
|
|
* The mozIStorageConnection to start a transaction on.
|
|
*/
|
|
function Transaction(aDatabase) {
|
|
this._db = aDatabase;
|
|
|
|
this._hasTransaction = false;
|
|
try {
|
|
this._db.beginTransaction();
|
|
this._hasTransaction = true;
|
|
}
|
|
catch(e) { /* om nom nom exceptions */ }
|
|
}
|
|
|
|
Transaction.prototype = {
|
|
commit : function() {
|
|
if (this._hasTransaction)
|
|
this._db.commitTransaction();
|
|
},
|
|
|
|
rollback : function() {
|
|
if (this._hasTransaction)
|
|
this._db.rollbackTransaction();
|
|
},
|
|
};
|
|
|
|
|
|
function LoginManagerStorage_mozStorage() { };
|
|
|
|
LoginManagerStorage_mozStorage.prototype = {
|
|
|
|
classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
|
|
QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
|
|
Ci.nsIInterfaceRequestor]),
|
|
getInterface : function(aIID) {
|
|
if (aIID.equals(Ci.mozIStorageConnection)) {
|
|
return this._dbConnection;
|
|
}
|
|
|
|
throw Cr.NS_ERROR_NO_INTERFACE;
|
|
},
|
|
|
|
__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;
|
|
},
|
|
|
|
__profileDir: null, // nsIFile for the user's profile dir
|
|
get _profileDir() {
|
|
if (!this.__profileDir)
|
|
this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
|
|
return this.__profileDir;
|
|
},
|
|
|
|
__storageService: null, // Storage service for using mozStorage
|
|
get _storageService() {
|
|
if (!this.__storageService)
|
|
this.__storageService = Cc["@mozilla.org/storage/service;1"].
|
|
getService(Ci.mozIStorageService);
|
|
return this.__storageService;
|
|
},
|
|
|
|
__uuidService: null,
|
|
get _uuidService() {
|
|
if (!this.__uuidService)
|
|
this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
|
|
getService(Ci.nsIUUIDGenerator);
|
|
return this.__uuidService;
|
|
},
|
|
|
|
|
|
// The current database schema.
|
|
_dbSchema: {
|
|
tables: {
|
|
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",
|
|
// Changes must be reflected in this._dbAreExpectedColumnsPresent(),
|
|
// this._searchLogins(), and this.modifyLogin().
|
|
|
|
moz_disabledHosts: "id INTEGER PRIMARY KEY," +
|
|
"hostname TEXT UNIQUE ON CONFLICT REPLACE",
|
|
|
|
moz_deleted_logins: "id INTEGER PRIMARY KEY," +
|
|
"guid TEXT," +
|
|
"timeDeleted INTEGER",
|
|
},
|
|
indices: {
|
|
moz_logins_hostname_index: {
|
|
table: "moz_logins",
|
|
columns: ["hostname"]
|
|
},
|
|
moz_logins_hostname_formSubmitURL_index: {
|
|
table: "moz_logins",
|
|
columns: ["hostname", "formSubmitURL"]
|
|
},
|
|
moz_logins_hostname_httpRealm_index: {
|
|
table: "moz_logins",
|
|
columns: ["hostname", "httpRealm"]
|
|
},
|
|
moz_logins_guid_index: {
|
|
table: "moz_logins",
|
|
columns: ["guid"]
|
|
},
|
|
moz_logins_encType_index: {
|
|
table: "moz_logins",
|
|
columns: ["encType"]
|
|
}
|
|
}
|
|
},
|
|
_dbConnection : null, // The database connection
|
|
_dbStmts : null, // Database statements for memoization
|
|
|
|
_prefBranch : null, // Preferences service
|
|
_signonsFile : null, // nsIFile for "signons.sqlite"
|
|
_importFile : null, // nsIFile for import from legacy
|
|
_debug : false, // mirrors signon.debug
|
|
_base64checked : false,
|
|
|
|
|
|
/*
|
|
* log
|
|
*
|
|
* Internal function for logging debug messages to the Error Console.
|
|
*/
|
|
log : function (message) {
|
|
if (!this._debug)
|
|
return;
|
|
dump("PwMgr mozStorage: " + message + "\n");
|
|
Services.console.logStringMessage("PwMgr mozStorage: " + message);
|
|
},
|
|
|
|
|
|
/*
|
|
* initWithFile
|
|
*
|
|
* Initialize the component, but override the default filename locations.
|
|
* This is primarily used to the unit tests and profile migration.
|
|
* aImportFile is legacy storage file, aDBFile is a sqlite/mozStorage file.
|
|
*/
|
|
initWithFile : function(aImportFile, aDBFile) {
|
|
if (aImportFile)
|
|
this._importFile = aImportFile;
|
|
if (aDBFile)
|
|
this._signonsFile = aDBFile;
|
|
|
|
this.init();
|
|
},
|
|
|
|
|
|
/*
|
|
* init
|
|
*
|
|
* Initialize this storage component; import from legacy files, if
|
|
* necessary. Most of the work is done in _deferredInit.
|
|
*/
|
|
init : function () {
|
|
this._dbStmts = {};
|
|
|
|
// Connect to the correct preferences branch.
|
|
this._prefBranch = Services.prefs.getBranch("signon.");
|
|
this._debug = this._prefBranch.getBoolPref("debug");
|
|
|
|
let isFirstRun;
|
|
try {
|
|
// If initWithFile is calling us, _signonsFile may already be set.
|
|
if (!this._signonsFile) {
|
|
// Initialize signons.sqlite
|
|
this._signonsFile = this._profileDir.clone();
|
|
this._signonsFile.append("signons.sqlite");
|
|
}
|
|
this.log("Opening database at " + this._signonsFile.path);
|
|
|
|
// Initialize the database (create, migrate as necessary)
|
|
isFirstRun = this._dbInit();
|
|
|
|
// On first run we want to import the default legacy storage files.
|
|
// Otherwise if passed a file, import from that.
|
|
if (isFirstRun && !this._importFile)
|
|
this._importLegacySignons();
|
|
else if (this._importFile)
|
|
this._importLegacySignons(this._importFile);
|
|
|
|
this._initialized = true;
|
|
} catch (e) {
|
|
this.log("Initialization failed: " + e);
|
|
// If the import fails on first run, we want to delete the db
|
|
if (isFirstRun && e == "Import failed")
|
|
this._dbCleanup(false);
|
|
throw "Initialization failed";
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* addLogin
|
|
*
|
|
*/
|
|
addLogin : function (login) {
|
|
this._addLogin(login, false);
|
|
},
|
|
|
|
|
|
/*
|
|
* _addLogin
|
|
*
|
|
* Private function wrapping core addLogin functionality.
|
|
*/
|
|
_addLogin : function (login, isEncrypted) {
|
|
let encUsername, encPassword;
|
|
|
|
// Throws if there are bogus values.
|
|
this._checkLoginValues(login);
|
|
|
|
// isEncrypted only set when importing from an legacy (signons3.txt)
|
|
// format, which only would have used SDR or BASE64 encoding. The
|
|
// latter of which is handled a little further down.
|
|
if (isEncrypted)
|
|
[encUsername, encPassword, encType] = [login.username, login.password, Ci.nsILoginManagerCrypto.ENCTYPE_SDR];
|
|
else
|
|
[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 = this._uuidService.generateUUID().toString();
|
|
}
|
|
|
|
// If we're migrating legacy storage, check for base64 logins.
|
|
if (isEncrypted &&
|
|
(encUsername.charAt(0) == '~' || encPassword.charAt(0) == '~'))
|
|
encType = this._crypto.ENCTYPE_BASE64;
|
|
|
|
// 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;
|
|
|
|
let query =
|
|
"INSERT INTO moz_logins " +
|
|
"(hostname, httpRealm, formSubmitURL, usernameField, " +
|
|
"passwordField, encryptedUsername, encryptedPassword, " +
|
|
"guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " +
|
|
"timesUsed) " +
|
|
"VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
|
|
":passwordField, :encryptedUsername, :encryptedPassword, " +
|
|
":guid, :encType, :timeCreated, :timeLastUsed, " +
|
|
":timePasswordChanged, :timesUsed)";
|
|
|
|
let params = {
|
|
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
|
|
};
|
|
|
|
let stmt;
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
this.log("_addLogin failed: " + e.name + " : " + e.message);
|
|
throw "Couldn't write to database, login not added.";
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
// Send a notification that a login was added.
|
|
if (!isEncrypted)
|
|
this._sendNotification("addLogin", loginClone);
|
|
},
|
|
|
|
|
|
/*
|
|
* removeLogin
|
|
*
|
|
*/
|
|
removeLogin : function (login) {
|
|
let [idToDelete, storedLogin] = this._getIdForLogin(login);
|
|
if (!idToDelete)
|
|
throw "No matching logins";
|
|
|
|
// Execute the statement & remove from DB
|
|
let query = "DELETE FROM moz_logins WHERE id = :id";
|
|
let params = { id: idToDelete };
|
|
let stmt;
|
|
let transaction = new Transaction(this._dbConnection);
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
this.storeDeletedLogin(storedLogin);
|
|
transaction.commit();
|
|
} catch (e) {
|
|
this.log("_removeLogin failed: " + e.name + " : " + e.message);
|
|
throw "Couldn't write to database, login not removed.";
|
|
transaction.rollback();
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
this._sendNotification("removeLogin", storedLogin);
|
|
},
|
|
|
|
|
|
/*
|
|
* modifyLogin
|
|
*
|
|
*/
|
|
modifyLogin : function (oldLogin, newLoginData) {
|
|
let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
|
|
if (!idToModify)
|
|
throw "No matching logins";
|
|
oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
|
|
|
|
let newLogin;
|
|
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.
|
|
if (newLogin.password != oldLogin.password)
|
|
newLogin.timePasswordChanged = Date.now();
|
|
} else if (newLoginData instanceof Ci.nsIPropertyBag) {
|
|
function _bagHasProperty(aPropName) {
|
|
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!";
|
|
}
|
|
|
|
// Throws if there are bogus values.
|
|
this._checkLoginValues(newLogin);
|
|
|
|
// Get the encrypted value of the username and password.
|
|
let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
|
|
|
|
let query =
|
|
"UPDATE moz_logins " +
|
|
"SET 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 " +
|
|
"WHERE id = :id";
|
|
|
|
let params = {
|
|
id: idToModify,
|
|
hostname: newLogin.hostname,
|
|
httpRealm: newLogin.httpRealm,
|
|
formSubmitURL: newLogin.formSubmitURL,
|
|
usernameField: newLogin.usernameField,
|
|
passwordField: newLogin.passwordField,
|
|
encryptedUsername: encUsername,
|
|
encryptedPassword: encPassword,
|
|
guid: newLogin.guid,
|
|
encType: encType,
|
|
timeCreated: newLogin.timeCreated,
|
|
timeLastUsed: newLogin.timeLastUsed,
|
|
timePasswordChanged: newLogin.timePasswordChanged,
|
|
timesUsed: newLogin.timesUsed
|
|
};
|
|
|
|
let stmt;
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
this.log("modifyLogin failed: " + e.name + " : " + e.message);
|
|
throw "Couldn't write to database, login not modified.";
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
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;
|
|
},
|
|
|
|
|
|
/*
|
|
* getAllEncryptedLogins
|
|
*
|
|
* Not implemented. This interface was added to extract logins from the
|
|
* legacy storage module without decrypting them. Now that logins are in
|
|
* mozStorage, if the encrypted data is really needed it can be easily
|
|
* obtained with SQL and the mozStorage APIs.
|
|
*/
|
|
getAllEncryptedLogins : function (count) {
|
|
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
|
|
/*
|
|
* 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) {
|
|
let conditions = [], params = {};
|
|
|
|
for (let field in matchData) {
|
|
let value = matchData[field];
|
|
switch (field) {
|
|
// Historical compatibility requires this special case
|
|
case "formSubmitURL":
|
|
if (value != null) {
|
|
conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
|
|
params["formSubmitURL"] = value;
|
|
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) {
|
|
conditions.push(field + " isnull");
|
|
} else {
|
|
conditions.push(field + " = :" + field);
|
|
params[field] = value;
|
|
}
|
|
break;
|
|
// Fail if caller requests an unknown property.
|
|
default:
|
|
throw "Unexpected field: " + field;
|
|
}
|
|
}
|
|
|
|
// Build query
|
|
let query = "SELECT * FROM moz_logins";
|
|
if (conditions.length) {
|
|
conditions = conditions.map(function(c) "(" + c + ")");
|
|
query += " WHERE " + conditions.join(" AND ");
|
|
}
|
|
|
|
let stmt;
|
|
let logins = [], ids = [];
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
// We can't execute as usual here, since we're iterating over rows
|
|
while (stmt.executeStep()) {
|
|
// Create the new nsLoginInfo object, push to array
|
|
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
|
|
createInstance(Ci.nsILoginInfo);
|
|
login.init(stmt.row.hostname, stmt.row.formSubmitURL,
|
|
stmt.row.httpRealm, stmt.row.encryptedUsername,
|
|
stmt.row.encryptedPassword, stmt.row.usernameField,
|
|
stmt.row.passwordField);
|
|
// set nsILoginMetaInfo values
|
|
login.QueryInterface(Ci.nsILoginMetaInfo);
|
|
login.guid = stmt.row.guid;
|
|
login.timeCreated = stmt.row.timeCreated;
|
|
login.timeLastUsed = stmt.row.timeLastUsed;
|
|
login.timePasswordChanged = stmt.row.timePasswordChanged;
|
|
login.timesUsed = stmt.row.timesUsed;
|
|
logins.push(login);
|
|
ids.push(stmt.row.id);
|
|
}
|
|
} catch (e) {
|
|
this.log("_searchLogins failed: " + e.name + " : " + e.message);
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
this.log("_searchLogins: returning " + logins.length + " logins");
|
|
return [logins, ids];
|
|
},
|
|
|
|
/* storeDeletedLogin
|
|
*
|
|
* Moves a login to the deleted logins table
|
|
*
|
|
*/
|
|
storeDeletedLogin : function(aLogin) {
|
|
#ifdef ANDROID
|
|
let stmt = null;
|
|
try {
|
|
this.log("Storing " + aLogin.guid + " in deleted passwords\n");
|
|
let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)";
|
|
let params = { guid: aLogin.guid,
|
|
timeDeleted: Date.now() };
|
|
let stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch(ex) {
|
|
throw ex;
|
|
} finally {
|
|
if (stmt)
|
|
stmt.reset();
|
|
}
|
|
#endif
|
|
},
|
|
|
|
|
|
/*
|
|
* removeAllLogins
|
|
*
|
|
* Removes all logins from storage.
|
|
*/
|
|
removeAllLogins : function () {
|
|
this.log("Removing all logins");
|
|
let query;
|
|
let stmt;
|
|
let transaction = new Transaction(this._dbConnection);
|
|
|
|
// Delete any old, unused files.
|
|
this._removeOldSignonsFiles();
|
|
|
|
// Disabled hosts kept, as one presumably doesn't want to erase those.
|
|
// TODO: Add these items to the deleted items table once we've sorted
|
|
// out the issues from bug 756701
|
|
query = "DELETE FROM moz_logins";
|
|
try {
|
|
stmt = this._dbCreateStatement(query);
|
|
stmt.execute();
|
|
transaction.commit();
|
|
} catch (e) {
|
|
this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
|
|
transaction.rollback();
|
|
throw "Couldn't write to database";
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
this._sendNotification("removeAllLogins", null);
|
|
},
|
|
|
|
|
|
/*
|
|
* getAllDisabledHosts
|
|
*
|
|
*/
|
|
getAllDisabledHosts : function (count) {
|
|
let disabledHosts = this._queryDisabledHosts(null);
|
|
|
|
this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
|
|
if (count)
|
|
count.value = disabledHosts.length; // needed for XPCOM
|
|
return disabledHosts;
|
|
},
|
|
|
|
|
|
/*
|
|
* getLoginSavingEnabled
|
|
*
|
|
*/
|
|
getLoginSavingEnabled : function (hostname) {
|
|
this.log("Getting login saving is enabled for " + hostname);
|
|
return this._queryDisabledHosts(hostname).length == 0
|
|
},
|
|
|
|
|
|
/*
|
|
* setLoginSavingEnabled
|
|
*
|
|
*/
|
|
setLoginSavingEnabled : function (hostname, enabled) {
|
|
// Throws if there are bogus values.
|
|
this._checkHostnameValue(hostname);
|
|
|
|
this.log("Setting login saving enabled for " + hostname + " to " + enabled);
|
|
let query;
|
|
if (enabled)
|
|
query = "DELETE FROM moz_disabledHosts " +
|
|
"WHERE hostname = :hostname";
|
|
else
|
|
query = "INSERT INTO moz_disabledHosts " +
|
|
"(hostname) VALUES (:hostname)";
|
|
let params = { hostname: hostname };
|
|
|
|
let stmt
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message);
|
|
throw "Couldn't write to database"
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Do checks for null and empty strings, adjust conditions and params
|
|
let [conditions, params] =
|
|
this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
|
|
|
|
let query = "SELECT COUNT(1) AS numLogins FROM moz_logins";
|
|
if (conditions.length) {
|
|
conditions = conditions.map(function(c) "(" + c + ")");
|
|
query += " WHERE " + conditions.join(" AND ");
|
|
}
|
|
|
|
let stmt, numLogins;
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.executeStep();
|
|
numLogins = stmt.row.numLogins;
|
|
} catch (e) {
|
|
this.log("_countLogins failed: " + e.name + " : " + e.message);
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
this.log("_countLogins: counted logins: " + numLogins);
|
|
return numLogins;
|
|
},
|
|
|
|
|
|
/*
|
|
* uiBusy
|
|
*/
|
|
get uiBusy() {
|
|
return this._crypto.uiBusy;
|
|
},
|
|
|
|
|
|
/*
|
|
* _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];
|
|
},
|
|
|
|
|
|
/*
|
|
* _queryDisabledHosts
|
|
*
|
|
* Returns an array of hostnames from the database according to the
|
|
* criteria given in the argument. If the argument hostname is null, the
|
|
* result array contains all hostnames
|
|
*/
|
|
_queryDisabledHosts : function (hostname) {
|
|
let disabledHosts = [];
|
|
|
|
let query = "SELECT hostname FROM moz_disabledHosts";
|
|
let params = {};
|
|
if (hostname) {
|
|
query += " WHERE hostname = :hostname";
|
|
params = { hostname: hostname };
|
|
}
|
|
|
|
let stmt;
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
while (stmt.executeStep())
|
|
disabledHosts.push(stmt.row.hostname);
|
|
} catch (e) {
|
|
this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message);
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
return disabledHosts;
|
|
},
|
|
|
|
|
|
/*
|
|
* _buildConditionsAndParams
|
|
*
|
|
* Adjusts the WHERE conditions and parameters for statements prior to the
|
|
* statement being created. This fixes the cases where nulls are involved
|
|
* and the empty string is supposed to be a wildcard match
|
|
*/
|
|
_buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) {
|
|
let conditions = [], params = {};
|
|
|
|
if (hostname == null) {
|
|
conditions.push("hostname isnull");
|
|
} else if (hostname != '') {
|
|
conditions.push("hostname = :hostname");
|
|
params["hostname"] = hostname;
|
|
}
|
|
|
|
if (formSubmitURL == null) {
|
|
conditions.push("formSubmitURL isnull");
|
|
} else if (formSubmitURL != '') {
|
|
conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
|
|
params["formSubmitURL"] = formSubmitURL;
|
|
}
|
|
|
|
if (httpRealm == null) {
|
|
conditions.push("httpRealm isnull");
|
|
} else if (httpRealm != '') {
|
|
conditions.push("httpRealm = :httpRealm");
|
|
params["httpRealm"] = httpRealm;
|
|
}
|
|
|
|
return [conditions, params];
|
|
},
|
|
|
|
|
|
/*
|
|
* _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
|
|
*
|
|
* Checks to see if the specified GUID already exists.
|
|
*/
|
|
_isGuidUnique : function (guid) {
|
|
let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid";
|
|
let params = { guid: guid };
|
|
|
|
let stmt, numLogins;
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.executeStep();
|
|
numLogins = stmt.row.numLogins;
|
|
} catch (e) {
|
|
this.log("_isGuidUnique failed: " + e.name + " : " + e.message);
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
return (numLogins == 0);
|
|
},
|
|
|
|
|
|
/*
|
|
* _importLegacySignons
|
|
*
|
|
* Imports a file that uses Legacy storage. Will use importFile if provided
|
|
* else it will attempt to initialize the Legacy storage normally.
|
|
*
|
|
*/
|
|
_importLegacySignons : function (importFile) {
|
|
this.log("Importing " + (importFile ? importFile.path : "legacy storage"));
|
|
|
|
let legacy = Cc["@mozilla.org/login-manager/storage/legacy;1"].
|
|
createInstance(Ci.nsILoginManagerStorage);
|
|
|
|
// Import all logins and disabled hosts
|
|
try {
|
|
if (importFile)
|
|
legacy.initWithFile(importFile, null);
|
|
else
|
|
legacy.init();
|
|
|
|
// Import logins and disabledHosts
|
|
let logins = legacy.getAllEncryptedLogins();
|
|
|
|
// Wrap in a transaction for better performance.
|
|
let transaction = new Transaction(this._dbConnection);
|
|
for each (let login in logins) {
|
|
try {
|
|
this._addLogin(login, true);
|
|
} catch (e) {
|
|
this.log("_importLegacySignons failed to add login: " + e);
|
|
}
|
|
}
|
|
let disabledHosts = legacy.getAllDisabledHosts();
|
|
for each (let hostname in disabledHosts)
|
|
this.setLoginSavingEnabled(hostname, false);
|
|
transaction.commit();
|
|
} catch (e) {
|
|
this.log("_importLegacySignons failed: " + e.name + " : " + e.message);
|
|
throw "Import failed";
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* _removeOldSignonsFiles
|
|
*
|
|
* Deletes any storage files that we're not using any more.
|
|
*/
|
|
_removeOldSignonsFiles : function () {
|
|
// We've used a number of prefs over time due to compatibility issues.
|
|
// We want to delete all files referenced in prefs, which are only for
|
|
// importing and clearing logins from storage-Legacy.js.
|
|
let filenamePrefs = ["SignonFileName3", "SignonFileName2", "SignonFileName"];
|
|
for each (let prefname in filenamePrefs) {
|
|
let filename = this._prefBranch.getCharPref(prefname);
|
|
let file = this._profileDir.clone();
|
|
file.append(filename);
|
|
|
|
if (file.exists()) {
|
|
this.log("Deleting old " + filename + " (" + prefname + ")");
|
|
try {
|
|
file.remove(false);
|
|
} catch (e) {
|
|
this.log("NOTICE: Couldn't delete " + filename + ": " + e);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* _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;
|
|
|
|
if (!this._base64checked)
|
|
this._reencryptBase64Logins();
|
|
|
|
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);
|
|
}
|
|
|
|
if (!this._base64checked)
|
|
this._reencryptBase64Logins();
|
|
|
|
return result;
|
|
},
|
|
|
|
|
|
/*
|
|
* _reencryptBase64Logins
|
|
*
|
|
* Checks the signons DB for any logins using the old wallet-style base64
|
|
* obscuring of the username/password, instead of proper encryption. We're
|
|
* called once per session, after the user has successfully encrypted or
|
|
* decrypted some login (this helps ensure the user doesn't get mysterious
|
|
* prompts for a master password, when set).
|
|
*/
|
|
_reencryptBase64Logins : function () {
|
|
let base64Type = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64;
|
|
this._base64checked = true;
|
|
// Ignore failures, will try again next session...
|
|
|
|
this.log("Reencrypting Base64 logins");
|
|
let transaction;
|
|
try {
|
|
let [logins, ids] = this._searchLogins({ encType: base64Type });
|
|
|
|
if (!logins.length)
|
|
return;
|
|
|
|
try {
|
|
logins = this._decryptLogins(logins);
|
|
} catch (e) {
|
|
// User might have canceled master password entry, just ignore.
|
|
return;
|
|
}
|
|
|
|
transaction = new Transaction(this._dbConnection);
|
|
|
|
let encUsername, encPassword, stmt;
|
|
for each (let login in logins) {
|
|
[encUsername, encPassword, encType] = this._encryptLogin(login);
|
|
|
|
let query =
|
|
"UPDATE moz_logins " +
|
|
"SET encryptedUsername = :encryptedUsername, " +
|
|
"encryptedPassword = :encryptedPassword, " +
|
|
"encType = :encType " +
|
|
"WHERE guid = :guid";
|
|
let params = {
|
|
encryptedUsername: encUsername,
|
|
encryptedPassword: encPassword,
|
|
encType: encType,
|
|
guid: login.guid
|
|
};
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
// Ignore singular errors, continue trying to update others.
|
|
this.log("_reencryptBase64Logins caught error: " + e);
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.log("_reencryptBase64Logins failed: " + e);
|
|
} finally {
|
|
if (transaction) {
|
|
transaction.commit();
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Database Creation & Access
|
|
|
|
/*
|
|
* _dbCreateStatement
|
|
*
|
|
* Creates a statement, wraps it, and then does parameter replacement
|
|
* Returns the wrapped statement for execution. Will use memoization
|
|
* so that statements can be reused.
|
|
*/
|
|
_dbCreateStatement : function (query, params) {
|
|
let wrappedStmt = this._dbStmts[query];
|
|
// Memoize the statements
|
|
if (!wrappedStmt) {
|
|
this.log("Creating new statement for query: " + query);
|
|
wrappedStmt = this._dbConnection.createStatement(query);
|
|
this._dbStmts[query] = wrappedStmt;
|
|
}
|
|
// Replace parameters, must be done 1 at a time
|
|
if (params)
|
|
for (let i in params)
|
|
wrappedStmt.params[i] = params[i];
|
|
return wrappedStmt;
|
|
},
|
|
|
|
|
|
/*
|
|
* _dbInit
|
|
*
|
|
* Attempts to initialize the database. This creates the file if it doesn't
|
|
* exist, performs any migrations, etc. When database is first created, we
|
|
* attempt to import legacy signons. Return if this is the first run.
|
|
*/
|
|
_dbInit : function () {
|
|
this.log("Initializing Database");
|
|
let isFirstRun = false;
|
|
try {
|
|
this._dbConnection = this._storageService.openDatabase(this._signonsFile);
|
|
// Get the version of the schema in the file. It will be 0 if the
|
|
// database has not been created yet.
|
|
let version = this._dbConnection.schemaVersion;
|
|
if (version == 0) {
|
|
this._dbCreate();
|
|
isFirstRun = true;
|
|
} else if (version != DB_VERSION) {
|
|
this._dbMigrate(version);
|
|
}
|
|
} catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
|
|
// Database is corrupted, so we backup the database, then throw
|
|
// causing initialization to fail and a new db to be created next use
|
|
this._dbCleanup(true);
|
|
throw e;
|
|
}
|
|
|
|
Services.obs.addObserver(this, "profile-before-change", false);
|
|
return isFirstRun;
|
|
},
|
|
|
|
observe: function (subject, topic, data) {
|
|
switch (topic) {
|
|
case "profile-before-change":
|
|
Services.obs.removeObserver(this, "profile-before-change");
|
|
this._dbClose();
|
|
break;
|
|
}
|
|
},
|
|
|
|
_dbCreate: function () {
|
|
this.log("Creating Database");
|
|
this._dbCreateSchema();
|
|
this._dbConnection.schemaVersion = DB_VERSION;
|
|
},
|
|
|
|
|
|
_dbCreateSchema : function () {
|
|
this._dbCreateTables();
|
|
this._dbCreateIndices();
|
|
},
|
|
|
|
|
|
_dbCreateTables : function () {
|
|
this.log("Creating Tables");
|
|
for (let name in this._dbSchema.tables)
|
|
this._dbConnection.createTable(name, this._dbSchema.tables[name]);
|
|
},
|
|
|
|
|
|
_dbCreateIndices : function () {
|
|
this.log("Creating Indices");
|
|
for (let name in this._dbSchema.indices) {
|
|
let index = this._dbSchema.indices[name];
|
|
let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
|
|
"(" + index.columns.join(", ") + ")";
|
|
this._dbConnection.executeSimpleSQL(statement);
|
|
}
|
|
},
|
|
|
|
|
|
_dbMigrate : function (oldVersion) {
|
|
this.log("Attempting to migrate from version " + oldVersion);
|
|
|
|
if (oldVersion > DB_VERSION) {
|
|
this.log("Downgrading to version " + DB_VERSION);
|
|
// User's DB is newer. Sanity check that our expected columns are
|
|
// present, and if so mark the lower version and merrily continue
|
|
// on. If the columns are borked, something is wrong so blow away
|
|
// the DB and start from scratch. [Future incompatible upgrades
|
|
// should swtich to a different table or file.]
|
|
|
|
if (!this._dbAreExpectedColumnsPresent())
|
|
throw Components.Exception("DB is missing expected columns",
|
|
Cr.NS_ERROR_FILE_CORRUPTED);
|
|
|
|
// Change the stored version to the current version. If the user
|
|
// runs the newer code again, it will see the lower version number
|
|
// and re-upgrade (to fixup any entries the old code added).
|
|
this._dbConnection.schemaVersion = DB_VERSION;
|
|
return;
|
|
}
|
|
|
|
// Upgrade to newer version...
|
|
|
|
let transaction = new Transaction(this._dbConnection);
|
|
|
|
try {
|
|
for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
|
|
this.log("Upgrading to version " + v + "...");
|
|
let migrateFunction = "_dbMigrateToVersion" + v;
|
|
this[migrateFunction]();
|
|
}
|
|
} catch (e) {
|
|
this.log("Migration failed: " + e);
|
|
transaction.rollback();
|
|
throw e;
|
|
}
|
|
|
|
this._dbConnection.schemaVersion = DB_VERSION;
|
|
transaction.commit();
|
|
this.log("DB migration completed.");
|
|
},
|
|
|
|
|
|
/*
|
|
* _dbMigrateToVersion2
|
|
*
|
|
* Version 2 adds a GUID column. Existing logins are assigned a random GUID.
|
|
*/
|
|
_dbMigrateToVersion2 : function () {
|
|
// Check to see if GUID column already exists, add if needed
|
|
let query;
|
|
if (!this._dbColumnExists("guid")) {
|
|
query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT";
|
|
this._dbConnection.executeSimpleSQL(query);
|
|
|
|
query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)";
|
|
this._dbConnection.executeSimpleSQL(query);
|
|
}
|
|
|
|
// Get a list of IDs for existing logins
|
|
let ids = [];
|
|
let query = "SELECT id FROM moz_logins WHERE guid isnull";
|
|
let stmt;
|
|
try {
|
|
stmt = this._dbCreateStatement(query);
|
|
while (stmt.executeStep())
|
|
ids.push(stmt.row.id);
|
|
} catch (e) {
|
|
this.log("Failed getting IDs: " + e);
|
|
throw e;
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
// Generate a GUID for each login and update the DB.
|
|
query = "UPDATE moz_logins SET guid = :guid WHERE id = :id";
|
|
for each (let id in ids) {
|
|
let params = {
|
|
id: id,
|
|
guid: this._uuidService.generateUUID().toString()
|
|
};
|
|
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
this.log("Failed setting GUID: " + e);
|
|
throw e;
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* _dbMigrateToVersion3
|
|
*
|
|
* Version 3 adds a encType column.
|
|
*/
|
|
_dbMigrateToVersion3 : function () {
|
|
// Check to see if encType column already exists, add if needed
|
|
let query;
|
|
if (!this._dbColumnExists("encType")) {
|
|
query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER";
|
|
this._dbConnection.executeSimpleSQL(query);
|
|
|
|
query = "CREATE INDEX IF NOT EXISTS " +
|
|
"moz_logins_encType_index ON moz_logins (encType)";
|
|
this._dbConnection.executeSimpleSQL(query);
|
|
}
|
|
|
|
// Get a list of existing logins
|
|
let logins = [];
|
|
let stmt;
|
|
query = "SELECT id, encryptedUsername, encryptedPassword " +
|
|
"FROM moz_logins WHERE encType isnull";
|
|
try {
|
|
stmt = this._dbCreateStatement(query);
|
|
while (stmt.executeStep()) {
|
|
let params = { id: stmt.row.id };
|
|
if (stmt.row.encryptedUsername.charAt(0) == '~' ||
|
|
stmt.row.encryptedPassword.charAt(0) == '~')
|
|
params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64;
|
|
else
|
|
params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
|
|
logins.push(params);
|
|
}
|
|
} catch (e) {
|
|
this.log("Failed getting logins: " + e);
|
|
throw e;
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
// Determine encryption type for each login and update the DB.
|
|
query = "UPDATE moz_logins SET encType = :encType WHERE id = :id";
|
|
for each (let params in logins) {
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
this.log("Failed setting encType: " + e);
|
|
throw e;
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* _dbMigrateToVersion4
|
|
*
|
|
* Version 4 adds timeCreated, timeLastUsed, timePasswordChanged,
|
|
* and timesUsed columns
|
|
*/
|
|
_dbMigrateToVersion4 : function () {
|
|
let query;
|
|
// Add the new columns, if needed.
|
|
for each (let column in ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
|
|
if (!this._dbColumnExists(column)) {
|
|
query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER";
|
|
this._dbConnection.executeSimpleSQL(query);
|
|
}
|
|
}
|
|
|
|
// Get a list of IDs for existing logins.
|
|
let ids = [];
|
|
let stmt;
|
|
query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " +
|
|
"timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull";
|
|
try {
|
|
stmt = this._dbCreateStatement(query);
|
|
while (stmt.executeStep())
|
|
ids.push(stmt.row.id);
|
|
} catch (e) {
|
|
this.log("Failed getting IDs: " + e);
|
|
throw e;
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
|
|
// Initialize logins with current time.
|
|
query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " +
|
|
"timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id";
|
|
let params = {
|
|
id: null,
|
|
initTime: Date.now()
|
|
};
|
|
for each (let id in ids) {
|
|
params.id = id;
|
|
try {
|
|
stmt = this._dbCreateStatement(query, params);
|
|
stmt.execute();
|
|
} catch (e) {
|
|
this.log("Failed setting timestamps: " + e);
|
|
throw e;
|
|
} finally {
|
|
if (stmt) {
|
|
stmt.reset();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/*
|
|
* _dbMigrateToVersion5
|
|
*
|
|
* Version 5 adds the moz_deleted_logins table
|
|
*/
|
|
_dbMigrateToVersion5 : function () {
|
|
if (!this._dbConnection.tableExists("moz_deleted_logins")) {
|
|
this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* _dbAreExpectedColumnsPresent
|
|
*
|
|
* Sanity check to ensure that the columns this version of the code expects
|
|
* are present in the DB we're using.
|
|
*/
|
|
_dbAreExpectedColumnsPresent : function () {
|
|
let query = "SELECT " +
|
|
"id, " +
|
|
"hostname, " +
|
|
"httpRealm, " +
|
|
"formSubmitURL, " +
|
|
"usernameField, " +
|
|
"passwordField, " +
|
|
"encryptedUsername, " +
|
|
"encryptedPassword, " +
|
|
"guid, " +
|
|
"encType, " +
|
|
"timeCreated, " +
|
|
"timeLastUsed, " +
|
|
"timePasswordChanged, " +
|
|
"timesUsed " +
|
|
"FROM moz_logins";
|
|
try {
|
|
let stmt = this._dbConnection.createStatement(query);
|
|
// (no need to execute statement, if it compiled we're good)
|
|
stmt.finalize();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
|
|
query = "SELECT " +
|
|
"id, " +
|
|
"hostname " +
|
|
"FROM moz_disabledHosts";
|
|
try {
|
|
let stmt = this._dbConnection.createStatement(query);
|
|
// (no need to execute statement, if it compiled we're good)
|
|
stmt.finalize();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
|
|
this.log("verified that expected columns are present in DB.");
|
|
return true;
|
|
},
|
|
|
|
|
|
/*
|
|
* _dbColumnExists
|
|
*
|
|
* Checks to see if the named column already exists.
|
|
*/
|
|
_dbColumnExists : function (columnName) {
|
|
let query = "SELECT " + columnName + " FROM moz_logins";
|
|
try {
|
|
let stmt = this._dbConnection.createStatement(query);
|
|
// (no need to execute statement, if it compiled we're good)
|
|
stmt.finalize();
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_dbClose : function () {
|
|
this.log("Closing the DB connection.");
|
|
// Finalize all statements to free memory, avoid errors later
|
|
for each (let stmt in this._dbStmts) {
|
|
stmt.finalize();
|
|
}
|
|
this._dbStmts = {};
|
|
|
|
if (this._dbConnection !== null) {
|
|
try {
|
|
this._dbConnection.close();
|
|
} catch (e) {
|
|
Components.utils.reportError(e);
|
|
}
|
|
}
|
|
this._dbConnection = null;
|
|
},
|
|
|
|
/*
|
|
* _dbCleanup
|
|
*
|
|
* Called when database creation fails. Finalizes database statements,
|
|
* closes the database connection, deletes the database file.
|
|
*/
|
|
_dbCleanup : function (backup) {
|
|
this.log("Cleaning up DB file - close & remove & backup=" + backup)
|
|
|
|
// Create backup file
|
|
if (backup) {
|
|
let backupFile = this._signonsFile.leafName + ".corrupt";
|
|
this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
|
|
}
|
|
|
|
this._dbClose();
|
|
this._signonsFile.remove(false);
|
|
}
|
|
|
|
}; // end of nsLoginManagerStorage_mozStorage implementation
|
|
|
|
let component = [LoginManagerStorage_mozStorage];
|
|
var NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
|