Bug 451155 ? Password manager does not work correctly on IDN site whose name contains...
[wine-gecko.git] / toolkit / components / passwordmgr / src / storage-mozStorage.js
blob2a1514e0e1f7ba67b833eea8953a9336c26c8a7a
1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
12 * License.
14 * The Original Code is mozilla.org code.
16 * The Initial Developer of the Original Code is Mozilla Corporation.
17 * Portions created by the Initial Developer are Copyright (C) 2007
18 * the Initial Developer. All Rights Reserved.
20 * Contributor(s):
21 * Paul O'Shannessy <poshannessy@mozilla.com> (primary author)
22 * Mrinal Kant <mrinal.kant@gmail.com> (original sqlite related changes)
23 * Justin Dolske <dolske@mozilla.com> (encryption/decryption functions are
24 * a lift from Justin's storage-Legacy.js)
26 * Alternatively, the contents of this file may be used under the terms of
27 * either the GNU General Public License Version 2 or later (the "GPL"), or
28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 * in which case the provisions of the GPL or the LGPL are applicable instead
30 * of those above. If you wish to allow use of your version of this file only
31 * under the terms of either the GPL or the LGPL, and not to allow others to
32 * use your version of this file under the terms of the MPL, indicate your
33 * decision by deleting the provisions above and replace them with the notice
34 * and other provisions required by the GPL or the LGPL. If you do not delete
35 * the provisions above, a recipient may use your version of this file under
36 * the terms of any one of the MPL, the GPL or the LGPL.
38 * ***** END LICENSE BLOCK ***** */
41 const Cc = Components.classes;
42 const Ci = Components.interfaces;
44 const DB_VERSION = 1; // The database schema version
46 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
48 function LoginManagerStorage_mozStorage() { };
50 LoginManagerStorage_mozStorage.prototype = {
52 classDescription : "LoginManagerStorage_mozStorage",
53 contractID : "@mozilla.org/login-manager/storage/mozStorage;1",
54 classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
55 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
57 __logService : null, // Console logging service, used for debugging.
58 get _logService() {
59 if (!this.__logService)
60 this.__logService = Cc["@mozilla.org/consoleservice;1"].
61 getService(Ci.nsIConsoleService);
62 return this.__logService;
65 __decoderRing : null, // nsSecretDecoderRing service
66 get _decoderRing() {
67 if (!this.__decoderRing)
68 this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
69 getService(Ci.nsISecretDecoderRing);
70 return this.__decoderRing;
73 __utfConverter : null, // UCS2 <--> UTF8 string conversion
74 get _utfConverter() {
75 if (!this.__utfConverter) {
76 this.__utfConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
77 createInstance(Ci.nsIScriptableUnicodeConverter);
78 this.__utfConverter.charset = "UTF-8";
80 return this.__utfConverter;
83 __profileDir: null, // nsIFile for the user's profile dir
84 get _profileDir() {
85 if (!this.__profileDir)
86 this.__profileDir = Cc["@mozilla.org/file/directory_service;1"].
87 getService(Ci.nsIProperties).
88 get("ProfD", Ci.nsIFile);
89 return this.__profileDir;
92 __storageService: null, // Storage service for using mozStorage
93 get _storageService() {
94 if (!this.__storageService)
95 this.__storageService = Cc["@mozilla.org/storage/service;1"].
96 getService(Ci.mozIStorageService);
97 return this.__storageService;
101 // The current database schema
102 _dbSchema: {
103 tables: {
104 moz_logins: "id INTEGER PRIMARY KEY," +
105 "hostname TEXT NOT NULL," +
106 "httpRealm TEXT," +
107 "formSubmitURL TEXT," +
108 "usernameField TEXT NOT NULL," +
109 "passwordField TEXT NOT NULL," +
110 "encryptedUsername TEXT NOT NULL," +
111 "encryptedPassword TEXT NOT NULL",
113 moz_disabledHosts: "id INTEGER PRIMARY KEY," +
114 "hostname TEXT UNIQUE ON CONFLICT REPLACE",
116 indices: {
117 moz_logins_hostname_index: {
118 table: "moz_logins",
119 columns: ["hostname"]
121 moz_logins_hostname_formSubmitURL_index: {
122 table: "moz_logins",
123 columns: ["hostname", "formSubmitURL"]
125 moz_logins_hostname_httpRealm_index: {
126 table: "moz_logins",
127 columns: ["hostname", "httpRealm"]
131 _dbConnection : null, // The database connection
132 _dbStmts : null, // Database statements for memoization
134 _prefBranch : null, // Preferences service
135 _signonsFile : null, // nsIFile for "signons.sqlite"
136 _importFile : null, // nsIFile for import from legacy
137 _debug : false, // mirrors signon.debug
138 _initialized : false, // have we initialized properly (for import failure)
139 _initializing : false, // prevent concurrent initializations
143 * log
145 * Internal function for logging debug messages to the Error Console.
147 log : function (message) {
148 if (!this._debug)
149 return;
150 dump("PwMgr mozStorage: " + message + "\n");
151 this._logService.logStringMessage("PwMgr mozStorage: " + message);
156 * initWithFile
158 * Initialize the component, but override the default filename locations.
159 * This is primarily used to the unit tests and profile migration.
160 * aImportFile is legacy storage file, aDBFile is a sqlite/mozStorage file.
162 initWithFile : function(aImportFile, aDBFile) {
163 if (aImportFile)
164 this._importFile = aImportFile;
165 if (aDBFile)
166 this._signonsFile = aDBFile;
168 this.init();
173 * init
175 * Initialize this storage component; import from legacy files, if
176 * necessary. Most of the work is done in _deferredInit.
178 init : function () {
179 this._dbStmts = [];
181 // Connect to the correct preferences branch.
182 this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
183 getService(Ci.nsIPrefService);
184 this._prefBranch = this._prefBranch.getBranch("signon.");
185 this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
187 this._debug = this._prefBranch.getBoolPref("debug");
189 // Check to see if the internal PKCS#11 token has been initialized.
190 // If not, set a blank password.
191 let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
192 getService(Ci.nsIPK11TokenDB);
194 let token = tokenDB.getInternalKeyToken();
195 if (token.needsUserInit) {
196 this.log("Initializing key3.db with default blank password.");
197 token.initPassword("");
200 // Most of the real work is done in _deferredInit, which will get
201 // called upon first use of storage
206 * _deferredInit
208 * Tries to initialize the module. Adds protection layer so initialization
209 * from different places will not conflict. Also, ensures that we will try
210 * to import again if import failed, specifically on cancellation of master
211 * password.
213 _deferredInit : function () {
214 let isFirstRun;
215 // Check that we are not already in an initializing state
216 if (this._initializing)
217 throw "Already initializing";
219 // Mark that we are initializing
220 this._initializing = true;
221 try {
222 // If initWithFile is calling us, _signonsFile may already be set.
223 if (!this._signonsFile) {
224 // Initialize signons.sqlite
225 this._signonsFile = this._profileDir.clone();
226 this._signonsFile.append("signons.sqlite");
228 this.log("Opening database at " + this._signonsFile.path);
230 // Initialize the database (create, migrate as necessary)
231 isFirstRun = this._dbInit();
233 // On first run we want to import the default legacy storage files.
234 // Otherwise if passed a file, import from that.
235 if (isFirstRun && !this._importFile)
236 this._importLegacySignons();
237 else if (this._importFile)
238 this._importLegacySignons(this._importFile);
240 this._initialized = true;
241 } catch (e) {
242 this.log("Initialization failed");
243 // If the import fails on first run, we want to delete the db
244 if (isFirstRun && e == "Import failed")
245 this._dbCleanup(false);
246 throw "Initialization failed";
247 } finally {
248 this._initializing = false;
254 * _checkInitializationState
256 * This snippet is needed in all the public methods. It's essentially only
257 * needed when we try to import a legacy file and the user refuses to enter
258 * the master password. We don't want to start saving new info if there is
259 * old info to import. Throws if attempt to initialize fails.
261 _checkInitializationState : function () {
262 if (!this._initialized) {
263 this.log("Trying to initialize.");
264 this._deferredInit();
270 * addLogin
273 addLogin : function (login) {
274 this._checkInitializationState();
275 this._addLogin(login);
280 * _addLogin
282 * Private function wrapping core addLogin functionality.
284 _addLogin : function (login) {
285 // Throws if there are bogus values.
286 this._checkLoginValues(login);
288 let userCanceled, encUsername, encPassword;
289 // Get the encrypted value of the username and password.
290 [encUsername, userCanceled] = this._encrypt(login.username);
291 if (userCanceled)
292 throw "User canceled master password entry, login not added.";
294 [encPassword, userCanceled] = this._encrypt(login.password);
295 // Probably can't hit this case, but for completeness...
296 if (userCanceled)
297 throw "User canceled master password entry, login not added.";
299 let query =
300 "INSERT INTO moz_logins " +
301 "(hostname, httpRealm, formSubmitURL, usernameField, " +
302 "passwordField, encryptedUsername, encryptedPassword) " +
303 "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
304 ":passwordField, :encryptedUsername, :encryptedPassword)";
306 let params = {
307 hostname: login.hostname,
308 httpRealm: login.httpRealm,
309 formSubmitURL: login.formSubmitURL,
310 usernameField: login.usernameField,
311 passwordField: login.passwordField,
312 encryptedUsername: encUsername,
313 encryptedPassword: encPassword
316 let stmt;
317 try {
318 stmt = this._dbCreateStatement(query, params);
319 stmt.execute();
320 } catch (e) {
321 this.log("_addLogin failed: " + e.name + " : " + e.message);
322 throw "Couldn't write to database, login not added.";
323 } finally {
324 stmt.reset();
330 * removeLogin
333 removeLogin : function (login) {
334 this._checkInitializationState();
336 let [logins, ids] =
337 this._searchLogins(login.hostname, login.formSubmitURL, login.httpRealm, false);
338 let idToDelete;
340 // The specified login isn't encrypted, so we need to ensure
341 // the logins we're comparing with are decrypted. We decrypt one entry
342 // at a time, lest _decryptLogins return fewer entries and screw up
343 // indices between the two.
344 for (let i = 0; i < logins.length; i++) {
345 let [[decryptedLogin], userCanceled] =
346 this._decryptLogins([logins[i]]);
348 if (userCanceled)
349 throw "User canceled master password entry, login not removed.";
351 if (!decryptedLogin || !decryptedLogin.equals(login))
352 continue;
354 // We've found a match, set id and break
355 idToDelete = ids[i];
356 break;
359 if (!idToDelete)
360 throw "No matching logins";
362 // Execute the statement & remove from DB
363 let query = "DELETE FROM moz_logins WHERE id = :id";
364 let params = { id: idToDelete };
365 let stmt;
366 try {
367 stmt = this._dbCreateStatement(query, params);
368 stmt.execute();
369 } catch (e) {
370 this.log("_removeLogin failed: " + e.name + " : " + e.message);
371 throw "Couldn't write to database, login not removed.";
372 } finally {
373 stmt.reset();
379 * modifyLogin
382 modifyLogin : function (oldLogin, newLogin) {
383 this._checkInitializationState();
385 // Throws if there are bogus values.
386 this._checkLoginValues(newLogin);
388 // Begin a transaction to wrap remove and add
389 // This will throw if there is a transaction in progress
390 this._dbConnection.beginTransaction();
392 // Wrap add/remove in try-catch so we can rollback on error
393 try {
394 this.removeLogin(oldLogin);
395 this.addLogin(newLogin);
396 } catch (e) {
397 this._dbConnection.rollbackTransaction();
398 throw e;
401 // Commit the transaction
402 this._dbConnection.commitTransaction();
407 * getAllLogins
409 * Returns an array of nsAccountInfo.
411 getAllLogins : function (count) {
412 this._checkInitializationState();
414 let userCanceled;
415 let [logins, ids] = this._queryLogins([], {}, false);
417 // decrypt entries for caller.
418 [logins, userCanceled] = this._decryptLogins(logins);
420 if (userCanceled)
421 throw "User canceled Master Password entry";
423 this.log("_getAllLogins: returning " + logins.length + " logins.");
424 count.value = logins.length; // needed for XPCOM
425 return logins;
430 * removeAllLogins
432 * Removes all logins from storage.
434 removeAllLogins : function () {
435 this._checkInitializationState();
437 this.log("Removing all logins");
438 // Delete any old, unused files.
439 this._removeOldSignonsFiles();
441 // Disabled hosts kept, as one presumably doesn't want to erase those.
442 let query = "DELETE FROM moz_logins";
443 let stmt;
444 try {
445 stmt = this._dbCreateStatement(query);
446 stmt.execute();
447 } catch (e) {
448 this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
449 throw "Couldn't write to database";
450 } finally {
451 stmt.reset();
457 * getAllDisabledHosts
460 getAllDisabledHosts : function (count) {
461 this._checkInitializationState();
463 let disabledHosts = this._queryDisabledHosts(null);
465 this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
466 count.value = disabledHosts.length; // needed for XPCOM
467 return disabledHosts;
472 * getLoginSavingEnabled
475 getLoginSavingEnabled : function (hostname) {
476 this._checkInitializationState();
478 this.log("Getting login saving is enabled for " + hostname);
479 return this._queryDisabledHosts(hostname).length == 0
484 * setLoginSavingEnabled
487 setLoginSavingEnabled : function (hostname, enabled) {
488 this._checkInitializationState();
489 this._setLoginSavingEnabled(hostname, enabled);
494 * _setLoginSavingEnabled
496 * Private function wrapping core setLoginSavingEnabled functionality.
498 _setLoginSavingEnabled : function (hostname, enabled) {
499 // Throws if there are bogus values.
500 this._checkHostnameValue(hostname);
502 this.log("Setting login saving enabled for " + hostname + " to " + enabled);
503 let query;
504 if (enabled)
505 query = "DELETE FROM moz_disabledHosts " +
506 "WHERE hostname = :hostname";
507 else
508 query = "INSERT INTO moz_disabledHosts " +
509 "(hostname) VALUES (:hostname)";
510 let params = { hostname: hostname };
512 let stmt
513 try {
514 stmt = this._dbCreateStatement(query, params);
515 stmt.execute();
516 } catch (e) {
517 this.log("_setLoginSavingEnabled failed: " + e.name + " : " + e.message);
518 throw "Couldn't write to database"
519 } finally {
520 stmt.reset();
526 * findLogins
529 findLogins : function (count, hostname, formSubmitURL, httpRealm) {
530 this._checkInitializationState();
532 let userCanceled;
533 let [logins, ids] = this._searchLogins(hostname, formSubmitURL, httpRealm, false);
535 // Decrypt entries found for the caller.
536 [logins, userCanceled] = this._decryptLogins(logins);
538 // We want to throw in this case, so that the Login Manager
539 // knows to stop processing forms on the page so the user isn't
540 // prompted multiple times.
541 if (userCanceled)
542 throw "User canceled Master Password entry";
544 this.log("_findLogins: returning " + logins.length + " logins");
545 count.value = logins.length; // needed for XPCOM
546 return logins;
551 * countLogins
554 countLogins : function (hostname, formSubmitURL, httpRealm) {
555 this._checkInitializationState();
557 // _searchLogins is returning just ids here
558 let [logins, ids] = this._searchLogins(hostname, formSubmitURL, httpRealm, true);
559 this.log("_countLogins: counted logins: " + ids.length);
560 return ids.length;
565 * _searchLogins
567 * Returns array of [logins, ids]. If countOnly is true, call to
568 * _queryLogins will not instantiate logins
570 _searchLogins : function (hostname, formSubmitURL, httpRealm, countOnly) {
571 let conditions = [];
572 let params = {};
573 // Do checks for null and empty strings, adjust conditions and params
574 if (hostname == null) {
575 conditions.push("hostname isnull");
576 } else if (hostname != '') {
577 conditions.push("hostname = :hostname");
578 params["hostname"] = hostname;
580 if (formSubmitURL == null) {
581 conditions.push("formSubmitURL isnull");
582 } else if (formSubmitURL != '') {
583 conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
584 params["formSubmitURL"] = formSubmitURL;
586 if (httpRealm == null) {
587 conditions.push("httpRealm isnull");
588 } else if (httpRealm != '') {
589 conditions.push("httpRealm = :httpRealm");
590 params["httpRealm"] = httpRealm;
593 return this._queryLogins(conditions, params, countOnly);
598 * _queryLogins
600 * Returns [logins, ids] for logins that match the conditions and params,
601 * where logins is an array of encrypted nsLoginInfo and ids is an array of
602 * associated ids in the database. Conditions are joined with AND. If
603 * countOnly is true, we will not instantiate the login objects, so the
604 * logins array returned will be empty. This saves memory and processing
605 * time when we don't need the logins.
607 _queryLogins : function (conditions, params, countOnly) {
608 let logins = [], ids = [];
610 let query = "SELECT * FROM moz_logins";
611 if (conditions.length) {
612 conditions = conditions.map(function(c) "(" + c + ")");
613 query += " WHERE " + conditions.join(" AND ");
616 let stmt;
617 try {
618 stmt = this._dbCreateStatement(query, params);
619 // We can't execute as usual here, since we're iterating over rows
620 while (stmt.step()) {
621 ids.push(stmt.row.id);
622 if (countOnly)
623 continue;
624 // Create the new nsLoginInfo object, push to array
625 let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
626 createInstance(Ci.nsILoginInfo);
627 login.init(stmt.row.hostname, stmt.row.formSubmitURL,
628 stmt.row.httpRealm, stmt.row.encryptedUsername,
629 stmt.row.encryptedPassword, stmt.row.usernameField,
630 stmt.row.passwordField);
631 logins.push(login);
633 } catch (e) {
634 this.log("_queryLogins failed: " + e.name + " : " + e.message);
635 } finally {
636 stmt.reset();
639 return [logins, ids];
644 * _queryDisabledHosts
646 * Returns an array of hostnames from the database according to the
647 * criteria given in the argument. If the argument hostname is null, the
648 * result array contains all hostnames
650 _queryDisabledHosts : function (hostname) {
651 let disabledHosts = [];
653 let query = "SELECT hostname FROM moz_disabledHosts";
654 let params = {};
655 if (hostname) {
656 query += " WHERE hostname = :hostname";
657 params = { hostname: hostname };
660 let stmt;
661 try {
662 stmt = this._dbCreateStatement(query, params);
663 while (stmt.step())
664 disabledHosts.push(stmt.row.hostname);
665 } catch (e) {
666 this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message);
667 } finally {
668 stmt.reset();
671 return disabledHosts;
676 * _checkLoginValues
678 * Due to the way the signons2.txt file is formatted, we need to make
679 * sure certain field values or characters do not cause the file to
680 * be parse incorrectly. Reject logins that we can't store correctly.
682 _checkLoginValues : function (aLogin) {
683 function badCharacterPresent(l, c) {
684 return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
685 (l.httpRealm && l.httpRealm.indexOf(c) != -1) ||
686 l.hostname.indexOf(c) != -1 ||
687 l.usernameField.indexOf(c) != -1 ||
688 l.passwordField.indexOf(c) != -1);
691 // Nulls are invalid, as they don't round-trip well.
692 // Mostly not a formatting problem, although ".\0" can be quirky.
693 if (badCharacterPresent(aLogin, "\0"))
694 throw "login values can't contain nulls";
696 // In theory these nulls should just be rolled up into the encrypted
697 // values, but nsISecretDecoderRing doesn't use nsStrings, so the
698 // nulls cause truncation. Check for them here just to avoid
699 // unexpected round-trip surprises.
700 if (aLogin.username.indexOf("\0") != -1 ||
701 aLogin.password.indexOf("\0") != -1)
702 throw "login values can't contain nulls";
704 // Newlines are invalid for any field stored as plaintext.
705 if (badCharacterPresent(aLogin, "\r") ||
706 badCharacterPresent(aLogin, "\n"))
707 throw "login values can't contain newlines";
709 // A line with just a "." can have special meaning.
710 if (aLogin.usernameField == "." ||
711 aLogin.formSubmitURL == ".")
712 throw "login values can't be periods";
714 // A hostname with "\ \(" won't roundtrip.
715 // eg host="foo (", realm="bar" --> "foo ( (bar)"
716 // vs host="foo", realm=" (bar" --> "foo ( (bar)"
717 if (aLogin.hostname.indexOf(" (") != -1)
718 throw "bad parens in hostname";
723 * _checkHostnameValue
725 * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
726 * that standard here. Throws on illegal format.
728 _checkHostnameValue : function (hostname) {
729 // File format prohibits certain values. Also, nulls
730 // won't round-trip with getAllDisabledHosts().
731 if (hostname == "." ||
732 hostname.indexOf("\r") != -1 ||
733 hostname.indexOf("\n") != -1 ||
734 hostname.indexOf("\0") != -1)
735 throw "Invalid hostname";
740 * _importLegacySignons
742 * Imports a file that uses Legacy storage. Will use importFile if provided
743 * else it will attempt to initialize the Legacy storage normally.
746 _importLegacySignons : function (importFile) {
747 this.log("Importing " + (importFile ? importFile.path : "legacy storage"));
749 let legacy = Cc["@mozilla.org/login-manager/storage/legacy;1"].
750 createInstance(Ci.nsILoginManagerStorage);
752 // Import all logins and disabled hosts
753 try {
754 if (importFile)
755 legacy.initWithFile(importFile, null);
756 else
757 legacy.init();
759 // Import logins and disabledHosts
760 let logins = legacy.getAllLogins({});
761 for each (let login in logins)
762 this._addLogin(login);
763 let disabledHosts = legacy.getAllDisabledHosts({});
764 for each (let hostname in disabledHosts)
765 this._setLoginSavingEnabled(hostname, false);
766 } catch (e) {
767 this.log("_importLegacySignons failed: " + e.name + " : " + e.message);
768 throw "Import failed";
774 * _removeOldSignonsFiles
776 * Deletes any storage files that we're not using any more.
778 _removeOldSignonsFiles : function () {
779 // We've used a number of prefs over time due to compatibility issues.
780 // We want to delete all files referenced in prefs, which are only for
781 // importing and clearing logins from storage-Legacy.js.
782 filenamePrefs = ["SignonFileName3", "SignonFileName2", "SignonFileName"];
783 for each (let prefname in filenamePrefs) {
784 let filename = this._prefBranch.getCharPref(prefname);
785 let file = this._profileDir.clone();
786 file.append(filename);
788 if (file.exists()) {
789 this.log("Deleting old " + filename + " (" + prefname + ")");
790 try {
791 file.remove(false);
792 } catch (e) {
793 this.log("NOTICE: Couldn't delete " + filename + ": " + e);
801 * _decryptLogins
803 * Decrypts username and password fields in the provided array of
804 * logins.
806 * The entries specified by the array will be decrypted, if possible.
807 * An array of successfully decrypted logins will be returned. The return
808 * value should be given to external callers (since still-encrypted
809 * entries are useless), whereas internal callers generally don't want
810 * to lose unencrypted entries (eg, because the user clicked Cancel
811 * instead of entering their master password)
813 _decryptLogins : function (logins) {
814 let result = [], userCanceled = false;
816 for each (let login in logins) {
817 let decryptedUsername, decryptedPassword;
819 [decryptedUsername, userCanceled] = this._decrypt(login.username);
821 if (userCanceled)
822 break;
824 [decryptedPassword, userCanceled] = this._decrypt(login.password);
826 // Probably can't hit this case, but for completeness...
827 if (userCanceled)
828 break;
830 // If decryption failed (corrupt entry?) skip it.
831 // Note that we allow password-only logins, so username con be "".
832 if (decryptedUsername == null || !decryptedPassword)
833 continue;
835 login.username = decryptedUsername;
836 login.password = decryptedPassword;
838 result.push(login);
841 return [result, userCanceled];
846 * _encrypt
848 * Encrypts the specified string, using the SecretDecoderRing.
850 * Returns [cipherText, userCanceled] where:
851 * cipherText -- the encrypted string, or null if it failed.
852 * userCanceled -- if the encryption failed, this is true if the
853 * user selected Cancel when prompted to enter their
854 * Master Password. The caller should bail out, and not
855 * not request that more things be encrypted (which
856 * results in prompting the user for a Master Password
857 * over and over.)
859 _encrypt : function (plainText) {
860 let cipherText = null, userCanceled = false;
862 try {
863 let plainOctet = this._utfConverter.ConvertFromUnicode(plainText);
864 plainOctet += this._utfConverter.Finish();
865 cipherText = this._decoderRing.encryptString(plainOctet);
866 } catch (e) {
867 this.log("Failed to encrypt string. (" + e.name + ")");
868 // If the user clicks Cancel, we get NS_ERROR_FAILURE.
869 // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
870 if (e.result == Components.results.NS_ERROR_FAILURE)
871 userCanceled = true;
874 return [cipherText, userCanceled];
879 * _decrypt
881 * Decrypts the specified string, using the SecretDecoderRing.
883 * Returns [plainText, userCanceled] where:
884 * plainText -- the decrypted string, or null if it failed.
885 * userCanceled -- if the decryption failed, this is true if the
886 * user selected Cancel when prompted to enter their
887 * Master Password. The caller should bail out, and not
888 * not request that more things be decrypted (which
889 * results in prompting the user for a Master Password
890 * over and over.)
892 _decrypt : function (cipherText) {
893 let plainText = null, userCanceled = false;
895 try {
896 let plainOctet = this._decoderRing.decryptString(cipherText);
897 plainText = this._utfConverter.ConvertToUnicode(plainOctet);
898 } catch (e) {
899 this.log("Failed to decrypt string: " + cipherText +
900 " (" + e.name + ")");
902 // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
903 // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
904 // Wrong passwords are handled by the decoderRing reprompting;
905 // we get no notification.
906 if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE)
907 userCanceled = true;
910 return [plainText, userCanceled];
914 //**************************************************************************//
915 // Database Creation & Access
916 // Hijacked from /toolkit/components/contentprefs/src/nsContentPrefService.js
917 // and modified to fit here. Look there for migration samples.
920 * _dbCreateStatement
922 * Creates a statement, wraps it, and then does parameter replacement
923 * Returns the wrapped statement for execution. Will use memoization
924 * so that statements can be reused.
926 _dbCreateStatement : function (query, params) {
927 // Memoize the statements
928 if (!this._dbStmts[query]) {
929 this.log("Creating new statement for query: " + query);
930 let stmt = this._dbConnection.createStatement(query);
932 let wrappedStmt = Cc["@mozilla.org/storage/statement-wrapper;1"].
933 createInstance(Ci.mozIStorageStatementWrapper);
934 wrappedStmt.initialize(stmt);
935 this._dbStmts[query] = wrappedStmt;
937 // Replace parameters, must be done 1 at a time
938 if (params)
939 for (let i in params)
940 this._dbStmts[query].params[i] = params[i];
941 return this._dbStmts[query];
946 * _dbInit
948 * Attempts to initialize the database. This creates the file if it doesn't
949 * exist, performs any migrations, etc. When database is first created, we
950 * attempt to import legacy signons. Return if this is the first run.
952 _dbInit : function () {
953 this.log("Initializing Database");
954 let isFirstRun = false;
955 try {
956 this._dbConnection = this._storageService.openDatabase(this._signonsFile);
957 // schemaVersion will be 0 if the database has not been created yet
958 if (this._dbConnection.schemaVersion == 0) {
959 this._dbCreate();
960 isFirstRun = true;
961 } else {
962 // Get the version of the schema in the file.
963 let version = this._dbConnection.schemaVersion;
965 // Try to migrate the schema in the database to the current schema used by
966 // the service.
967 if (version != DB_VERSION) {
968 try {
969 this._dbMigrate(version, DB_VERSION);
971 catch (e) {
972 this.log("Migration Failed");
973 throw(e);
977 } catch (e) {
978 // Database is corrupted, so we backup the database, then throw
979 // causing initialization to fail and a new db to be created next use
980 if (e.result == Components.results.NS_ERROR_FILE_CORRUPTED)
981 this._dbCleanup(true);
982 throw e;
983 // TODO handle migration failures
985 return isFirstRun;
989 _dbCreate: function () {
990 this.log("Creating Database");
991 this._dbCreateSchema();
992 this._dbConnection.schemaVersion = DB_VERSION;
996 _dbCreateSchema : function () {
997 this._dbCreateTables();
998 this._dbCreateIndices();
1002 _dbCreateTables : function () {
1003 this.log("Creating Tables");
1004 for (let name in this._dbSchema.tables)
1005 this._dbConnection.createTable(name, this._dbSchema.tables[name]);
1009 _dbCreateIndices : function () {
1010 this.log("Creating Indices");
1011 for (let name in this._dbSchema.indices) {
1012 let index = this._dbSchema.indices[name];
1013 let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
1014 "(" + index.columns.join(", ") + ")";
1015 this._dbConnection.executeSimpleSQL(statement);
1020 _dbMigrate : function (oldVersion, newVersion) {
1021 this.log("Attempting to migrate from v" + oldVersion + "to v" + newVersion);
1022 if (this["_dbMigrate" + oldVersion + "To" + newVersion]) {
1023 this._dbConnection.beginTransaction();
1024 try {
1025 this["_dbMigrate" + oldVersion + "To" + newVersion]();
1026 this._dbConnection.schemaVersion = newVersion;
1027 this._dbConnection.commitTransaction();
1029 catch (e) {
1030 this._dbConnection.rollbackTransaction();
1031 throw e;
1034 else {
1035 throw("no migrator function from version " + oldVersion +
1036 " to version " + newVersion);
1042 * _dbCleanup
1044 * Called when database creation fails. Finalizes database statements,
1045 * closes the database connection, deletes the database file.
1047 _dbCleanup : function (backup) {
1048 this.log("Cleaning up DB file - close & remove & backup=" + backup)
1050 // Create backup file
1051 if (backup) {
1052 let backupFile = this._signonsFile.leafName + ".corrupt";
1053 this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
1056 // Finalize all statements to free memory, avoid errors later
1057 for (let i = 0; i < this._dbStmts.length; i++)
1058 this._dbStmts[i].statement.finalize();
1059 this._dbStmts = [];
1061 // Close the connection, ignore 'already closed' error
1062 try { this._dbConnection.close() } catch(e) {}
1063 this._signonsFile.remove(false);
1066 }; // end of nsLoginManagerStorage_mozStorage implementation
1068 let component = [LoginManagerStorage_mozStorage];
1069 function NSGetModule(compMgr, fileSpec) {
1070 return XPCOMUtils.generateModule(component);