toolkit/components/passwordmgr/storage-mozStorage.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
michael@0 2 /* vim: set sw=4 ts=4 et lcs=trail\:.,tab\:>~ : */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7
michael@0 8 const Cc = Components.classes;
michael@0 9 const Ci = Components.interfaces;
michael@0 10 const Cr = Components.results;
michael@0 11
michael@0 12 const DB_VERSION = 5; // The database schema version
michael@0 13
michael@0 14 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 15 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 16
michael@0 17 /**
michael@0 18 * Object that manages a database transaction properly so consumers don't have
michael@0 19 * to worry about it throwing.
michael@0 20 *
michael@0 21 * @param aDatabase
michael@0 22 * The mozIStorageConnection to start a transaction on.
michael@0 23 */
michael@0 24 function Transaction(aDatabase) {
michael@0 25 this._db = aDatabase;
michael@0 26
michael@0 27 this._hasTransaction = false;
michael@0 28 try {
michael@0 29 this._db.beginTransaction();
michael@0 30 this._hasTransaction = true;
michael@0 31 }
michael@0 32 catch(e) { /* om nom nom exceptions */ }
michael@0 33 }
michael@0 34
michael@0 35 Transaction.prototype = {
michael@0 36 commit : function() {
michael@0 37 if (this._hasTransaction)
michael@0 38 this._db.commitTransaction();
michael@0 39 },
michael@0 40
michael@0 41 rollback : function() {
michael@0 42 if (this._hasTransaction)
michael@0 43 this._db.rollbackTransaction();
michael@0 44 },
michael@0 45 };
michael@0 46
michael@0 47
michael@0 48 function LoginManagerStorage_mozStorage() { };
michael@0 49
michael@0 50 LoginManagerStorage_mozStorage.prototype = {
michael@0 51
michael@0 52 classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
michael@0 53 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
michael@0 54 Ci.nsIInterfaceRequestor]),
michael@0 55 getInterface : function(aIID) {
michael@0 56 if (aIID.equals(Ci.mozIStorageConnection)) {
michael@0 57 return this._dbConnection;
michael@0 58 }
michael@0 59
michael@0 60 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 61 },
michael@0 62
michael@0 63 __crypto : null, // nsILoginManagerCrypto service
michael@0 64 get _crypto() {
michael@0 65 if (!this.__crypto)
michael@0 66 this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
michael@0 67 getService(Ci.nsILoginManagerCrypto);
michael@0 68 return this.__crypto;
michael@0 69 },
michael@0 70
michael@0 71 __profileDir: null, // nsIFile for the user's profile dir
michael@0 72 get _profileDir() {
michael@0 73 if (!this.__profileDir)
michael@0 74 this.__profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
michael@0 75 return this.__profileDir;
michael@0 76 },
michael@0 77
michael@0 78 __storageService: null, // Storage service for using mozStorage
michael@0 79 get _storageService() {
michael@0 80 if (!this.__storageService)
michael@0 81 this.__storageService = Cc["@mozilla.org/storage/service;1"].
michael@0 82 getService(Ci.mozIStorageService);
michael@0 83 return this.__storageService;
michael@0 84 },
michael@0 85
michael@0 86 __uuidService: null,
michael@0 87 get _uuidService() {
michael@0 88 if (!this.__uuidService)
michael@0 89 this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
michael@0 90 getService(Ci.nsIUUIDGenerator);
michael@0 91 return this.__uuidService;
michael@0 92 },
michael@0 93
michael@0 94
michael@0 95 // The current database schema.
michael@0 96 _dbSchema: {
michael@0 97 tables: {
michael@0 98 moz_logins: "id INTEGER PRIMARY KEY," +
michael@0 99 "hostname TEXT NOT NULL," +
michael@0 100 "httpRealm TEXT," +
michael@0 101 "formSubmitURL TEXT," +
michael@0 102 "usernameField TEXT NOT NULL," +
michael@0 103 "passwordField TEXT NOT NULL," +
michael@0 104 "encryptedUsername TEXT NOT NULL," +
michael@0 105 "encryptedPassword TEXT NOT NULL," +
michael@0 106 "guid TEXT," +
michael@0 107 "encType INTEGER," +
michael@0 108 "timeCreated INTEGER," +
michael@0 109 "timeLastUsed INTEGER," +
michael@0 110 "timePasswordChanged INTEGER," +
michael@0 111 "timesUsed INTEGER",
michael@0 112 // Changes must be reflected in this._dbAreExpectedColumnsPresent(),
michael@0 113 // this._searchLogins(), and this.modifyLogin().
michael@0 114
michael@0 115 moz_disabledHosts: "id INTEGER PRIMARY KEY," +
michael@0 116 "hostname TEXT UNIQUE ON CONFLICT REPLACE",
michael@0 117
michael@0 118 moz_deleted_logins: "id INTEGER PRIMARY KEY," +
michael@0 119 "guid TEXT," +
michael@0 120 "timeDeleted INTEGER",
michael@0 121 },
michael@0 122 indices: {
michael@0 123 moz_logins_hostname_index: {
michael@0 124 table: "moz_logins",
michael@0 125 columns: ["hostname"]
michael@0 126 },
michael@0 127 moz_logins_hostname_formSubmitURL_index: {
michael@0 128 table: "moz_logins",
michael@0 129 columns: ["hostname", "formSubmitURL"]
michael@0 130 },
michael@0 131 moz_logins_hostname_httpRealm_index: {
michael@0 132 table: "moz_logins",
michael@0 133 columns: ["hostname", "httpRealm"]
michael@0 134 },
michael@0 135 moz_logins_guid_index: {
michael@0 136 table: "moz_logins",
michael@0 137 columns: ["guid"]
michael@0 138 },
michael@0 139 moz_logins_encType_index: {
michael@0 140 table: "moz_logins",
michael@0 141 columns: ["encType"]
michael@0 142 }
michael@0 143 }
michael@0 144 },
michael@0 145 _dbConnection : null, // The database connection
michael@0 146 _dbStmts : null, // Database statements for memoization
michael@0 147
michael@0 148 _prefBranch : null, // Preferences service
michael@0 149 _signonsFile : null, // nsIFile for "signons.sqlite"
michael@0 150 _debug : false, // mirrors signon.debug
michael@0 151
michael@0 152
michael@0 153 /*
michael@0 154 * log
michael@0 155 *
michael@0 156 * Internal function for logging debug messages to the Error Console.
michael@0 157 */
michael@0 158 log : function (message) {
michael@0 159 if (!this._debug)
michael@0 160 return;
michael@0 161 dump("PwMgr mozStorage: " + message + "\n");
michael@0 162 Services.console.logStringMessage("PwMgr mozStorage: " + message);
michael@0 163 },
michael@0 164
michael@0 165
michael@0 166 /*
michael@0 167 * initWithFile
michael@0 168 *
michael@0 169 * Initialize the component, but override the default filename locations.
michael@0 170 * This is primarily used to the unit tests and profile migration.
michael@0 171 */
michael@0 172 initWithFile : function(aDBFile) {
michael@0 173 if (aDBFile)
michael@0 174 this._signonsFile = aDBFile;
michael@0 175
michael@0 176 this.init();
michael@0 177 },
michael@0 178
michael@0 179
michael@0 180 /*
michael@0 181 * init
michael@0 182 *
michael@0 183 */
michael@0 184 init : function () {
michael@0 185 this._dbStmts = {};
michael@0 186
michael@0 187 // Connect to the correct preferences branch.
michael@0 188 this._prefBranch = Services.prefs.getBranch("signon.");
michael@0 189 this._debug = this._prefBranch.getBoolPref("debug");
michael@0 190
michael@0 191 let isFirstRun;
michael@0 192 try {
michael@0 193 // Force initialization of the crypto module.
michael@0 194 // See bug 717490 comment 17.
michael@0 195 this._crypto;
michael@0 196
michael@0 197 // If initWithFile is calling us, _signonsFile may already be set.
michael@0 198 if (!this._signonsFile) {
michael@0 199 // Initialize signons.sqlite
michael@0 200 this._signonsFile = this._profileDir.clone();
michael@0 201 this._signonsFile.append("signons.sqlite");
michael@0 202 }
michael@0 203 this.log("Opening database at " + this._signonsFile.path);
michael@0 204
michael@0 205 // Initialize the database (create, migrate as necessary)
michael@0 206 isFirstRun = this._dbInit();
michael@0 207
michael@0 208 this._initialized = true;
michael@0 209 } catch (e) {
michael@0 210 this.log("Initialization failed: " + e);
michael@0 211 // If the import fails on first run, we want to delete the db
michael@0 212 if (isFirstRun && e == "Import failed")
michael@0 213 this._dbCleanup(false);
michael@0 214 throw "Initialization failed";
michael@0 215 }
michael@0 216 },
michael@0 217
michael@0 218
michael@0 219 /*
michael@0 220 * addLogin
michael@0 221 *
michael@0 222 */
michael@0 223 addLogin : function (login) {
michael@0 224 let encUsername, encPassword;
michael@0 225
michael@0 226 // Throws if there are bogus values.
michael@0 227 this._checkLoginValues(login);
michael@0 228
michael@0 229 [encUsername, encPassword, encType] = this._encryptLogin(login);
michael@0 230
michael@0 231 // Clone the login, so we don't modify the caller's object.
michael@0 232 let loginClone = login.clone();
michael@0 233
michael@0 234 // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
michael@0 235 loginClone.QueryInterface(Ci.nsILoginMetaInfo);
michael@0 236 if (loginClone.guid) {
michael@0 237 if (!this._isGuidUnique(loginClone.guid))
michael@0 238 throw "specified GUID already exists";
michael@0 239 } else {
michael@0 240 loginClone.guid = this._uuidService.generateUUID().toString();
michael@0 241 }
michael@0 242
michael@0 243 // Set timestamps
michael@0 244 let currentTime = Date.now();
michael@0 245 if (!loginClone.timeCreated)
michael@0 246 loginClone.timeCreated = currentTime;
michael@0 247 if (!loginClone.timeLastUsed)
michael@0 248 loginClone.timeLastUsed = currentTime;
michael@0 249 if (!loginClone.timePasswordChanged)
michael@0 250 loginClone.timePasswordChanged = currentTime;
michael@0 251 if (!loginClone.timesUsed)
michael@0 252 loginClone.timesUsed = 1;
michael@0 253
michael@0 254 let query =
michael@0 255 "INSERT INTO moz_logins " +
michael@0 256 "(hostname, httpRealm, formSubmitURL, usernameField, " +
michael@0 257 "passwordField, encryptedUsername, encryptedPassword, " +
michael@0 258 "guid, encType, timeCreated, timeLastUsed, timePasswordChanged, " +
michael@0 259 "timesUsed) " +
michael@0 260 "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
michael@0 261 ":passwordField, :encryptedUsername, :encryptedPassword, " +
michael@0 262 ":guid, :encType, :timeCreated, :timeLastUsed, " +
michael@0 263 ":timePasswordChanged, :timesUsed)";
michael@0 264
michael@0 265 let params = {
michael@0 266 hostname: loginClone.hostname,
michael@0 267 httpRealm: loginClone.httpRealm,
michael@0 268 formSubmitURL: loginClone.formSubmitURL,
michael@0 269 usernameField: loginClone.usernameField,
michael@0 270 passwordField: loginClone.passwordField,
michael@0 271 encryptedUsername: encUsername,
michael@0 272 encryptedPassword: encPassword,
michael@0 273 guid: loginClone.guid,
michael@0 274 encType: encType,
michael@0 275 timeCreated: loginClone.timeCreated,
michael@0 276 timeLastUsed: loginClone.timeLastUsed,
michael@0 277 timePasswordChanged: loginClone.timePasswordChanged,
michael@0 278 timesUsed: loginClone.timesUsed
michael@0 279 };
michael@0 280
michael@0 281 let stmt;
michael@0 282 try {
michael@0 283 stmt = this._dbCreateStatement(query, params);
michael@0 284 stmt.execute();
michael@0 285 } catch (e) {
michael@0 286 this.log("addLogin failed: " + e.name + " : " + e.message);
michael@0 287 throw "Couldn't write to database, login not added.";
michael@0 288 } finally {
michael@0 289 if (stmt) {
michael@0 290 stmt.reset();
michael@0 291 }
michael@0 292 }
michael@0 293
michael@0 294 // Send a notification that a login was added.
michael@0 295 this._sendNotification("addLogin", loginClone);
michael@0 296 },
michael@0 297
michael@0 298
michael@0 299 /*
michael@0 300 * removeLogin
michael@0 301 *
michael@0 302 */
michael@0 303 removeLogin : function (login) {
michael@0 304 let [idToDelete, storedLogin] = this._getIdForLogin(login);
michael@0 305 if (!idToDelete)
michael@0 306 throw "No matching logins";
michael@0 307
michael@0 308 // Execute the statement & remove from DB
michael@0 309 let query = "DELETE FROM moz_logins WHERE id = :id";
michael@0 310 let params = { id: idToDelete };
michael@0 311 let stmt;
michael@0 312 let transaction = new Transaction(this._dbConnection);
michael@0 313 try {
michael@0 314 stmt = this._dbCreateStatement(query, params);
michael@0 315 stmt.execute();
michael@0 316 this.storeDeletedLogin(storedLogin);
michael@0 317 transaction.commit();
michael@0 318 } catch (e) {
michael@0 319 this.log("_removeLogin failed: " + e.name + " : " + e.message);
michael@0 320 throw "Couldn't write to database, login not removed.";
michael@0 321 transaction.rollback();
michael@0 322 } finally {
michael@0 323 if (stmt) {
michael@0 324 stmt.reset();
michael@0 325 }
michael@0 326 }
michael@0 327 this._sendNotification("removeLogin", storedLogin);
michael@0 328 },
michael@0 329
michael@0 330
michael@0 331 /*
michael@0 332 * modifyLogin
michael@0 333 *
michael@0 334 */
michael@0 335 modifyLogin : function (oldLogin, newLoginData) {
michael@0 336 let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
michael@0 337 if (!idToModify)
michael@0 338 throw "No matching logins";
michael@0 339 oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
michael@0 340
michael@0 341 let newLogin;
michael@0 342 if (newLoginData instanceof Ci.nsILoginInfo) {
michael@0 343 // Clone the existing login to get its nsILoginMetaInfo, then init it
michael@0 344 // with the replacement nsILoginInfo data from the new login.
michael@0 345 newLogin = oldStoredLogin.clone();
michael@0 346 newLogin.init(newLoginData.hostname,
michael@0 347 newLoginData.formSubmitURL, newLoginData.httpRealm,
michael@0 348 newLoginData.username, newLoginData.password,
michael@0 349 newLoginData.usernameField, newLoginData.passwordField);
michael@0 350 newLogin.QueryInterface(Ci.nsILoginMetaInfo);
michael@0 351
michael@0 352 // Automatically update metainfo when password is changed.
michael@0 353 if (newLogin.password != oldLogin.password)
michael@0 354 newLogin.timePasswordChanged = Date.now();
michael@0 355 } else if (newLoginData instanceof Ci.nsIPropertyBag) {
michael@0 356 function _bagHasProperty(aPropName) {
michael@0 357 try {
michael@0 358 newLoginData.getProperty(aPropName);
michael@0 359 return true;
michael@0 360 } catch (e) {
michael@0 361 return false;
michael@0 362 }
michael@0 363 }
michael@0 364
michael@0 365 // Clone the existing login, along with all its properties.
michael@0 366 newLogin = oldStoredLogin.clone();
michael@0 367 newLogin.QueryInterface(Ci.nsILoginMetaInfo);
michael@0 368
michael@0 369 // Automatically update metainfo when password is changed.
michael@0 370 // (Done before the main property updates, lest the caller be
michael@0 371 // explicitly updating both .password and .timePasswordChanged)
michael@0 372 if (_bagHasProperty("password")) {
michael@0 373 let newPassword = newLoginData.getProperty("password");
michael@0 374 if (newPassword != oldLogin.password)
michael@0 375 newLogin.timePasswordChanged = Date.now();
michael@0 376 }
michael@0 377
michael@0 378 let propEnum = newLoginData.enumerator;
michael@0 379 while (propEnum.hasMoreElements()) {
michael@0 380 let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
michael@0 381 switch (prop.name) {
michael@0 382 // nsILoginInfo properties...
michael@0 383 case "hostname":
michael@0 384 case "httpRealm":
michael@0 385 case "formSubmitURL":
michael@0 386 case "username":
michael@0 387 case "password":
michael@0 388 case "usernameField":
michael@0 389 case "passwordField":
michael@0 390 // nsILoginMetaInfo properties...
michael@0 391 case "guid":
michael@0 392 case "timeCreated":
michael@0 393 case "timeLastUsed":
michael@0 394 case "timePasswordChanged":
michael@0 395 case "timesUsed":
michael@0 396 newLogin[prop.name] = prop.value;
michael@0 397 if (prop.name == "guid" && !this._isGuidUnique(newLogin.guid))
michael@0 398 throw "specified GUID already exists";
michael@0 399 break;
michael@0 400
michael@0 401 // Fake property, allows easy incrementing.
michael@0 402 case "timesUsedIncrement":
michael@0 403 newLogin.timesUsed += prop.value;
michael@0 404 break;
michael@0 405
michael@0 406 // Fail if caller requests setting an unknown property.
michael@0 407 default:
michael@0 408 throw "Unexpected propertybag item: " + prop.name;
michael@0 409 }
michael@0 410 }
michael@0 411 } else {
michael@0 412 throw "newLoginData needs an expected interface!";
michael@0 413 }
michael@0 414
michael@0 415 // Throws if there are bogus values.
michael@0 416 this._checkLoginValues(newLogin);
michael@0 417
michael@0 418 // Get the encrypted value of the username and password.
michael@0 419 let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
michael@0 420
michael@0 421 let query =
michael@0 422 "UPDATE moz_logins " +
michael@0 423 "SET hostname = :hostname, " +
michael@0 424 "httpRealm = :httpRealm, " +
michael@0 425 "formSubmitURL = :formSubmitURL, " +
michael@0 426 "usernameField = :usernameField, " +
michael@0 427 "passwordField = :passwordField, " +
michael@0 428 "encryptedUsername = :encryptedUsername, " +
michael@0 429 "encryptedPassword = :encryptedPassword, " +
michael@0 430 "guid = :guid, " +
michael@0 431 "encType = :encType, " +
michael@0 432 "timeCreated = :timeCreated, " +
michael@0 433 "timeLastUsed = :timeLastUsed, " +
michael@0 434 "timePasswordChanged = :timePasswordChanged, " +
michael@0 435 "timesUsed = :timesUsed " +
michael@0 436 "WHERE id = :id";
michael@0 437
michael@0 438 let params = {
michael@0 439 id: idToModify,
michael@0 440 hostname: newLogin.hostname,
michael@0 441 httpRealm: newLogin.httpRealm,
michael@0 442 formSubmitURL: newLogin.formSubmitURL,
michael@0 443 usernameField: newLogin.usernameField,
michael@0 444 passwordField: newLogin.passwordField,
michael@0 445 encryptedUsername: encUsername,
michael@0 446 encryptedPassword: encPassword,
michael@0 447 guid: newLogin.guid,
michael@0 448 encType: encType,
michael@0 449 timeCreated: newLogin.timeCreated,
michael@0 450 timeLastUsed: newLogin.timeLastUsed,
michael@0 451 timePasswordChanged: newLogin.timePasswordChanged,
michael@0 452 timesUsed: newLogin.timesUsed
michael@0 453 };
michael@0 454
michael@0 455 let stmt;
michael@0 456 try {
michael@0 457 stmt = this._dbCreateStatement(query, params);
michael@0 458 stmt.execute();
michael@0 459 } catch (e) {
michael@0 460 this.log("modifyLogin failed: " + e.name + " : " + e.message);
michael@0 461 throw "Couldn't write to database, login not modified.";
michael@0 462 } finally {
michael@0 463 if (stmt) {
michael@0 464 stmt.reset();
michael@0 465 }
michael@0 466 }
michael@0 467
michael@0 468 this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
michael@0 469 },
michael@0 470
michael@0 471
michael@0 472 /*
michael@0 473 * getAllLogins
michael@0 474 *
michael@0 475 * Returns an array of nsILoginInfo.
michael@0 476 */
michael@0 477 getAllLogins : function (count) {
michael@0 478 let [logins, ids] = this._searchLogins({});
michael@0 479
michael@0 480 // decrypt entries for caller.
michael@0 481 logins = this._decryptLogins(logins);
michael@0 482
michael@0 483 this.log("_getAllLogins: returning " + logins.length + " logins.");
michael@0 484 if (count)
michael@0 485 count.value = logins.length; // needed for XPCOM
michael@0 486 return logins;
michael@0 487 },
michael@0 488
michael@0 489
michael@0 490 /*
michael@0 491 * getAllEncryptedLogins
michael@0 492 *
michael@0 493 * Not implemented. This interface was added to extract logins from the
michael@0 494 * legacy storage module without decrypting them. Now that logins are in
michael@0 495 * mozStorage, if the encrypted data is really needed it can be easily
michael@0 496 * obtained with SQL and the mozStorage APIs.
michael@0 497 */
michael@0 498 getAllEncryptedLogins : function (count) {
michael@0 499 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
michael@0 500 },
michael@0 501
michael@0 502
michael@0 503 /*
michael@0 504 * searchLogins
michael@0 505 *
michael@0 506 * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
michael@0 507 * JavaScript object and decrypt the results.
michael@0 508 *
michael@0 509 * Returns an array of decrypted nsILoginInfo.
michael@0 510 */
michael@0 511 searchLogins : function(count, matchData) {
michael@0 512 let realMatchData = {};
michael@0 513 // Convert nsIPropertyBag to normal JS object
michael@0 514 let propEnum = matchData.enumerator;
michael@0 515 while (propEnum.hasMoreElements()) {
michael@0 516 let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
michael@0 517 realMatchData[prop.name] = prop.value;
michael@0 518 }
michael@0 519
michael@0 520 let [logins, ids] = this._searchLogins(realMatchData);
michael@0 521
michael@0 522 // Decrypt entries found for the caller.
michael@0 523 logins = this._decryptLogins(logins);
michael@0 524
michael@0 525 count.value = logins.length; // needed for XPCOM
michael@0 526 return logins;
michael@0 527 },
michael@0 528
michael@0 529
michael@0 530 /*
michael@0 531 * _searchLogins
michael@0 532 *
michael@0 533 * Private method to perform arbitrary searches on any field. Decryption is
michael@0 534 * left to the caller.
michael@0 535 *
michael@0 536 * Returns [logins, ids] for logins that match the arguments, where logins
michael@0 537 * is an array of encrypted nsLoginInfo and ids is an array of associated
michael@0 538 * ids in the database.
michael@0 539 */
michael@0 540 _searchLogins : function (matchData) {
michael@0 541 let conditions = [], params = {};
michael@0 542
michael@0 543 for (let field in matchData) {
michael@0 544 let value = matchData[field];
michael@0 545 switch (field) {
michael@0 546 // Historical compatibility requires this special case
michael@0 547 case "formSubmitURL":
michael@0 548 if (value != null) {
michael@0 549 conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
michael@0 550 params["formSubmitURL"] = value;
michael@0 551 break;
michael@0 552 }
michael@0 553 // Normal cases.
michael@0 554 case "hostname":
michael@0 555 case "httpRealm":
michael@0 556 case "id":
michael@0 557 case "usernameField":
michael@0 558 case "passwordField":
michael@0 559 case "encryptedUsername":
michael@0 560 case "encryptedPassword":
michael@0 561 case "guid":
michael@0 562 case "encType":
michael@0 563 case "timeCreated":
michael@0 564 case "timeLastUsed":
michael@0 565 case "timePasswordChanged":
michael@0 566 case "timesUsed":
michael@0 567 if (value == null) {
michael@0 568 conditions.push(field + " isnull");
michael@0 569 } else {
michael@0 570 conditions.push(field + " = :" + field);
michael@0 571 params[field] = value;
michael@0 572 }
michael@0 573 break;
michael@0 574 // Fail if caller requests an unknown property.
michael@0 575 default:
michael@0 576 throw "Unexpected field: " + field;
michael@0 577 }
michael@0 578 }
michael@0 579
michael@0 580 // Build query
michael@0 581 let query = "SELECT * FROM moz_logins";
michael@0 582 if (conditions.length) {
michael@0 583 conditions = conditions.map(function(c) "(" + c + ")");
michael@0 584 query += " WHERE " + conditions.join(" AND ");
michael@0 585 }
michael@0 586
michael@0 587 let stmt;
michael@0 588 let logins = [], ids = [];
michael@0 589 try {
michael@0 590 stmt = this._dbCreateStatement(query, params);
michael@0 591 // We can't execute as usual here, since we're iterating over rows
michael@0 592 while (stmt.executeStep()) {
michael@0 593 // Create the new nsLoginInfo object, push to array
michael@0 594 let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
michael@0 595 createInstance(Ci.nsILoginInfo);
michael@0 596 login.init(stmt.row.hostname, stmt.row.formSubmitURL,
michael@0 597 stmt.row.httpRealm, stmt.row.encryptedUsername,
michael@0 598 stmt.row.encryptedPassword, stmt.row.usernameField,
michael@0 599 stmt.row.passwordField);
michael@0 600 // set nsILoginMetaInfo values
michael@0 601 login.QueryInterface(Ci.nsILoginMetaInfo);
michael@0 602 login.guid = stmt.row.guid;
michael@0 603 login.timeCreated = stmt.row.timeCreated;
michael@0 604 login.timeLastUsed = stmt.row.timeLastUsed;
michael@0 605 login.timePasswordChanged = stmt.row.timePasswordChanged;
michael@0 606 login.timesUsed = stmt.row.timesUsed;
michael@0 607 logins.push(login);
michael@0 608 ids.push(stmt.row.id);
michael@0 609 }
michael@0 610 } catch (e) {
michael@0 611 this.log("_searchLogins failed: " + e.name + " : " + e.message);
michael@0 612 } finally {
michael@0 613 if (stmt) {
michael@0 614 stmt.reset();
michael@0 615 }
michael@0 616 }
michael@0 617
michael@0 618 this.log("_searchLogins: returning " + logins.length + " logins");
michael@0 619 return [logins, ids];
michael@0 620 },
michael@0 621
michael@0 622 /* storeDeletedLogin
michael@0 623 *
michael@0 624 * Moves a login to the deleted logins table
michael@0 625 *
michael@0 626 */
michael@0 627 storeDeletedLogin : function(aLogin) {
michael@0 628 #ifdef ANDROID
michael@0 629 let stmt = null;
michael@0 630 try {
michael@0 631 this.log("Storing " + aLogin.guid + " in deleted passwords\n");
michael@0 632 let query = "INSERT INTO moz_deleted_logins (guid, timeDeleted) VALUES (:guid, :timeDeleted)";
michael@0 633 let params = { guid: aLogin.guid,
michael@0 634 timeDeleted: Date.now() };
michael@0 635 let stmt = this._dbCreateStatement(query, params);
michael@0 636 stmt.execute();
michael@0 637 } catch(ex) {
michael@0 638 throw ex;
michael@0 639 } finally {
michael@0 640 if (stmt)
michael@0 641 stmt.reset();
michael@0 642 }
michael@0 643 #endif
michael@0 644 },
michael@0 645
michael@0 646
michael@0 647 /*
michael@0 648 * removeAllLogins
michael@0 649 *
michael@0 650 * Removes all logins from storage.
michael@0 651 */
michael@0 652 removeAllLogins : function () {
michael@0 653 this.log("Removing all logins");
michael@0 654 let query;
michael@0 655 let stmt;
michael@0 656 let transaction = new Transaction(this._dbConnection);
michael@0 657
michael@0 658 // Disabled hosts kept, as one presumably doesn't want to erase those.
michael@0 659 // TODO: Add these items to the deleted items table once we've sorted
michael@0 660 // out the issues from bug 756701
michael@0 661 query = "DELETE FROM moz_logins";
michael@0 662 try {
michael@0 663 stmt = this._dbCreateStatement(query);
michael@0 664 stmt.execute();
michael@0 665 transaction.commit();
michael@0 666 } catch (e) {
michael@0 667 this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
michael@0 668 transaction.rollback();
michael@0 669 throw "Couldn't write to database";
michael@0 670 } finally {
michael@0 671 if (stmt) {
michael@0 672 stmt.reset();
michael@0 673 }
michael@0 674 }
michael@0 675
michael@0 676 this._sendNotification("removeAllLogins", null);
michael@0 677 },
michael@0 678
michael@0 679
michael@0 680 /*
michael@0 681 * getAllDisabledHosts
michael@0 682 *
michael@0 683 */
michael@0 684 getAllDisabledHosts : function (count) {
michael@0 685 let disabledHosts = this._queryDisabledHosts(null);
michael@0 686
michael@0 687 this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
michael@0 688 if (count)
michael@0 689 count.value = disabledHosts.length; // needed for XPCOM
michael@0 690 return disabledHosts;
michael@0 691 },
michael@0 692
michael@0 693
michael@0 694 /*
michael@0 695 * getLoginSavingEnabled
michael@0 696 *
michael@0 697 */
michael@0 698 getLoginSavingEnabled : function (hostname) {
michael@0 699 this.log("Getting login saving is enabled for " + hostname);
michael@0 700 return this._queryDisabledHosts(hostname).length == 0
michael@0 701 },
michael@0 702
michael@0 703
michael@0 704 /*
michael@0 705 * setLoginSavingEnabled
michael@0 706 *
michael@0 707 */
michael@0 708 setLoginSavingEnabled : function (hostname, enabled) {
michael@0 709 // Throws if there are bogus values.
michael@0 710 this._checkHostnameValue(hostname);
michael@0 711
michael@0 712 this.log("Setting login saving enabled for " + hostname + " to " + enabled);
michael@0 713 let query;
michael@0 714 if (enabled)
michael@0 715 query = "DELETE FROM moz_disabledHosts " +
michael@0 716 "WHERE hostname = :hostname";
michael@0 717 else
michael@0 718 query = "INSERT INTO moz_disabledHosts " +
michael@0 719 "(hostname) VALUES (:hostname)";
michael@0 720 let params = { hostname: hostname };
michael@0 721
michael@0 722 let stmt
michael@0 723 try {
michael@0 724 stmt = this._dbCreateStatement(query, params);
michael@0 725 stmt.execute();
michael@0 726 } catch (e) {
michael@0 727 this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message);
michael@0 728 throw "Couldn't write to database"
michael@0 729 } finally {
michael@0 730 if (stmt) {
michael@0 731 stmt.reset();
michael@0 732 }
michael@0 733 }
michael@0 734
michael@0 735 this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
michael@0 736 },
michael@0 737
michael@0 738
michael@0 739 /*
michael@0 740 * findLogins
michael@0 741 *
michael@0 742 */
michael@0 743 findLogins : function (count, hostname, formSubmitURL, httpRealm) {
michael@0 744 let loginData = {
michael@0 745 hostname: hostname,
michael@0 746 formSubmitURL: formSubmitURL,
michael@0 747 httpRealm: httpRealm
michael@0 748 };
michael@0 749 let matchData = { };
michael@0 750 for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
michael@0 751 if (loginData[field] != '')
michael@0 752 matchData[field] = loginData[field];
michael@0 753 let [logins, ids] = this._searchLogins(matchData);
michael@0 754
michael@0 755 // Decrypt entries found for the caller.
michael@0 756 logins = this._decryptLogins(logins);
michael@0 757
michael@0 758 this.log("_findLogins: returning " + logins.length + " logins");
michael@0 759 count.value = logins.length; // needed for XPCOM
michael@0 760 return logins;
michael@0 761 },
michael@0 762
michael@0 763
michael@0 764 /*
michael@0 765 * countLogins
michael@0 766 *
michael@0 767 */
michael@0 768 countLogins : function (hostname, formSubmitURL, httpRealm) {
michael@0 769 // Do checks for null and empty strings, adjust conditions and params
michael@0 770 let [conditions, params] =
michael@0 771 this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
michael@0 772
michael@0 773 let query = "SELECT COUNT(1) AS numLogins FROM moz_logins";
michael@0 774 if (conditions.length) {
michael@0 775 conditions = conditions.map(function(c) "(" + c + ")");
michael@0 776 query += " WHERE " + conditions.join(" AND ");
michael@0 777 }
michael@0 778
michael@0 779 let stmt, numLogins;
michael@0 780 try {
michael@0 781 stmt = this._dbCreateStatement(query, params);
michael@0 782 stmt.executeStep();
michael@0 783 numLogins = stmt.row.numLogins;
michael@0 784 } catch (e) {
michael@0 785 this.log("_countLogins failed: " + e.name + " : " + e.message);
michael@0 786 } finally {
michael@0 787 if (stmt) {
michael@0 788 stmt.reset();
michael@0 789 }
michael@0 790 }
michael@0 791
michael@0 792 this.log("_countLogins: counted logins: " + numLogins);
michael@0 793 return numLogins;
michael@0 794 },
michael@0 795
michael@0 796
michael@0 797 /*
michael@0 798 * uiBusy
michael@0 799 */
michael@0 800 get uiBusy() {
michael@0 801 return this._crypto.uiBusy;
michael@0 802 },
michael@0 803
michael@0 804
michael@0 805 /*
michael@0 806 * isLoggedIn
michael@0 807 */
michael@0 808 get isLoggedIn() {
michael@0 809 return this._crypto.isLoggedIn;
michael@0 810 },
michael@0 811
michael@0 812
michael@0 813 /*
michael@0 814 * _sendNotification
michael@0 815 *
michael@0 816 * Send a notification when stored data is changed.
michael@0 817 */
michael@0 818 _sendNotification : function (changeType, data) {
michael@0 819 let dataObject = data;
michael@0 820 // Can't pass a raw JS string or array though notifyObservers(). :-(
michael@0 821 if (data instanceof Array) {
michael@0 822 dataObject = Cc["@mozilla.org/array;1"].
michael@0 823 createInstance(Ci.nsIMutableArray);
michael@0 824 for (let i = 0; i < data.length; i++)
michael@0 825 dataObject.appendElement(data[i], false);
michael@0 826 } else if (typeof(data) == "string") {
michael@0 827 dataObject = Cc["@mozilla.org/supports-string;1"].
michael@0 828 createInstance(Ci.nsISupportsString);
michael@0 829 dataObject.data = data;
michael@0 830 }
michael@0 831 Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
michael@0 832 },
michael@0 833
michael@0 834
michael@0 835 /*
michael@0 836 * _getIdForLogin
michael@0 837 *
michael@0 838 * Returns an array with two items: [id, login]. If the login was not
michael@0 839 * found, both items will be null. The returned login contains the actual
michael@0 840 * stored login (useful for looking at the actual nsILoginMetaInfo values).
michael@0 841 */
michael@0 842 _getIdForLogin : function (login) {
michael@0 843 let matchData = { };
michael@0 844 for each (let field in ["hostname", "formSubmitURL", "httpRealm"])
michael@0 845 if (login[field] != '')
michael@0 846 matchData[field] = login[field];
michael@0 847 let [logins, ids] = this._searchLogins(matchData);
michael@0 848
michael@0 849 let id = null;
michael@0 850 let foundLogin = null;
michael@0 851
michael@0 852 // The specified login isn't encrypted, so we need to ensure
michael@0 853 // the logins we're comparing with are decrypted. We decrypt one entry
michael@0 854 // at a time, lest _decryptLogins return fewer entries and screw up
michael@0 855 // indices between the two.
michael@0 856 for (let i = 0; i < logins.length; i++) {
michael@0 857 let [decryptedLogin] = this._decryptLogins([logins[i]]);
michael@0 858
michael@0 859 if (!decryptedLogin || !decryptedLogin.equals(login))
michael@0 860 continue;
michael@0 861
michael@0 862 // We've found a match, set id and break
michael@0 863 foundLogin = decryptedLogin;
michael@0 864 id = ids[i];
michael@0 865 break;
michael@0 866 }
michael@0 867
michael@0 868 return [id, foundLogin];
michael@0 869 },
michael@0 870
michael@0 871
michael@0 872 /*
michael@0 873 * _queryDisabledHosts
michael@0 874 *
michael@0 875 * Returns an array of hostnames from the database according to the
michael@0 876 * criteria given in the argument. If the argument hostname is null, the
michael@0 877 * result array contains all hostnames
michael@0 878 */
michael@0 879 _queryDisabledHosts : function (hostname) {
michael@0 880 let disabledHosts = [];
michael@0 881
michael@0 882 let query = "SELECT hostname FROM moz_disabledHosts";
michael@0 883 let params = {};
michael@0 884 if (hostname) {
michael@0 885 query += " WHERE hostname = :hostname";
michael@0 886 params = { hostname: hostname };
michael@0 887 }
michael@0 888
michael@0 889 let stmt;
michael@0 890 try {
michael@0 891 stmt = this._dbCreateStatement(query, params);
michael@0 892 while (stmt.executeStep())
michael@0 893 disabledHosts.push(stmt.row.hostname);
michael@0 894 } catch (e) {
michael@0 895 this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message);
michael@0 896 } finally {
michael@0 897 if (stmt) {
michael@0 898 stmt.reset();
michael@0 899 }
michael@0 900 }
michael@0 901
michael@0 902 return disabledHosts;
michael@0 903 },
michael@0 904
michael@0 905
michael@0 906 /*
michael@0 907 * _buildConditionsAndParams
michael@0 908 *
michael@0 909 * Adjusts the WHERE conditions and parameters for statements prior to the
michael@0 910 * statement being created. This fixes the cases where nulls are involved
michael@0 911 * and the empty string is supposed to be a wildcard match
michael@0 912 */
michael@0 913 _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) {
michael@0 914 let conditions = [], params = {};
michael@0 915
michael@0 916 if (hostname == null) {
michael@0 917 conditions.push("hostname isnull");
michael@0 918 } else if (hostname != '') {
michael@0 919 conditions.push("hostname = :hostname");
michael@0 920 params["hostname"] = hostname;
michael@0 921 }
michael@0 922
michael@0 923 if (formSubmitURL == null) {
michael@0 924 conditions.push("formSubmitURL isnull");
michael@0 925 } else if (formSubmitURL != '') {
michael@0 926 conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
michael@0 927 params["formSubmitURL"] = formSubmitURL;
michael@0 928 }
michael@0 929
michael@0 930 if (httpRealm == null) {
michael@0 931 conditions.push("httpRealm isnull");
michael@0 932 } else if (httpRealm != '') {
michael@0 933 conditions.push("httpRealm = :httpRealm");
michael@0 934 params["httpRealm"] = httpRealm;
michael@0 935 }
michael@0 936
michael@0 937 return [conditions, params];
michael@0 938 },
michael@0 939
michael@0 940
michael@0 941 /*
michael@0 942 * _checkLoginValues
michael@0 943 *
michael@0 944 * Due to the way the signons2.txt file is formatted, we need to make
michael@0 945 * sure certain field values or characters do not cause the file to
michael@0 946 * be parse incorrectly. Reject logins that we can't store correctly.
michael@0 947 */
michael@0 948 _checkLoginValues : function (aLogin) {
michael@0 949 function badCharacterPresent(l, c) {
michael@0 950 return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
michael@0 951 (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
michael@0 952 l.hostname.indexOf(c) != -1 ||
michael@0 953 l.usernameField.indexOf(c) != -1 ||
michael@0 954 l.passwordField.indexOf(c) != -1);
michael@0 955 }
michael@0 956
michael@0 957 // Nulls are invalid, as they don't round-trip well.
michael@0 958 // Mostly not a formatting problem, although ".\0" can be quirky.
michael@0 959 if (badCharacterPresent(aLogin, "\0"))
michael@0 960 throw "login values can't contain nulls";
michael@0 961
michael@0 962 // In theory these nulls should just be rolled up into the encrypted
michael@0 963 // values, but nsISecretDecoderRing doesn't use nsStrings, so the
michael@0 964 // nulls cause truncation. Check for them here just to avoid
michael@0 965 // unexpected round-trip surprises.
michael@0 966 if (aLogin.username.indexOf("\0") != -1 ||
michael@0 967 aLogin.password.indexOf("\0") != -1)
michael@0 968 throw "login values can't contain nulls";
michael@0 969
michael@0 970 // Newlines are invalid for any field stored as plaintext.
michael@0 971 if (badCharacterPresent(aLogin, "\r") ||
michael@0 972 badCharacterPresent(aLogin, "\n"))
michael@0 973 throw "login values can't contain newlines";
michael@0 974
michael@0 975 // A line with just a "." can have special meaning.
michael@0 976 if (aLogin.usernameField == "." ||
michael@0 977 aLogin.formSubmitURL == ".")
michael@0 978 throw "login values can't be periods";
michael@0 979
michael@0 980 // A hostname with "\ \(" won't roundtrip.
michael@0 981 // eg host="foo (", realm="bar" --> "foo ( (bar)"
michael@0 982 // vs host="foo", realm=" (bar" --> "foo ( (bar)"
michael@0 983 if (aLogin.hostname.indexOf(" (") != -1)
michael@0 984 throw "bad parens in hostname";
michael@0 985 },
michael@0 986
michael@0 987
michael@0 988 /*
michael@0 989 * _checkHostnameValue
michael@0 990 *
michael@0 991 * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
michael@0 992 * that standard here. Throws on illegal format.
michael@0 993 */
michael@0 994 _checkHostnameValue : function (hostname) {
michael@0 995 // File format prohibits certain values. Also, nulls
michael@0 996 // won't round-trip with getAllDisabledHosts().
michael@0 997 if (hostname == "." ||
michael@0 998 hostname.indexOf("\r") != -1 ||
michael@0 999 hostname.indexOf("\n") != -1 ||
michael@0 1000 hostname.indexOf("\0") != -1)
michael@0 1001 throw "Invalid hostname";
michael@0 1002 },
michael@0 1003
michael@0 1004
michael@0 1005 /*
michael@0 1006 * _isGuidUnique
michael@0 1007 *
michael@0 1008 * Checks to see if the specified GUID already exists.
michael@0 1009 */
michael@0 1010 _isGuidUnique : function (guid) {
michael@0 1011 let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid";
michael@0 1012 let params = { guid: guid };
michael@0 1013
michael@0 1014 let stmt, numLogins;
michael@0 1015 try {
michael@0 1016 stmt = this._dbCreateStatement(query, params);
michael@0 1017 stmt.executeStep();
michael@0 1018 numLogins = stmt.row.numLogins;
michael@0 1019 } catch (e) {
michael@0 1020 this.log("_isGuidUnique failed: " + e.name + " : " + e.message);
michael@0 1021 } finally {
michael@0 1022 if (stmt) {
michael@0 1023 stmt.reset();
michael@0 1024 }
michael@0 1025 }
michael@0 1026
michael@0 1027 return (numLogins == 0);
michael@0 1028 },
michael@0 1029
michael@0 1030
michael@0 1031 /*
michael@0 1032 * _encryptLogin
michael@0 1033 *
michael@0 1034 * Returns the encrypted username, password, and encrypton type for the specified
michael@0 1035 * login. Can throw if the user cancels a master password entry.
michael@0 1036 */
michael@0 1037 _encryptLogin : function (login) {
michael@0 1038 let encUsername = this._crypto.encrypt(login.username);
michael@0 1039 let encPassword = this._crypto.encrypt(login.password);
michael@0 1040 let encType = this._crypto.defaultEncType;
michael@0 1041
michael@0 1042 return [encUsername, encPassword, encType];
michael@0 1043 },
michael@0 1044
michael@0 1045
michael@0 1046 /*
michael@0 1047 * _decryptLogins
michael@0 1048 *
michael@0 1049 * Decrypts username and password fields in the provided array of
michael@0 1050 * logins.
michael@0 1051 *
michael@0 1052 * The entries specified by the array will be decrypted, if possible.
michael@0 1053 * An array of successfully decrypted logins will be returned. The return
michael@0 1054 * value should be given to external callers (since still-encrypted
michael@0 1055 * entries are useless), whereas internal callers generally don't want
michael@0 1056 * to lose unencrypted entries (eg, because the user clicked Cancel
michael@0 1057 * instead of entering their master password)
michael@0 1058 */
michael@0 1059 _decryptLogins : function (logins) {
michael@0 1060 let result = [];
michael@0 1061
michael@0 1062 for each (let login in logins) {
michael@0 1063 try {
michael@0 1064 login.username = this._crypto.decrypt(login.username);
michael@0 1065 login.password = this._crypto.decrypt(login.password);
michael@0 1066 } catch (e) {
michael@0 1067 // If decryption failed (corrupt entry?), just skip it.
michael@0 1068 // Rethrow other errors (like canceling entry of a master pw)
michael@0 1069 if (e.result == Cr.NS_ERROR_FAILURE)
michael@0 1070 continue;
michael@0 1071 throw e;
michael@0 1072 }
michael@0 1073 result.push(login);
michael@0 1074 }
michael@0 1075
michael@0 1076 return result;
michael@0 1077 },
michael@0 1078
michael@0 1079
michael@0 1080 //**************************************************************************//
michael@0 1081 // Database Creation & Access
michael@0 1082
michael@0 1083 /*
michael@0 1084 * _dbCreateStatement
michael@0 1085 *
michael@0 1086 * Creates a statement, wraps it, and then does parameter replacement
michael@0 1087 * Returns the wrapped statement for execution. Will use memoization
michael@0 1088 * so that statements can be reused.
michael@0 1089 */
michael@0 1090 _dbCreateStatement : function (query, params) {
michael@0 1091 let wrappedStmt = this._dbStmts[query];
michael@0 1092 // Memoize the statements
michael@0 1093 if (!wrappedStmt) {
michael@0 1094 this.log("Creating new statement for query: " + query);
michael@0 1095 wrappedStmt = this._dbConnection.createStatement(query);
michael@0 1096 this._dbStmts[query] = wrappedStmt;
michael@0 1097 }
michael@0 1098 // Replace parameters, must be done 1 at a time
michael@0 1099 if (params)
michael@0 1100 for (let i in params)
michael@0 1101 wrappedStmt.params[i] = params[i];
michael@0 1102 return wrappedStmt;
michael@0 1103 },
michael@0 1104
michael@0 1105
michael@0 1106 /*
michael@0 1107 * _dbInit
michael@0 1108 *
michael@0 1109 * Attempts to initialize the database. This creates the file if it doesn't
michael@0 1110 * exist, performs any migrations, etc. Return if this is the first run.
michael@0 1111 */
michael@0 1112 _dbInit : function () {
michael@0 1113 this.log("Initializing Database");
michael@0 1114 let isFirstRun = false;
michael@0 1115 try {
michael@0 1116 this._dbConnection = this._storageService.openDatabase(this._signonsFile);
michael@0 1117 // Get the version of the schema in the file. It will be 0 if the
michael@0 1118 // database has not been created yet.
michael@0 1119 let version = this._dbConnection.schemaVersion;
michael@0 1120 if (version == 0) {
michael@0 1121 this._dbCreate();
michael@0 1122 isFirstRun = true;
michael@0 1123 } else if (version != DB_VERSION) {
michael@0 1124 this._dbMigrate(version);
michael@0 1125 }
michael@0 1126 } catch (e if e.result == Cr.NS_ERROR_FILE_CORRUPTED) {
michael@0 1127 // Database is corrupted, so we backup the database, then throw
michael@0 1128 // causing initialization to fail and a new db to be created next use
michael@0 1129 this._dbCleanup(true);
michael@0 1130 throw e;
michael@0 1131 }
michael@0 1132
michael@0 1133 Services.obs.addObserver(this, "profile-before-change", false);
michael@0 1134 return isFirstRun;
michael@0 1135 },
michael@0 1136
michael@0 1137 observe: function (subject, topic, data) {
michael@0 1138 switch (topic) {
michael@0 1139 case "profile-before-change":
michael@0 1140 Services.obs.removeObserver(this, "profile-before-change");
michael@0 1141 this._dbClose();
michael@0 1142 break;
michael@0 1143 }
michael@0 1144 },
michael@0 1145
michael@0 1146 _dbCreate: function () {
michael@0 1147 this.log("Creating Database");
michael@0 1148 this._dbCreateSchema();
michael@0 1149 this._dbConnection.schemaVersion = DB_VERSION;
michael@0 1150 },
michael@0 1151
michael@0 1152
michael@0 1153 _dbCreateSchema : function () {
michael@0 1154 this._dbCreateTables();
michael@0 1155 this._dbCreateIndices();
michael@0 1156 },
michael@0 1157
michael@0 1158
michael@0 1159 _dbCreateTables : function () {
michael@0 1160 this.log("Creating Tables");
michael@0 1161 for (let name in this._dbSchema.tables)
michael@0 1162 this._dbConnection.createTable(name, this._dbSchema.tables[name]);
michael@0 1163 },
michael@0 1164
michael@0 1165
michael@0 1166 _dbCreateIndices : function () {
michael@0 1167 this.log("Creating Indices");
michael@0 1168 for (let name in this._dbSchema.indices) {
michael@0 1169 let index = this._dbSchema.indices[name];
michael@0 1170 let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
michael@0 1171 "(" + index.columns.join(", ") + ")";
michael@0 1172 this._dbConnection.executeSimpleSQL(statement);
michael@0 1173 }
michael@0 1174 },
michael@0 1175
michael@0 1176
michael@0 1177 _dbMigrate : function (oldVersion) {
michael@0 1178 this.log("Attempting to migrate from version " + oldVersion);
michael@0 1179
michael@0 1180 if (oldVersion > DB_VERSION) {
michael@0 1181 this.log("Downgrading to version " + DB_VERSION);
michael@0 1182 // User's DB is newer. Sanity check that our expected columns are
michael@0 1183 // present, and if so mark the lower version and merrily continue
michael@0 1184 // on. If the columns are borked, something is wrong so blow away
michael@0 1185 // the DB and start from scratch. [Future incompatible upgrades
michael@0 1186 // should swtich to a different table or file.]
michael@0 1187
michael@0 1188 if (!this._dbAreExpectedColumnsPresent())
michael@0 1189 throw Components.Exception("DB is missing expected columns",
michael@0 1190 Cr.NS_ERROR_FILE_CORRUPTED);
michael@0 1191
michael@0 1192 // Change the stored version to the current version. If the user
michael@0 1193 // runs the newer code again, it will see the lower version number
michael@0 1194 // and re-upgrade (to fixup any entries the old code added).
michael@0 1195 this._dbConnection.schemaVersion = DB_VERSION;
michael@0 1196 return;
michael@0 1197 }
michael@0 1198
michael@0 1199 // Upgrade to newer version...
michael@0 1200
michael@0 1201 let transaction = new Transaction(this._dbConnection);
michael@0 1202
michael@0 1203 try {
michael@0 1204 for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
michael@0 1205 this.log("Upgrading to version " + v + "...");
michael@0 1206 let migrateFunction = "_dbMigrateToVersion" + v;
michael@0 1207 this[migrateFunction]();
michael@0 1208 }
michael@0 1209 } catch (e) {
michael@0 1210 this.log("Migration failed: " + e);
michael@0 1211 transaction.rollback();
michael@0 1212 throw e;
michael@0 1213 }
michael@0 1214
michael@0 1215 this._dbConnection.schemaVersion = DB_VERSION;
michael@0 1216 transaction.commit();
michael@0 1217 this.log("DB migration completed.");
michael@0 1218 },
michael@0 1219
michael@0 1220
michael@0 1221 /*
michael@0 1222 * _dbMigrateToVersion2
michael@0 1223 *
michael@0 1224 * Version 2 adds a GUID column. Existing logins are assigned a random GUID.
michael@0 1225 */
michael@0 1226 _dbMigrateToVersion2 : function () {
michael@0 1227 // Check to see if GUID column already exists, add if needed
michael@0 1228 let query;
michael@0 1229 if (!this._dbColumnExists("guid")) {
michael@0 1230 query = "ALTER TABLE moz_logins ADD COLUMN guid TEXT";
michael@0 1231 this._dbConnection.executeSimpleSQL(query);
michael@0 1232
michael@0 1233 query = "CREATE INDEX IF NOT EXISTS moz_logins_guid_index ON moz_logins (guid)";
michael@0 1234 this._dbConnection.executeSimpleSQL(query);
michael@0 1235 }
michael@0 1236
michael@0 1237 // Get a list of IDs for existing logins
michael@0 1238 let ids = [];
michael@0 1239 let query = "SELECT id FROM moz_logins WHERE guid isnull";
michael@0 1240 let stmt;
michael@0 1241 try {
michael@0 1242 stmt = this._dbCreateStatement(query);
michael@0 1243 while (stmt.executeStep())
michael@0 1244 ids.push(stmt.row.id);
michael@0 1245 } catch (e) {
michael@0 1246 this.log("Failed getting IDs: " + e);
michael@0 1247 throw e;
michael@0 1248 } finally {
michael@0 1249 if (stmt) {
michael@0 1250 stmt.reset();
michael@0 1251 }
michael@0 1252 }
michael@0 1253
michael@0 1254 // Generate a GUID for each login and update the DB.
michael@0 1255 query = "UPDATE moz_logins SET guid = :guid WHERE id = :id";
michael@0 1256 for each (let id in ids) {
michael@0 1257 let params = {
michael@0 1258 id: id,
michael@0 1259 guid: this._uuidService.generateUUID().toString()
michael@0 1260 };
michael@0 1261
michael@0 1262 try {
michael@0 1263 stmt = this._dbCreateStatement(query, params);
michael@0 1264 stmt.execute();
michael@0 1265 } catch (e) {
michael@0 1266 this.log("Failed setting GUID: " + e);
michael@0 1267 throw e;
michael@0 1268 } finally {
michael@0 1269 if (stmt) {
michael@0 1270 stmt.reset();
michael@0 1271 }
michael@0 1272 }
michael@0 1273 }
michael@0 1274 },
michael@0 1275
michael@0 1276
michael@0 1277 /*
michael@0 1278 * _dbMigrateToVersion3
michael@0 1279 *
michael@0 1280 * Version 3 adds a encType column.
michael@0 1281 */
michael@0 1282 _dbMigrateToVersion3 : function () {
michael@0 1283 // Check to see if encType column already exists, add if needed
michael@0 1284 let query;
michael@0 1285 if (!this._dbColumnExists("encType")) {
michael@0 1286 query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER";
michael@0 1287 this._dbConnection.executeSimpleSQL(query);
michael@0 1288
michael@0 1289 query = "CREATE INDEX IF NOT EXISTS " +
michael@0 1290 "moz_logins_encType_index ON moz_logins (encType)";
michael@0 1291 this._dbConnection.executeSimpleSQL(query);
michael@0 1292 }
michael@0 1293
michael@0 1294 // Get a list of existing logins
michael@0 1295 let logins = [];
michael@0 1296 let stmt;
michael@0 1297 query = "SELECT id, encryptedUsername, encryptedPassword " +
michael@0 1298 "FROM moz_logins WHERE encType isnull";
michael@0 1299 try {
michael@0 1300 stmt = this._dbCreateStatement(query);
michael@0 1301 while (stmt.executeStep()) {
michael@0 1302 let params = { id: stmt.row.id };
michael@0 1303 // We will tag base64 logins correctly, but no longer support their use.
michael@0 1304 if (stmt.row.encryptedUsername.charAt(0) == '~' ||
michael@0 1305 stmt.row.encryptedPassword.charAt(0) == '~')
michael@0 1306 params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_BASE64;
michael@0 1307 else
michael@0 1308 params.encType = Ci.nsILoginManagerCrypto.ENCTYPE_SDR;
michael@0 1309 logins.push(params);
michael@0 1310 }
michael@0 1311 } catch (e) {
michael@0 1312 this.log("Failed getting logins: " + e);
michael@0 1313 throw e;
michael@0 1314 } finally {
michael@0 1315 if (stmt) {
michael@0 1316 stmt.reset();
michael@0 1317 }
michael@0 1318 }
michael@0 1319
michael@0 1320 // Determine encryption type for each login and update the DB.
michael@0 1321 query = "UPDATE moz_logins SET encType = :encType WHERE id = :id";
michael@0 1322 for each (let params in logins) {
michael@0 1323 try {
michael@0 1324 stmt = this._dbCreateStatement(query, params);
michael@0 1325 stmt.execute();
michael@0 1326 } catch (e) {
michael@0 1327 this.log("Failed setting encType: " + e);
michael@0 1328 throw e;
michael@0 1329 } finally {
michael@0 1330 if (stmt) {
michael@0 1331 stmt.reset();
michael@0 1332 }
michael@0 1333 }
michael@0 1334 }
michael@0 1335 },
michael@0 1336
michael@0 1337
michael@0 1338 /*
michael@0 1339 * _dbMigrateToVersion4
michael@0 1340 *
michael@0 1341 * Version 4 adds timeCreated, timeLastUsed, timePasswordChanged,
michael@0 1342 * and timesUsed columns
michael@0 1343 */
michael@0 1344 _dbMigrateToVersion4 : function () {
michael@0 1345 let query;
michael@0 1346 // Add the new columns, if needed.
michael@0 1347 for each (let column in ["timeCreated", "timeLastUsed", "timePasswordChanged", "timesUsed"]) {
michael@0 1348 if (!this._dbColumnExists(column)) {
michael@0 1349 query = "ALTER TABLE moz_logins ADD COLUMN " + column + " INTEGER";
michael@0 1350 this._dbConnection.executeSimpleSQL(query);
michael@0 1351 }
michael@0 1352 }
michael@0 1353
michael@0 1354 // Get a list of IDs for existing logins.
michael@0 1355 let ids = [];
michael@0 1356 let stmt;
michael@0 1357 query = "SELECT id FROM moz_logins WHERE timeCreated isnull OR " +
michael@0 1358 "timeLastUsed isnull OR timePasswordChanged isnull OR timesUsed isnull";
michael@0 1359 try {
michael@0 1360 stmt = this._dbCreateStatement(query);
michael@0 1361 while (stmt.executeStep())
michael@0 1362 ids.push(stmt.row.id);
michael@0 1363 } catch (e) {
michael@0 1364 this.log("Failed getting IDs: " + e);
michael@0 1365 throw e;
michael@0 1366 } finally {
michael@0 1367 if (stmt) {
michael@0 1368 stmt.reset();
michael@0 1369 }
michael@0 1370 }
michael@0 1371
michael@0 1372 // Initialize logins with current time.
michael@0 1373 query = "UPDATE moz_logins SET timeCreated = :initTime, timeLastUsed = :initTime, " +
michael@0 1374 "timePasswordChanged = :initTime, timesUsed = 1 WHERE id = :id";
michael@0 1375 let params = {
michael@0 1376 id: null,
michael@0 1377 initTime: Date.now()
michael@0 1378 };
michael@0 1379 for each (let id in ids) {
michael@0 1380 params.id = id;
michael@0 1381 try {
michael@0 1382 stmt = this._dbCreateStatement(query, params);
michael@0 1383 stmt.execute();
michael@0 1384 } catch (e) {
michael@0 1385 this.log("Failed setting timestamps: " + e);
michael@0 1386 throw e;
michael@0 1387 } finally {
michael@0 1388 if (stmt) {
michael@0 1389 stmt.reset();
michael@0 1390 }
michael@0 1391 }
michael@0 1392 }
michael@0 1393 },
michael@0 1394
michael@0 1395
michael@0 1396 /*
michael@0 1397 * _dbMigrateToVersion5
michael@0 1398 *
michael@0 1399 * Version 5 adds the moz_deleted_logins table
michael@0 1400 */
michael@0 1401 _dbMigrateToVersion5 : function () {
michael@0 1402 if (!this._dbConnection.tableExists("moz_deleted_logins")) {
michael@0 1403 this._dbConnection.createTable("moz_deleted_logins", this._dbSchema.tables.moz_deleted_logins);
michael@0 1404 }
michael@0 1405 },
michael@0 1406
michael@0 1407 /*
michael@0 1408 * _dbAreExpectedColumnsPresent
michael@0 1409 *
michael@0 1410 * Sanity check to ensure that the columns this version of the code expects
michael@0 1411 * are present in the DB we're using.
michael@0 1412 */
michael@0 1413 _dbAreExpectedColumnsPresent : function () {
michael@0 1414 let query = "SELECT " +
michael@0 1415 "id, " +
michael@0 1416 "hostname, " +
michael@0 1417 "httpRealm, " +
michael@0 1418 "formSubmitURL, " +
michael@0 1419 "usernameField, " +
michael@0 1420 "passwordField, " +
michael@0 1421 "encryptedUsername, " +
michael@0 1422 "encryptedPassword, " +
michael@0 1423 "guid, " +
michael@0 1424 "encType, " +
michael@0 1425 "timeCreated, " +
michael@0 1426 "timeLastUsed, " +
michael@0 1427 "timePasswordChanged, " +
michael@0 1428 "timesUsed " +
michael@0 1429 "FROM moz_logins";
michael@0 1430 try {
michael@0 1431 let stmt = this._dbConnection.createStatement(query);
michael@0 1432 // (no need to execute statement, if it compiled we're good)
michael@0 1433 stmt.finalize();
michael@0 1434 } catch (e) {
michael@0 1435 return false;
michael@0 1436 }
michael@0 1437
michael@0 1438 query = "SELECT " +
michael@0 1439 "id, " +
michael@0 1440 "hostname " +
michael@0 1441 "FROM moz_disabledHosts";
michael@0 1442 try {
michael@0 1443 let stmt = this._dbConnection.createStatement(query);
michael@0 1444 // (no need to execute statement, if it compiled we're good)
michael@0 1445 stmt.finalize();
michael@0 1446 } catch (e) {
michael@0 1447 return false;
michael@0 1448 }
michael@0 1449
michael@0 1450 this.log("verified that expected columns are present in DB.");
michael@0 1451 return true;
michael@0 1452 },
michael@0 1453
michael@0 1454
michael@0 1455 /*
michael@0 1456 * _dbColumnExists
michael@0 1457 *
michael@0 1458 * Checks to see if the named column already exists.
michael@0 1459 */
michael@0 1460 _dbColumnExists : function (columnName) {
michael@0 1461 let query = "SELECT " + columnName + " FROM moz_logins";
michael@0 1462 try {
michael@0 1463 let stmt = this._dbConnection.createStatement(query);
michael@0 1464 // (no need to execute statement, if it compiled we're good)
michael@0 1465 stmt.finalize();
michael@0 1466 return true;
michael@0 1467 } catch (e) {
michael@0 1468 return false;
michael@0 1469 }
michael@0 1470 },
michael@0 1471
michael@0 1472 _dbClose : function () {
michael@0 1473 this.log("Closing the DB connection.");
michael@0 1474 // Finalize all statements to free memory, avoid errors later
michael@0 1475 for each (let stmt in this._dbStmts) {
michael@0 1476 stmt.finalize();
michael@0 1477 }
michael@0 1478 this._dbStmts = {};
michael@0 1479
michael@0 1480 if (this._dbConnection !== null) {
michael@0 1481 try {
michael@0 1482 this._dbConnection.close();
michael@0 1483 } catch (e) {
michael@0 1484 Components.utils.reportError(e);
michael@0 1485 }
michael@0 1486 }
michael@0 1487 this._dbConnection = null;
michael@0 1488 },
michael@0 1489
michael@0 1490 /*
michael@0 1491 * _dbCleanup
michael@0 1492 *
michael@0 1493 * Called when database creation fails. Finalizes database statements,
michael@0 1494 * closes the database connection, deletes the database file.
michael@0 1495 */
michael@0 1496 _dbCleanup : function (backup) {
michael@0 1497 this.log("Cleaning up DB file - close & remove & backup=" + backup)
michael@0 1498
michael@0 1499 // Create backup file
michael@0 1500 if (backup) {
michael@0 1501 let backupFile = this._signonsFile.leafName + ".corrupt";
michael@0 1502 this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
michael@0 1503 }
michael@0 1504
michael@0 1505 this._dbClose();
michael@0 1506 this._signonsFile.remove(false);
michael@0 1507 }
michael@0 1508
michael@0 1509 }; // end of nsLoginManagerStorage_mozStorage implementation
michael@0 1510
michael@0 1511 let component = [LoginManagerStorage_mozStorage];
michael@0 1512 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);

mercurial