1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/passwordmgr/storage-mozStorage.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1512 @@ 1.4 +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ 1.5 +/* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 + 1.11 +const Cc = Components.classes; 1.12 +const Ci = Components.interfaces; 1.13 +const Cr = Components.results; 1.14 + 1.15 +const DB_VERSION = 5; // The database schema version 1.16 + 1.17 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.19 + 1.20 +/** 1.21 + * Object that manages a database transaction properly so consumers don't have 1.22 + * to worry about it throwing. 1.23 + * 1.24 + * @param aDatabase 1.25 + * The mozIStorageConnection to start a transaction on. 1.26 + */ 1.27 +function Transaction(aDatabase) { 1.28 + this._db = aDatabase; 1.29 + 1.30 + this._hasTransaction = false; 1.31 + try { 1.32 + this._db.beginTransaction(); 1.33 + this._hasTransaction = true; 1.34 + } 1.35 + catch(e) { /* om nom nom exceptions */ } 1.36 +} 1.37 + 1.38 +Transaction.prototype = { 1.39 + commit : function() { 1.40 + if (this._hasTransaction) 1.41 + this._db.commitTransaction(); 1.42 + }, 1.43 + 1.44 + rollback : function() { 1.45 + if (this._hasTransaction) 1.46 + this._db.rollbackTransaction(); 1.47 + }, 1.48 +}; 1.49 + 1.50 + 1.51 +function LoginManagerStorage_mozStorage() { }; 1.52 + 1.53 +LoginManagerStorage_mozStorage.prototype = { 1.54 + 1.55 + classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"), 1.56 + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage, 1.57 + Ci.nsIInterfaceRequestor]), 1.58 + getInterface : function(aIID) { 1.59 + if (aIID.equals(Ci.mozIStorageConnection)) { 1.60 + return this._dbConnection; 1.61 + } 1.62 + 1.63 + throw Cr.NS_ERROR_NO_INTERFACE; 1.64 + }, 1.65 + 1.66 + __crypto : null, // nsILoginManagerCrypto service 1.67 + get _crypto() { 1.68 + if (!this.__crypto) 1.69 + this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"]. 1.70 + getService(Ci.nsILoginManagerCrypto); 1.71 + return this.__crypto; 1.72 + }, 1.73 + 1.74 + __profileDir: null, // nsIFile for the user's profile dir 1.75 + get _profileDir() { 1.76 + if (!this.__profileDir) 1.77 + this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); 1.78 + return this.__profileDir; 1.79 + }, 1.80 + 1.81 + __storageService: null, // Storage service for using mozStorage 1.82 + get _storageService() { 1.83 + if (!this.__storageService) 1.84 + this.__storageService = Cc["@mozilla.org/storage/service;1"]. 1.85 + getService(Ci.mozIStorageService); 1.86 + return this.__storageService; 1.87 + }, 1.88 + 1.89 + __uuidService: null, 1.90 + get _uuidService() { 1.91 + if (!this.__uuidService) 1.92 + this.__uuidService = Cc["@mozilla.org/uuid-generator;1"]. 1.93 + getService(Ci.nsIUUIDGenerator); 1.94 + return this.__uuidService; 1.95 + }, 1.96 + 1.97 + 1.98 + // The current database schema. 1.99 + _dbSchema: { 1.100 + tables: { 1.101 + moz_logins: "id INTEGER PRIMARY KEY," + 1.102 + "hostname TEXT NOT NULL," + 1.103 + "httpRealm TEXT," + 1.104 + "formSubmitURL TEXT," + 1.105 + "usernameField TEXT NOT NULL," + 1.106 + "passwordField TEXT NOT NULL," + 1.107 + "encryptedUsername TEXT NOT NULL," + 1.108 + "encryptedPassword TEXT NOT NULL," + 1.109 + "guid TEXT," + 1.110 + "encType INTEGER," + 1.111 + "timeCreated INTEGER," + 1.112 + "timeLastUsed INTEGER," + 1.113 + "timePasswordChanged INTEGER," + 1.114 + "timesUsed INTEGER", 1.115 + // Changes must be reflected in this._dbAreExpectedColumnsPresent(), 1.116 + // this._searchLogins(), and this.modifyLogin(). 1.117 + 1.118 + moz_disabledHosts: "id INTEGER PRIMARY KEY," + 1.119 + "hostname TEXT UNIQUE ON CONFLICT REPLACE", 1.120 + 1.121 + moz_deleted_logins: "id INTEGER PRIMARY KEY," + 1.122 + "guid TEXT," + 1.123 + "timeDeleted INTEGER", 1.124 + }, 1.125 + indices: { 1.126 + moz_logins_hostname_index: { 1.127 + table: "moz_logins", 1.128 + columns: ["hostname"] 1.129 + }, 1.130 + moz_logins_hostname_formSubmitURL_index: { 1.131 + table: "moz_logins", 1.132 + columns: ["hostname", "formSubmitURL"] 1.133 + }, 1.134 + moz_logins_hostname_httpRealm_index: { 1.135 + table: "moz_logins", 1.136 + columns: ["hostname", "httpRealm"] 1.137 + }, 1.138 + moz_logins_guid_index: { 1.139 + table: "moz_logins", 1.140 + columns: ["guid"] 1.141 + }, 1.142 + moz_logins_encType_index: { 1.143 + table: "moz_logins", 1.144 + columns: ["encType"] 1.145 + } 1.146 + } 1.147 + }, 1.148 + _dbConnection : null, // The database connection 1.149 + _dbStmts : null, // Database statements for memoization 1.150 + 1.151 + _prefBranch : null, // Preferences service 1.152 + _signonsFile : null, // nsIFile for "signons.sqlite" 1.153 + _debug : false, // mirrors signon.debug 1.154 + 1.155 + 1.156 + /* 1.157 + * log 1.158 + * 1.159 + * Internal function for logging debug messages to the Error Console. 1.160 + */ 1.161 + log : function (message) { 1.162 + if (!this._debug) 1.163 + return; 1.164 + dump("PwMgr mozStorage: " + message + "\n"); 1.165 + Services.console.logStringMessage("PwMgr mozStorage: " + message); 1.166 + }, 1.167 + 1.168 + 1.169 + /* 1.170 + * initWithFile 1.171 + * 1.172 + * Initialize the component, but override the default filename locations. 1.173 + * This is primarily used to the unit tests and profile migration. 1.174 + */ 1.175 + initWithFile : function(aDBFile) { 1.176 + if (aDBFile) 1.177 + this._signonsFile = aDBFile; 1.178 + 1.179 + this.init(); 1.180 + }, 1.181 + 1.182 + 1.183 + /* 1.184 + * init 1.185 + * 1.186 + */ 1.187 + init : function () { 1.188 + this._dbStmts = {}; 1.189 + 1.190 + // Connect to the correct preferences branch. 1.191 + this._prefBranch = Services.prefs.getBranch("signon."); 1.192 + this._debug = this._prefBranch.getBoolPref("debug"); 1.193 + 1.194 + let isFirstRun; 1.195 + try { 1.196 + // Force initialization of the crypto module. 1.197 + // See bug 717490 comment 17. 1.198 + this._crypto; 1.199 + 1.200 + // If initWithFile is calling us, _signonsFile may already be set. 1.201 + if (!this._signonsFile) { 1.202 + // Initialize signons.sqlite 1.203 + this._signonsFile = this._profileDir.clone(); 1.204 + this._signonsFile.append("signons.sqlite"); 1.205 + } 1.206 + this.log("Opening database at " + this._signonsFile.path); 1.207 + 1.208 + // Initialize the database (create, migrate as necessary) 1.209 + isFirstRun = this._dbInit(); 1.210 + 1.211 + this._initialized = true; 1.212 + } catch (e) { 1.213 + this.log("Initialization failed: " + e); 1.214 + // If the import fails on first run, we want to delete the db 1.215 + if (isFirstRun && e == "Import failed") 1.216 + this._dbCleanup(false); 1.217 + throw "Initialization failed"; 1.218 + } 1.219 + }, 1.220 + 1.221 + 1.222 + /* 1.223 + * addLogin 1.224 + * 1.225 + */ 1.226 + addLogin : function (login) { 1.227 + let encUsername, encPassword; 1.228 + 1.229 + // Throws if there are bogus values. 1.230 + this._checkLoginValues(login); 1.231 + 1.232 + [encUsername, encPassword, encType] = this._encryptLogin(login); 1.233 + 1.234 + // Clone the login, so we don't modify the caller's object. 1.235 + let loginClone = login.clone(); 1.236 + 1.237 + // Initialize the nsILoginMetaInfo fields, unless the caller gave us values 1.238 + loginClone.QueryInterface(Ci.nsILoginMetaInfo); 1.239 + if (loginClone.guid) { 1.240 + if (!this._isGuidUnique(loginClone.guid)) 1.241 + throw "specified GUID already exists"; 1.242 + } else { 1.243 + loginClone.guid = this._uuidService.generateUUID().toString(); 1.244 + } 1.245 + 1.246 + // Set timestamps 1.247 + let currentTime = Date.now(); 1.248 + if (!loginClone.timeCreated) 1.249 + loginClone.timeCreated = currentTime; 1.250 + if (!loginClone.timeLastUsed) 1.251 + loginClone.timeLastUsed = currentTime; 1.252 + if (!loginClone.timePasswordChanged) 1.253 + loginClone.timePasswordChanged = currentTime; 1.254 + if (!loginClone.timesUsed) 1.255 + loginClone.timesUsed = 1; 1.256 + 1.257 + let query = 1.258 + "INSERT INTO moz_logins " + 1.259 + "(hostname, httpRealm, formSubmitURL, usernameField, " + 1.260 + "passwordField, encryptedUsername, encryptedPassword, " + 1.261 + "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " + 1.262 + "timesUsed) " + 1.263 + "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " + 1.264 + ":passwordField, :encryptedUsername, :encryptedPassword, " + 1.265 + ":guid, :encType, :timeCreated, :timeLastUsed, " + 1.266 + ":timePasswordChanged, :timesUsed)"; 1.267 + 1.268 + let params = { 1.269 + hostname: loginClone.hostname, 1.270 + httpRealm: loginClone.httpRealm, 1.271 + formSubmitURL: loginClone.formSubmitURL, 1.272 + usernameField: loginClone.usernameField, 1.273 + passwordField: loginClone.passwordField, 1.274 + encryptedUsername: encUsername, 1.275 + encryptedPassword: encPassword, 1.276 + guid: loginClone.guid, 1.277 + encType: encType, 1.278 + timeCreated: loginClone.timeCreated, 1.279 + timeLastUsed: loginClone.timeLastUsed, 1.280 + timePasswordChanged: loginClone.timePasswordChanged, 1.281 + timesUsed: loginClone.timesUsed 1.282 + }; 1.283 + 1.284 + let stmt; 1.285 + try { 1.286 + stmt = this._dbCreateStatement(query, params); 1.287 + stmt.execute(); 1.288 + } catch (e) { 1.289 + this.log("addLogin failed: " + e.name + " : " + e.message); 1.290 + throw "Couldn't write to database, login not added."; 1.291 + } finally { 1.292 + if (stmt) { 1.293 + stmt.reset(); 1.294 + } 1.295 + } 1.296 + 1.297 + // Send a notification that a login was added. 1.298 + this._sendNotification("addLogin", loginClone); 1.299 + }, 1.300 + 1.301 + 1.302 + /* 1.303 + * removeLogin 1.304 + * 1.305 + */ 1.306 + removeLogin : function (login) { 1.307 + let [idToDelete, storedLogin] = this._getIdForLogin(login); 1.308 + if (!idToDelete) 1.309 + throw "No matching logins"; 1.310 + 1.311 + // Execute the statement & remove from DB 1.312 + let query = "DELETE FROM moz_logins WHERE id = :id"; 1.313 + let params = { id: idToDelete }; 1.314 + let stmt; 1.315 + let transaction = new Transaction(this._dbConnection); 1.316 + try { 1.317 + stmt = this._dbCreateStatement(query, params); 1.318 + stmt.execute(); 1.319 + this.storeDeletedLogin(storedLogin); 1.320 + transaction.commit(); 1.321 + } catch (e) { 1.322 + this.log("_removeLogin failed: " + e.name + " : " + e.message); 1.323 + throw "Couldn't write to database, login not removed."; 1.324 + transaction.rollback(); 1.325 + } finally { 1.326 + if (stmt) { 1.327 + stmt.reset(); 1.328 + } 1.329 + } 1.330 + this._sendNotification("removeLogin", storedLogin); 1.331 + }, 1.332 + 1.333 + 1.334 + /* 1.335 + * modifyLogin 1.336 + * 1.337 + */ 1.338 + modifyLogin : function (oldLogin, newLoginData) { 1.339 + let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin); 1.340 + if (!idToModify) 1.341 + throw "No matching logins"; 1.342 + oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo); 1.343 + 1.344 + let newLogin; 1.345 + if (newLoginData instanceof Ci.nsILoginInfo) { 1.346 + // Clone the existing login to get its nsILoginMetaInfo, then init it 1.347 + // with the replacement nsILoginInfo data from the new login. 1.348 + newLogin = oldStoredLogin.clone(); 1.349 + newLogin.init(newLoginData.hostname, 1.350 + newLoginData.formSubmitURL, newLoginData.httpRealm, 1.351 + newLoginData.username, newLoginData.password, 1.352 + newLoginData.usernameField, newLoginData.passwordField); 1.353 + newLogin.QueryInterface(Ci.nsILoginMetaInfo); 1.354 + 1.355 + // Automatically update metainfo when password is changed. 1.356 + if (newLogin.password != oldLogin.password) 1.357 + newLogin.timePasswordChanged = Date.now(); 1.358 + } else if (newLoginData instanceof Ci.nsIPropertyBag) { 1.359 + function _bagHasProperty(aPropName) { 1.360 + try { 1.361 + newLoginData.getProperty(aPropName); 1.362 + return true; 1.363 + } catch (e) { 1.364 + return false; 1.365 + } 1.366 + } 1.367 + 1.368 + // Clone the existing login, along with all its properties. 1.369 + newLogin = oldStoredLogin.clone(); 1.370 + newLogin.QueryInterface(Ci.nsILoginMetaInfo); 1.371 + 1.372 + // Automatically update metainfo when password is changed. 1.373 + // (Done before the main property updates, lest the caller be 1.374 + // explicitly updating both .password and .timePasswordChanged) 1.375 + if (_bagHasProperty("password")) { 1.376 + let newPassword = newLoginData.getProperty("password"); 1.377 + if (newPassword != oldLogin.password) 1.378 + newLogin.timePasswordChanged = Date.now(); 1.379 + } 1.380 + 1.381 + let propEnum = newLoginData.enumerator; 1.382 + while (propEnum.hasMoreElements()) { 1.383 + let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); 1.384 + switch (prop.name) { 1.385 + // nsILoginInfo properties... 1.386 + case "hostname": 1.387 + case "httpRealm": 1.388 + case "formSubmitURL": 1.389 + case "username": 1.390 + case "password": 1.391 + case "usernameField": 1.392 + case "passwordField": 1.393 + // nsILoginMetaInfo properties... 1.394 + case "guid": 1.395 + case "timeCreated": 1.396 + case "timeLastUsed": 1.397 + case "timePasswordChanged": 1.398 + case "timesUsed": 1.399 + newLogin[prop.name] = prop.value; 1.400 + if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid)) 1.401 + throw "specified GUID already exists"; 1.402 + break; 1.403 + 1.404 + // Fake property, allows easy incrementing. 1.405 + case "timesUsedIncrement": 1.406 + newLogin.timesUsed += prop.value; 1.407 + break; 1.408 + 1.409 + // Fail if caller requests setting an unknown property. 1.410 + default: 1.411 + throw "Unexpected propertybag item: " + prop.name; 1.412 + } 1.413 + } 1.414 + } else { 1.415 + throw "newLoginData needs an expected interface!"; 1.416 + } 1.417 + 1.418 + // Throws if there are bogus values. 1.419 + this._checkLoginValues(newLogin); 1.420 + 1.421 + // Get the encrypted value of the username and password. 1.422 + let [encUsername, encPassword, encType] = this._encryptLogin(newLogin); 1.423 + 1.424 + let query = 1.425 + "UPDATE moz_logins " + 1.426 + "SET hostname = :hostname, " + 1.427 + "httpRealm = :httpRealm, " + 1.428 + "formSubmitURL = :formSubmitURL, " + 1.429 + "usernameField = :usernameField, " + 1.430 + "passwordField = :passwordField, " + 1.431 + "encryptedUsername = :encryptedUsername, " + 1.432 + "encryptedPassword = :encryptedPassword, " + 1.433 + "guid = :guid, " + 1.434 + "encType = :encType, " + 1.435 + "timeCreated = :timeCreated, " + 1.436 + "timeLastUsed = :timeLastUsed, " + 1.437 + "timePasswordChanged = :timePasswordChanged, " + 1.438 + "timesUsed = :timesUsed " + 1.439 + "WHERE id = :id"; 1.440 + 1.441 + let params = { 1.442 + id: idToModify, 1.443 + hostname: newLogin.hostname, 1.444 + httpRealm: newLogin.httpRealm, 1.445 + formSubmitURL: newLogin.formSubmitURL, 1.446 + usernameField: newLogin.usernameField, 1.447 + passwordField: newLogin.passwordField, 1.448 + encryptedUsername: encUsername, 1.449 + encryptedPassword: encPassword, 1.450 + guid: newLogin.guid, 1.451 + encType: encType, 1.452 + timeCreated: newLogin.timeCreated, 1.453 + timeLastUsed: newLogin.timeLastUsed, 1.454 + timePasswordChanged: newLogin.timePasswordChanged, 1.455 + timesUsed: newLogin.timesUsed 1.456 + }; 1.457 + 1.458 + let stmt; 1.459 + try { 1.460 + stmt = this._dbCreateStatement(query, params); 1.461 + stmt.execute(); 1.462 + } catch (e) { 1.463 + this.log("modifyLogin failed: " + e.name + " : " + e.message); 1.464 + throw "Couldn't write to database, login not modified."; 1.465 + } finally { 1.466 + if (stmt) { 1.467 + stmt.reset(); 1.468 + } 1.469 + } 1.470 + 1.471 + this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]); 1.472 + }, 1.473 + 1.474 + 1.475 + /* 1.476 + * getAllLogins 1.477 + * 1.478 + * Returns an array of nsILoginInfo. 1.479 + */ 1.480 + getAllLogins : function (count) { 1.481 + let [logins, ids] = this._searchLogins({}); 1.482 + 1.483 + // decrypt entries for caller. 1.484 + logins = this._decryptLogins(logins); 1.485 + 1.486 + this.log("_getAllLogins: returning " + logins.length + " logins."); 1.487 + if (count) 1.488 + count.value = logins.length; // needed for XPCOM 1.489 + return logins; 1.490 + }, 1.491 + 1.492 + 1.493 + /* 1.494 + * getAllEncryptedLogins 1.495 + * 1.496 + * Not implemented. This interface was added to extract logins from the 1.497 + * legacy storage module without decrypting them. Now that logins are in 1.498 + * mozStorage, if the encrypted data is really needed it can be easily 1.499 + * obtained with SQL and the mozStorage APIs. 1.500 + */ 1.501 + getAllEncryptedLogins : function (count) { 1.502 + throw Cr.NS_ERROR_NOT_IMPLEMENTED; 1.503 + }, 1.504 + 1.505 + 1.506 + /* 1.507 + * searchLogins 1.508 + * 1.509 + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a 1.510 + * JavaScript object and decrypt the results. 1.511 + * 1.512 + * Returns an array of decrypted nsILoginInfo. 1.513 + */ 1.514 + searchLogins : function(count, matchData) { 1.515 + let realMatchData = {}; 1.516 + // Convert nsIPropertyBag to normal JS object 1.517 + let propEnum = matchData.enumerator; 1.518 + while (propEnum.hasMoreElements()) { 1.519 + let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty); 1.520 + realMatchData[prop.name] = prop.value; 1.521 + } 1.522 + 1.523 + let [logins, ids] = this._searchLogins(realMatchData); 1.524 + 1.525 + // Decrypt entries found for the caller. 1.526 + logins = this._decryptLogins(logins); 1.527 + 1.528 + count.value = logins.length; // needed for XPCOM 1.529 + return logins; 1.530 + }, 1.531 + 1.532 + 1.533 + /* 1.534 + * _searchLogins 1.535 + * 1.536 + * Private method to perform arbitrary searches on any field. Decryption is 1.537 + * left to the caller. 1.538 + * 1.539 + * Returns [logins, ids] for logins that match the arguments, where logins 1.540 + * is an array of encrypted nsLoginInfo and ids is an array of associated 1.541 + * ids in the database. 1.542 + */ 1.543 + _searchLogins : function (matchData) { 1.544 + let conditions = [], params = {}; 1.545 + 1.546 + for (let field in matchData) { 1.547 + let value = matchData[field]; 1.548 + switch (field) { 1.549 + // Historical compatibility requires this special case 1.550 + case "formSubmitURL": 1.551 + if (value != null) { 1.552 + conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''"); 1.553 + params["formSubmitURL"] = value; 1.554 + break; 1.555 + } 1.556 + // Normal cases. 1.557 + case "hostname": 1.558 + case "httpRealm": 1.559 + case "id": 1.560 + case "usernameField": 1.561 + case "passwordField": 1.562 + case "encryptedUsername": 1.563 + case "encryptedPassword": 1.564 + case "guid": 1.565 + case "encType": 1.566 + case "timeCreated": 1.567 + case "timeLastUsed": 1.568 + case "timePasswordChanged": 1.569 + case "timesUsed": 1.570 + if (value == null) { 1.571 + conditions.push(field + " isnull"); 1.572 + } else { 1.573 + conditions.push(field + " = :" + field); 1.574 + params[field] = value; 1.575 + } 1.576 + break; 1.577 + // Fail if caller requests an unknown property. 1.578 + default: 1.579 + throw "Unexpected field: " + field; 1.580 + } 1.581 + } 1.582 + 1.583 + // Build query 1.584 + let query = "SELECT * FROM moz_logins"; 1.585 + if (conditions.length) { 1.586 + conditions = conditions.map(function(c) "(" + c + ")"); 1.587 + query += " WHERE " + conditions.join(" AND "); 1.588 + } 1.589 + 1.590 + let stmt; 1.591 + let logins = [], ids = []; 1.592 + try { 1.593 + stmt = this._dbCreateStatement(query, params); 1.594 + // We can't execute as usual here, since we're iterating over rows 1.595 + while (stmt.executeStep()) { 1.596 + // Create the new nsLoginInfo object, push to array 1.597 + let login = Cc["@mozilla.org/login-manager/loginInfo;1"]. 1.598 + createInstance(Ci.nsILoginInfo); 1.599 + login.init(stmt.row.hostname, stmt.row.formSubmitURL, 1.600 + stmt.row.httpRealm, stmt.row.encryptedUsername, 1.601 + stmt.row.encryptedPassword, stmt.row.usernameField, 1.602 + stmt.row.passwordField); 1.603 + // set nsILoginMetaInfo values 1.604 + login.QueryInterface(Ci.nsILoginMetaInfo); 1.605 + login.guid = stmt.row.guid; 1.606 + login.timeCreated = stmt.row.timeCreated; 1.607 + login.timeLastUsed = stmt.row.timeLastUsed; 1.608 + login.timePasswordChanged = stmt.row.timePasswordChanged; 1.609 + login.timesUsed = stmt.row.timesUsed; 1.610 + logins.push(login); 1.611 + ids.push(stmt.row.id); 1.612 + } 1.613 + } catch (e) { 1.614 + this.log("_searchLogins failed: " + e.name + " : " + e.message); 1.615 + } finally { 1.616 + if (stmt) { 1.617 + stmt.reset(); 1.618 + } 1.619 + } 1.620 + 1.621 + this.log("_searchLogins: returning " + logins.length + " logins"); 1.622 + return [logins, ids]; 1.623 + }, 1.624 + 1.625 + /* storeDeletedLogin 1.626 + * 1.627 + * Moves a login to the deleted logins table 1.628 + * 1.629 + */ 1.630 + storeDeletedLogin : function(aLogin) { 1.631 +#ifdef ANDROID 1.632 + let stmt = null; 1.633 + try { 1.634 + this.log("Storing " + aLogin.guid + " in deleted passwords\n"); 1.635 + let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)"; 1.636 + let params = { guid: aLogin.guid, 1.637 + timeDeleted: Date.now() }; 1.638 + let stmt = this._dbCreateStatement(query, params); 1.639 + stmt.execute(); 1.640 + } catch(ex) { 1.641 + throw ex; 1.642 + } finally { 1.643 + if (stmt) 1.644 + stmt.reset(); 1.645 + } 1.646 +#endif 1.647 + }, 1.648 + 1.649 + 1.650 + /* 1.651 + * removeAllLogins 1.652 + * 1.653 + * Removes all logins from storage. 1.654 + */ 1.655 + removeAllLogins : function () { 1.656 + this.log("Removing all logins"); 1.657 + let query; 1.658 + let stmt; 1.659 + let transaction = new Transaction(this._dbConnection); 1.660 + 1.661 + // Disabled hosts kept, as one presumably doesn't want to erase those. 1.662 + // TODO: Add these items to the deleted items table once we've sorted 1.663 + // out the issues from bug 756701 1.664 + query = "DELETE FROM moz_logins"; 1.665 + try { 1.666 + stmt = this._dbCreateStatement(query); 1.667 + stmt.execute(); 1.668 + transaction.commit(); 1.669 + } catch (e) { 1.670 + this.log("_removeAllLogins failed: " + e.name + " : " + e.message); 1.671 + transaction.rollback(); 1.672 + throw "Couldn't write to database"; 1.673 + } finally { 1.674 + if (stmt) { 1.675 + stmt.reset(); 1.676 + } 1.677 + } 1.678 + 1.679 + this._sendNotification("removeAllLogins", null); 1.680 + }, 1.681 + 1.682 + 1.683 + /* 1.684 + * getAllDisabledHosts 1.685 + * 1.686 + */ 1.687 + getAllDisabledHosts : function (count) { 1.688 + let disabledHosts = this._queryDisabledHosts(null); 1.689 + 1.690 + this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts."); 1.691 + if (count) 1.692 + count.value = disabledHosts.length; // needed for XPCOM 1.693 + return disabledHosts; 1.694 + }, 1.695 + 1.696 + 1.697 + /* 1.698 + * getLoginSavingEnabled 1.699 + * 1.700 + */ 1.701 + getLoginSavingEnabled : function (hostname) { 1.702 + this.log("Getting login saving is enabled for " + hostname); 1.703 + return this._queryDisabledHosts(hostname).length == 0 1.704 + }, 1.705 + 1.706 + 1.707 + /* 1.708 + * setLoginSavingEnabled 1.709 + * 1.710 + */ 1.711 + setLoginSavingEnabled : function (hostname, enabled) { 1.712 + // Throws if there are bogus values. 1.713 + this._checkHostnameValue(hostname); 1.714 + 1.715 + this.log("Setting login saving enabled for " + hostname + " to " + enabled); 1.716 + let query; 1.717 + if (enabled) 1.718 + query = "DELETE FROM moz_disabledHosts " + 1.719 + "WHERE hostname = :hostname"; 1.720 + else 1.721 + query = "INSERT INTO moz_disabledHosts " + 1.722 + "(hostname) VALUES (:hostname)"; 1.723 + let params = { hostname: hostname }; 1.724 + 1.725 + let stmt 1.726 + try { 1.727 + stmt = this._dbCreateStatement(query, params); 1.728 + stmt.execute(); 1.729 + } catch (e) { 1.730 + this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message); 1.731 + throw "Couldn't write to database" 1.732 + } finally { 1.733 + if (stmt) { 1.734 + stmt.reset(); 1.735 + } 1.736 + } 1.737 + 1.738 + this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname); 1.739 + }, 1.740 + 1.741 + 1.742 + /* 1.743 + * findLogins 1.744 + * 1.745 + */ 1.746 + findLogins : function (count, hostname, formSubmitURL, httpRealm) { 1.747 + let loginData = { 1.748 + hostname: hostname, 1.749 + formSubmitURL: formSubmitURL, 1.750 + httpRealm: httpRealm 1.751 + }; 1.752 + let matchData = { }; 1.753 + for each (let field in ["hostname", "formSubmitURL", "httpRealm"]) 1.754 + if (loginData[field] != '') 1.755 + matchData[field] = loginData[field]; 1.756 + let [logins, ids] = this._searchLogins(matchData); 1.757 + 1.758 + // Decrypt entries found for the caller. 1.759 + logins = this._decryptLogins(logins); 1.760 + 1.761 + this.log("_findLogins: returning " + logins.length + " logins"); 1.762 + count.value = logins.length; // needed for XPCOM 1.763 + return logins; 1.764 + }, 1.765 + 1.766 + 1.767 + /* 1.768 + * countLogins 1.769 + * 1.770 + */ 1.771 + countLogins : function (hostname, formSubmitURL, httpRealm) { 1.772 + // Do checks for null and empty strings, adjust conditions and params 1.773 + let [conditions, params] = 1.774 + this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm); 1.775 + 1.776 + let query = "SELECT COUNT(1) AS numLogins FROM moz_logins"; 1.777 + if (conditions.length) { 1.778 + conditions = conditions.map(function(c) "(" + c + ")"); 1.779 + query += " WHERE " + conditions.join(" AND "); 1.780 + } 1.781 + 1.782 + let stmt, numLogins; 1.783 + try { 1.784 + stmt = this._dbCreateStatement(query, params); 1.785 + stmt.executeStep(); 1.786 + numLogins = stmt.row.numLogins; 1.787 + } catch (e) { 1.788 + this.log("_countLogins failed: " + e.name + " : " + e.message); 1.789 + } finally { 1.790 + if (stmt) { 1.791 + stmt.reset(); 1.792 + } 1.793 + } 1.794 + 1.795 + this.log("_countLogins: counted logins: " + numLogins); 1.796 + return numLogins; 1.797 + }, 1.798 + 1.799 + 1.800 + /* 1.801 + * uiBusy 1.802 + */ 1.803 + get uiBusy() { 1.804 + return this._crypto.uiBusy; 1.805 + }, 1.806 + 1.807 + 1.808 + /* 1.809 + * isLoggedIn 1.810 + */ 1.811 + get isLoggedIn() { 1.812 + return this._crypto.isLoggedIn; 1.813 + }, 1.814 + 1.815 + 1.816 + /* 1.817 + * _sendNotification 1.818 + * 1.819 + * Send a notification when stored data is changed. 1.820 + */ 1.821 + _sendNotification : function (changeType, data) { 1.822 + let dataObject = data; 1.823 + // Can't pass a raw JS string or array though notifyObservers(). :-( 1.824 + if (data instanceof Array) { 1.825 + dataObject = Cc["@mozilla.org/array;1"]. 1.826 + createInstance(Ci.nsIMutableArray); 1.827 + for (let i = 0; i < data.length; i++) 1.828 + dataObject.appendElement(data[i], false); 1.829 + } else if (typeof(data) == "string") { 1.830 + dataObject = Cc["@mozilla.org/supports-string;1"]. 1.831 + createInstance(Ci.nsISupportsString); 1.832 + dataObject.data = data; 1.833 + } 1.834 + Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType); 1.835 + }, 1.836 + 1.837 + 1.838 + /* 1.839 + * _getIdForLogin 1.840 + * 1.841 + * Returns an array with two items: [id, login]. If the login was not 1.842 + * found, both items will be null. The returned login contains the actual 1.843 + * stored login (useful for looking at the actual nsILoginMetaInfo values). 1.844 + */ 1.845 + _getIdForLogin : function (login) { 1.846 + let matchData = { }; 1.847 + for each (let field in ["hostname", "formSubmitURL", "httpRealm"]) 1.848 + if (login[field] != '') 1.849 + matchData[field] = login[field]; 1.850 + let [logins, ids] = this._searchLogins(matchData); 1.851 + 1.852 + let id = null; 1.853 + let foundLogin = null; 1.854 + 1.855 + // The specified login isn't encrypted, so we need to ensure 1.856 + // the logins we're comparing with are decrypted. We decrypt one entry 1.857 + // at a time, lest _decryptLogins return fewer entries and screw up 1.858 + // indices between the two. 1.859 + for (let i = 0; i < logins.length; i++) { 1.860 + let [decryptedLogin] = this._decryptLogins([logins[i]]); 1.861 + 1.862 + if (!decryptedLogin || !decryptedLogin.equals(login)) 1.863 + continue; 1.864 + 1.865 + // We've found a match, set id and break 1.866 + foundLogin = decryptedLogin; 1.867 + id = ids[i]; 1.868 + break; 1.869 + } 1.870 + 1.871 + return [id, foundLogin]; 1.872 + }, 1.873 + 1.874 + 1.875 + /* 1.876 + * _queryDisabledHosts 1.877 + * 1.878 + * Returns an array of hostnames from the database according to the 1.879 + * criteria given in the argument. If the argument hostname is null, the 1.880 + * result array contains all hostnames 1.881 + */ 1.882 + _queryDisabledHosts : function (hostname) { 1.883 + let disabledHosts = []; 1.884 + 1.885 + let query = "SELECT hostname FROM moz_disabledHosts"; 1.886 + let params = {}; 1.887 + if (hostname) { 1.888 + query += " WHERE hostname = :hostname"; 1.889 + params = { hostname: hostname }; 1.890 + } 1.891 + 1.892 + let stmt; 1.893 + try { 1.894 + stmt = this._dbCreateStatement(query, params); 1.895 + while (stmt.executeStep()) 1.896 + disabledHosts.push(stmt.row.hostname); 1.897 + } catch (e) { 1.898 + this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message); 1.899 + } finally { 1.900 + if (stmt) { 1.901 + stmt.reset(); 1.902 + } 1.903 + } 1.904 + 1.905 + return disabledHosts; 1.906 + }, 1.907 + 1.908 + 1.909 + /* 1.910 + * _buildConditionsAndParams 1.911 + * 1.912 + * Adjusts the WHERE conditions and parameters for statements prior to the 1.913 + * statement being created. This fixes the cases where nulls are involved 1.914 + * and the empty string is supposed to be a wildcard match 1.915 + */ 1.916 + _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) { 1.917 + let conditions = [], params = {}; 1.918 + 1.919 + if (hostname == null) { 1.920 + conditions.push("hostname isnull"); 1.921 + } else if (hostname != '') { 1.922 + conditions.push("hostname = :hostname"); 1.923 + params["hostname"] = hostname; 1.924 + } 1.925 + 1.926 + if (formSubmitURL == null) { 1.927 + conditions.push("formSubmitURL isnull"); 1.928 + } else if (formSubmitURL != '') { 1.929 + conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''"); 1.930 + params["formSubmitURL"] = formSubmitURL; 1.931 + } 1.932 + 1.933 + if (httpRealm == null) { 1.934 + conditions.push("httpRealm isnull"); 1.935 + } else if (httpRealm != '') { 1.936 + conditions.push("httpRealm = :httpRealm"); 1.937 + params["httpRealm"] = httpRealm; 1.938 + } 1.939 + 1.940 + return [conditions, params]; 1.941 + }, 1.942 + 1.943 + 1.944 + /* 1.945 + * _checkLoginValues 1.946 + * 1.947 + * Due to the way the signons2.txt file is formatted, we need to make 1.948 + * sure certain field values or characters do not cause the file to 1.949 + * be parse incorrectly. Reject logins that we can't store correctly. 1.950 + */ 1.951 + _checkLoginValues : function (aLogin) { 1.952 + function badCharacterPresent(l, c) { 1.953 + return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) || 1.954 + (l.httpRealm && l.httpRealm.indexOf(c) != -1) || 1.955 + l.hostname.indexOf(c) != -1 || 1.956 + l.usernameField.indexOf(c) != -1 || 1.957 + l.passwordField.indexOf(c) != -1); 1.958 + } 1.959 + 1.960 + // Nulls are invalid, as they don't round-trip well. 1.961 + // Mostly not a formatting problem, although ".\0" can be quirky. 1.962 + if (badCharacterPresent(aLogin, "\0")) 1.963 + throw "login values can't contain nulls"; 1.964 + 1.965 + // In theory these nulls should just be rolled up into the encrypted 1.966 + // values, but nsISecretDecoderRing doesn't use nsStrings, so the 1.967 + // nulls cause truncation. Check for them here just to avoid 1.968 + // unexpected round-trip surprises. 1.969 + if (aLogin.username.indexOf("\0") != -1 || 1.970 + aLogin.password.indexOf("\0") != -1) 1.971 + throw "login values can't contain nulls"; 1.972 + 1.973 + // Newlines are invalid for any field stored as plaintext. 1.974 + if (badCharacterPresent(aLogin, "\r") || 1.975 + badCharacterPresent(aLogin, "\n")) 1.976 + throw "login values can't contain newlines"; 1.977 + 1.978 + // A line with just a "." can have special meaning. 1.979 + if (aLogin.usernameField == "." || 1.980 + aLogin.formSubmitURL == ".") 1.981 + throw "login values can't be periods"; 1.982 + 1.983 + // A hostname with "\ \(" won't roundtrip. 1.984 + // eg host="foo (", realm="bar" --> "foo ( (bar)" 1.985 + // vs host="foo", realm=" (bar" --> "foo ( (bar)" 1.986 + if (aLogin.hostname.indexOf(" (") != -1) 1.987 + throw "bad parens in hostname"; 1.988 + }, 1.989 + 1.990 + 1.991 + /* 1.992 + * _checkHostnameValue 1.993 + * 1.994 + * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep 1.995 + * that standard here. Throws on illegal format. 1.996 + */ 1.997 + _checkHostnameValue : function (hostname) { 1.998 + // File format prohibits certain values. Also, nulls 1.999 + // won't round-trip with getAllDisabledHosts(). 1.1000 + if (hostname == "." || 1.1001 + hostname.indexOf("\r") != -1 || 1.1002 + hostname.indexOf("\n") != -1 || 1.1003 + hostname.indexOf("\0") != -1) 1.1004 + throw "Invalid hostname"; 1.1005 + }, 1.1006 + 1.1007 + 1.1008 + /* 1.1009 + * _isGuidUnique 1.1010 + * 1.1011 + * Checks to see if the specified GUID already exists. 1.1012 + */ 1.1013 + _isGuidUnique : function (guid) { 1.1014 + let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid"; 1.1015 + let params = { guid: guid }; 1.1016 + 1.1017 + let stmt, numLogins; 1.1018 + try { 1.1019 + stmt = this._dbCreateStatement(query, params); 1.1020 + stmt.executeStep(); 1.1021 + numLogins = stmt.row.numLogins; 1.1022 + } catch (e) { 1.1023 + this.log("_isGuidUnique failed: " + e.name + " : " + e.message); 1.1024 + } finally { 1.1025 + if (stmt) { 1.1026 + stmt.reset(); 1.1027 + } 1.1028 + } 1.1029 + 1.1030 + return (numLogins == 0); 1.1031 + }, 1.1032 + 1.1033 + 1.1034 + /* 1.1035 + * _encryptLogin 1.1036 + * 1.1037 + * Returns the encrypted username, password, and encrypton type for the specified 1.1038 + * login. Can throw if the user cancels a master password entry. 1.1039 + */ 1.1040 + _encryptLogin : function (login) { 1.1041 + let encUsername = this._crypto.encrypt(login.username); 1.1042 + let encPassword = this._crypto.encrypt(login.password); 1.1043 + let encType = this._crypto.defaultEncType; 1.1044 + 1.1045 + return [encUsername, encPassword, encType]; 1.1046 + }, 1.1047 + 1.1048 + 1.1049 + /* 1.1050 + * _decryptLogins 1.1051 + * 1.1052 + * Decrypts username and password fields in the provided array of 1.1053 + * logins. 1.1054 + * 1.1055 + * The entries specified by the array will be decrypted, if possible. 1.1056 + * An array of successfully decrypted logins will be returned. The return 1.1057 + * value should be given to external callers (since still-encrypted 1.1058 + * entries are useless), whereas internal callers generally don't want 1.1059 + * to lose unencrypted entries (eg, because the user clicked Cancel 1.1060 + * instead of entering their master password) 1.1061 + */ 1.1062 + _decryptLogins : function (logins) { 1.1063 + let result = []; 1.1064 + 1.1065 + for each (let login in logins) { 1.1066 + try { 1.1067 + login.username = this._crypto.decrypt(login.username); 1.1068 + login.password = this._crypto.decrypt(login.password); 1.1069 + } catch (e) { 1.1070 + // If decryption failed (corrupt entry?), just skip it. 1.1071 + // Rethrow other errors (like canceling entry of a master pw) 1.1072 + if (e.result == Cr.NS_ERROR_FAILURE) 1.1073 + continue; 1.1074 + throw e; 1.1075 + } 1.1076 + result.push(login); 1.1077 + } 1.1078 + 1.1079 + return result; 1.1080 + }, 1.1081 + 1.1082 + 1.1083 + //**************************************************************************// 1.1084 + // Database Creation & Access 1.1085 + 1.1086 + /* 1.1087 + * _dbCreateStatement 1.1088 + * 1.1089 + * Creates a statement, wraps it, and then does parameter replacement 1.1090 + * Returns the wrapped statement for execution. Will use memoization 1.1091 + * so that statements can be reused. 1.1092 + */ 1.1093 + _dbCreateStatement : function (query, params) { 1.1094 + let wrappedStmt = this._dbStmts[query]; 1.1095 + // Memoize the statements 1.1096 + if (!wrappedStmt) { 1.1097 + this.log("Creating new statement for query: " + query); 1.1098 + wrappedStmt = this._dbConnection.createStatement(query); 1.1099 + this._dbStmts[query] = wrappedStmt; 1.1100 + } 1.1101 + // Replace parameters, must be done 1 at a time 1.1102 + if (params) 1.1103 + for (let i in params) 1.1104 + wrappedStmt.params[i] = params[i]; 1.1105 + return wrappedStmt; 1.1106 + }, 1.1107 + 1.1108 + 1.1109 + /* 1.1110 + * _dbInit 1.1111 + * 1.1112 + * Attempts to initialize the database. This creates the file if it doesn't 1.1113 + * exist, performs any migrations, etc. Return if this is the first run. 1.1114 + */ 1.1115 + _dbInit : function () { 1.1116 + this.log("Initializing Database"); 1.1117 + let isFirstRun = false; 1.1118 + try { 1.1119 + this._dbConnection = this._storageService.openDatabase(this._signonsFile); 1.1120 + // Get the version of the schema in the file. It will be 0 if the 1.1121 + // database has not been created yet. 1.1122 + let version = this._dbConnection.schemaVersion; 1.1123 + if (version == 0) { 1.1124 + this._dbCreate(); 1.1125 + isFirstRun = true; 1.1126 + } else if (version != DB_VERSION) { 1.1127 + this._dbMigrate(version); 1.1128 + } 1.1129 + } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) { 1.1130 + // Database is corrupted, so we backup the database, then throw 1.1131 + // causing initialization to fail and a new db to be created next use 1.1132 + this._dbCleanup(true); 1.1133 + throw e; 1.1134 + } 1.1135 + 1.1136 + Services.obs.addObserver(this, "profile-before-change", false); 1.1137 + return isFirstRun; 1.1138 + }, 1.1139 + 1.1140 + observe: function (subject, topic, data) { 1.1141 + switch (topic) { 1.1142 + case "profile-before-change": 1.1143 + Services.obs.removeObserver(this, "profile-before-change"); 1.1144 + this._dbClose(); 1.1145 + break; 1.1146 + } 1.1147 + }, 1.1148 + 1.1149 + _dbCreate: function () { 1.1150 + this.log("Creating Database"); 1.1151 + this._dbCreateSchema(); 1.1152 + this._dbConnection.schemaVersion = DB_VERSION; 1.1153 + }, 1.1154 + 1.1155 + 1.1156 + _dbCreateSchema : function () { 1.1157 + this._dbCreateTables(); 1.1158 + this._dbCreateIndices(); 1.1159 + }, 1.1160 + 1.1161 + 1.1162 + _dbCreateTables : function () { 1.1163 + this.log("Creating Tables"); 1.1164 + for (let name in this._dbSchema.tables) 1.1165 + this._dbConnection.createTable(name, this._dbSchema.tables[name]); 1.1166 + }, 1.1167 + 1.1168 + 1.1169 + _dbCreateIndices : function () { 1.1170 + this.log("Creating Indices"); 1.1171 + for (let name in this._dbSchema.indices) { 1.1172 + let index = this._dbSchema.indices[name]; 1.1173 + let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table + 1.1174 + "(" + index.columns.join(", ") + ")"; 1.1175 + this._dbConnection.executeSimpleSQL(statement); 1.1176 + } 1.1177 + }, 1.1178 + 1.1179 + 1.1180 + _dbMigrate : function (oldVersion) { 1.1181 + this.log("Attempting to migrate from version " + oldVersion); 1.1182 + 1.1183 + if (oldVersion > DB_VERSION) { 1.1184 + this.log("Downgrading to version " + DB_VERSION); 1.1185 + // User's DB is newer. Sanity check that our expected columns are 1.1186 + // present, and if so mark the lower version and merrily continue 1.1187 + // on. If the columns are borked, something is wrong so blow away 1.1188 + // the DB and start from scratch. [Future incompatible upgrades 1.1189 + // should swtich to a different table or file.] 1.1190 + 1.1191 + if (!this._dbAreExpectedColumnsPresent()) 1.1192 + throw Components.Exception("DB is missing expected columns", 1.1193 + Cr.NS_ERROR_FILE_CORRUPTED); 1.1194 + 1.1195 + // Change the stored version to the current version. If the user 1.1196 + // runs the newer code again, it will see the lower version number 1.1197 + // and re-upgrade (to fixup any entries the old code added). 1.1198 + this._dbConnection.schemaVersion = DB_VERSION; 1.1199 + return; 1.1200 + } 1.1201 + 1.1202 + // Upgrade to newer version... 1.1203 + 1.1204 + let transaction = new Transaction(this._dbConnection); 1.1205 + 1.1206 + try { 1.1207 + for (let v = oldVersion + 1; v <= DB_VERSION; v++) { 1.1208 + this.log("Upgrading to version " + v + "..."); 1.1209 + let migrateFunction = "_dbMigrateToVersion" + v; 1.1210 + this[migrateFunction](); 1.1211 + } 1.1212 + } catch (e) { 1.1213 + this.log("Migration failed: " + e); 1.1214 + transaction.rollback(); 1.1215 + throw e; 1.1216 + } 1.1217 + 1.1218 + this._dbConnection.schemaVersion = DB_VERSION; 1.1219 + transaction.commit(); 1.1220 + this.log("DB migration completed."); 1.1221 + }, 1.1222 + 1.1223 + 1.1224 + /* 1.1225 + * _dbMigrateToVersion2 1.1226 + * 1.1227 + * Version 2 adds a GUID column. Existing logins are assigned a random GUID. 1.1228 + */ 1.1229 + _dbMigrateToVersion2 : function () { 1.1230 + // Check to see if GUID column already exists, add if needed 1.1231 + let query; 1.1232 + if (!this._dbColumnExists("guid")) { 1.1233 + query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT"; 1.1234 + this._dbConnection.executeSimpleSQL(query); 1.1235 + 1.1236 + query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)"; 1.1237 + this._dbConnection.executeSimpleSQL(query); 1.1238 + } 1.1239 + 1.1240 + // Get a list of IDs for existing logins 1.1241 + let ids = []; 1.1242 + let query = "SELECT id FROM moz_logins WHERE guid isnull"; 1.1243 + let stmt; 1.1244 + try { 1.1245 + stmt = this._dbCreateStatement(query); 1.1246 + while (stmt.executeStep()) 1.1247 + ids.push(stmt.row.id); 1.1248 + } catch (e) { 1.1249 + this.log("Failed getting IDs: " + e); 1.1250 + throw e; 1.1251 + } finally { 1.1252 + if (stmt) { 1.1253 + stmt.reset(); 1.1254 + } 1.1255 + } 1.1256 + 1.1257 + // Generate a GUID for each login and update the DB. 1.1258 + query = "UPDATE moz_logins SET guid = :guid WHERE id = :id"; 1.1259 + for each (let id in ids) { 1.1260 + let params = { 1.1261 + id: id, 1.1262 + guid: this._uuidService.generateUUID().toString() 1.1263 + }; 1.1264 + 1.1265 + try { 1.1266 + stmt = this._dbCreateStatement(query, params); 1.1267 + stmt.execute(); 1.1268 + } catch (e) { 1.1269 + this.log("Failed setting GUID: " + e); 1.1270 + throw e; 1.1271 + } finally { 1.1272 + if (stmt) { 1.1273 + stmt.reset(); 1.1274 + } 1.1275 + } 1.1276 + } 1.1277 + }, 1.1278 + 1.1279 + 1.1280 + /* 1.1281 + * _dbMigrateToVersion3 1.1282 + * 1.1283 + * Version 3 adds a encType column. 1.1284 + */ 1.1285 + _dbMigrateToVersion3 : function () { 1.1286 + // Check to see if encType column already exists, add if needed 1.1287 + let query; 1.1288 + if (!this._dbColumnExists("encType")) { 1.1289 + query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER"; 1.1290 + this._dbConnection.executeSimpleSQL(query); 1.1291 + 1.1292 + query = "CREATE INDEX IF NOT EXISTS " + 1.1293 + "moz_logins_encType_index ON moz_logins (encType)"; 1.1294 + this._dbConnection.executeSimpleSQL(query); 1.1295 + } 1.1296 + 1.1297 + // Get a list of existing logins 1.1298 + let logins = []; 1.1299 + let stmt; 1.1300 + query = "SELECT id, encryptedUsername, encryptedPassword " + 1.1301 + "FROM moz_logins WHERE encType isnull"; 1.1302 + try { 1.1303 + stmt = this._dbCreateStatement(query); 1.1304 + while (stmt.executeStep()) { 1.1305 + let params = { id: stmt.row.id }; 1.1306 + // We will tag base64 logins correctly, but no longer support their use. 1.1307 + if (stmt.row.encryptedUsername.charAt(0) == '~' || 1.1308 + stmt.row.encryptedPassword.charAt(0) == '~') 1.1309 + params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64; 1.1310 + else 1.1311 + params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR; 1.1312 + logins.push(params); 1.1313 + } 1.1314 + } catch (e) { 1.1315 + this.log("Failed getting logins: " + e); 1.1316 + throw e; 1.1317 + } finally { 1.1318 + if (stmt) { 1.1319 + stmt.reset(); 1.1320 + } 1.1321 + } 1.1322 + 1.1323 + // Determine encryption type for each login and update the DB. 1.1324 + query = "UPDATE moz_logins SET encType = :encType WHERE id = :id"; 1.1325 + for each (let params in logins) { 1.1326 + try { 1.1327 + stmt = this._dbCreateStatement(query, params); 1.1328 + stmt.execute(); 1.1329 + } catch (e) { 1.1330 + this.log("Failed setting encType: " + e); 1.1331 + throw e; 1.1332 + } finally { 1.1333 + if (stmt) { 1.1334 + stmt.reset(); 1.1335 + } 1.1336 + } 1.1337 + } 1.1338 + }, 1.1339 + 1.1340 + 1.1341 + /* 1.1342 + * _dbMigrateToVersion4 1.1343 + * 1.1344 + * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged, 1.1345 + * and timesUsed columns 1.1346 + */ 1.1347 + _dbMigrateToVersion4 : function () { 1.1348 + let query; 1.1349 + // Add the new columns, if needed. 1.1350 + for each (let column in ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) { 1.1351 + if (!this._dbColumnExists(column)) { 1.1352 + query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER"; 1.1353 + this._dbConnection.executeSimpleSQL(query); 1.1354 + } 1.1355 + } 1.1356 + 1.1357 + // Get a list of IDs for existing logins. 1.1358 + let ids = []; 1.1359 + let stmt; 1.1360 + query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " + 1.1361 + "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull"; 1.1362 + try { 1.1363 + stmt = this._dbCreateStatement(query); 1.1364 + while (stmt.executeStep()) 1.1365 + ids.push(stmt.row.id); 1.1366 + } catch (e) { 1.1367 + this.log("Failed getting IDs: " + e); 1.1368 + throw e; 1.1369 + } finally { 1.1370 + if (stmt) { 1.1371 + stmt.reset(); 1.1372 + } 1.1373 + } 1.1374 + 1.1375 + // Initialize logins with current time. 1.1376 + query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " + 1.1377 + "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id"; 1.1378 + let params = { 1.1379 + id: null, 1.1380 + initTime: Date.now() 1.1381 + }; 1.1382 + for each (let id in ids) { 1.1383 + params.id = id; 1.1384 + try { 1.1385 + stmt = this._dbCreateStatement(query, params); 1.1386 + stmt.execute(); 1.1387 + } catch (e) { 1.1388 + this.log("Failed setting timestamps: " + e); 1.1389 + throw e; 1.1390 + } finally { 1.1391 + if (stmt) { 1.1392 + stmt.reset(); 1.1393 + } 1.1394 + } 1.1395 + } 1.1396 + }, 1.1397 + 1.1398 + 1.1399 + /* 1.1400 + * _dbMigrateToVersion5 1.1401 + * 1.1402 + * Version 5 adds the moz_deleted_logins table 1.1403 + */ 1.1404 + _dbMigrateToVersion5 : function () { 1.1405 + if (!this._dbConnection.tableExists("moz_deleted_logins")) { 1.1406 + this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins); 1.1407 + } 1.1408 + }, 1.1409 + 1.1410 + /* 1.1411 + * _dbAreExpectedColumnsPresent 1.1412 + * 1.1413 + * Sanity check to ensure that the columns this version of the code expects 1.1414 + * are present in the DB we're using. 1.1415 + */ 1.1416 + _dbAreExpectedColumnsPresent : function () { 1.1417 + let query = "SELECT " + 1.1418 + "id, " + 1.1419 + "hostname, " + 1.1420 + "httpRealm, " + 1.1421 + "formSubmitURL, " + 1.1422 + "usernameField, " + 1.1423 + "passwordField, " + 1.1424 + "encryptedUsername, " + 1.1425 + "encryptedPassword, " + 1.1426 + "guid, " + 1.1427 + "encType, " + 1.1428 + "timeCreated, " + 1.1429 + "timeLastUsed, " + 1.1430 + "timePasswordChanged, " + 1.1431 + "timesUsed " + 1.1432 + "FROM moz_logins"; 1.1433 + try { 1.1434 + let stmt = this._dbConnection.createStatement(query); 1.1435 + // (no need to execute statement, if it compiled we're good) 1.1436 + stmt.finalize(); 1.1437 + } catch (e) { 1.1438 + return false; 1.1439 + } 1.1440 + 1.1441 + query = "SELECT " + 1.1442 + "id, " + 1.1443 + "hostname " + 1.1444 + "FROM moz_disabledHosts"; 1.1445 + try { 1.1446 + let stmt = this._dbConnection.createStatement(query); 1.1447 + // (no need to execute statement, if it compiled we're good) 1.1448 + stmt.finalize(); 1.1449 + } catch (e) { 1.1450 + return false; 1.1451 + } 1.1452 + 1.1453 + this.log("verified that expected columns are present in DB."); 1.1454 + return true; 1.1455 + }, 1.1456 + 1.1457 + 1.1458 + /* 1.1459 + * _dbColumnExists 1.1460 + * 1.1461 + * Checks to see if the named column already exists. 1.1462 + */ 1.1463 + _dbColumnExists : function (columnName) { 1.1464 + let query = "SELECT " + columnName + " FROM moz_logins"; 1.1465 + try { 1.1466 + let stmt = this._dbConnection.createStatement(query); 1.1467 + // (no need to execute statement, if it compiled we're good) 1.1468 + stmt.finalize(); 1.1469 + return true; 1.1470 + } catch (e) { 1.1471 + return false; 1.1472 + } 1.1473 + }, 1.1474 + 1.1475 + _dbClose : function () { 1.1476 + this.log("Closing the DB connection."); 1.1477 + // Finalize all statements to free memory, avoid errors later 1.1478 + for each (let stmt in this._dbStmts) { 1.1479 + stmt.finalize(); 1.1480 + } 1.1481 + this._dbStmts = {}; 1.1482 + 1.1483 + if (this._dbConnection !== null) { 1.1484 + try { 1.1485 + this._dbConnection.close(); 1.1486 + } catch (e) { 1.1487 + Components.utils.reportError(e); 1.1488 + } 1.1489 + } 1.1490 + this._dbConnection = null; 1.1491 + }, 1.1492 + 1.1493 + /* 1.1494 + * _dbCleanup 1.1495 + * 1.1496 + * Called when database creation fails. Finalizes database statements, 1.1497 + * closes the database connection, deletes the database file. 1.1498 + */ 1.1499 + _dbCleanup : function (backup) { 1.1500 + this.log("Cleaning up DB file - close & remove & backup=" + backup) 1.1501 + 1.1502 + // Create backup file 1.1503 + if (backup) { 1.1504 + let backupFile = this._signonsFile.leafName + ".corrupt"; 1.1505 + this._storageService.backupDatabaseFile(this._signonsFile, backupFile); 1.1506 + } 1.1507 + 1.1508 + this._dbClose(); 1.1509 + this._signonsFile.remove(false); 1.1510 + } 1.1511 + 1.1512 +}; // end of nsLoginManagerStorage_mozStorage implementation 1.1513 + 1.1514 +let component = [LoginManagerStorage_mozStorage]; 1.1515 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);