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