storage-json.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /*
  5. * nsILoginManagerStorage implementation for the JSON back-end.
  6. */
  7. "use strict";
  8. const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
  9. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  10. Cu.import("resource://gre/modules/Services.jsm");
  11. Cu.import("resource://gre/modules/Task.jsm");
  12. XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
  13. "resource://gre/modules/LoginHelper.jsm");
  14. XPCOMUtils.defineLazyModuleGetter(this, "LoginImport",
  15. "resource://gre/modules/LoginImport.jsm");
  16. XPCOMUtils.defineLazyModuleGetter(this, "LoginStore",
  17. "resource://gre/modules/LoginStore.jsm");
  18. XPCOMUtils.defineLazyModuleGetter(this, "OS",
  19. "resource://gre/modules/osfile.jsm");
  20. XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
  21. "@mozilla.org/uuid-generator;1",
  22. "nsIUUIDGenerator");
  23. this.LoginManagerStorage_json = function () {};
  24. this.LoginManagerStorage_json.prototype = {
  25. classID: Components.ID("{c00c432d-a0c9-46d7-bef6-9c45b4d07341}"),
  26. QueryInterface: XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
  27. __crypto: null, // nsILoginManagerCrypto service
  28. get _crypto() {
  29. if (!this.__crypto)
  30. this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].
  31. getService(Ci.nsILoginManagerCrypto);
  32. return this.__crypto;
  33. },
  34. initialize() {
  35. try {
  36. // Force initialization of the crypto module.
  37. // See bug 717490 comment 17.
  38. this._crypto;
  39. // Set the reference to LoginStore synchronously.
  40. let jsonPath = OS.Path.join(OS.Constants.Path.profileDir,
  41. "logins.json");
  42. this._store = new LoginStore(jsonPath);
  43. return Task.spawn(function* () {
  44. // Load the data asynchronously.
  45. this.log("Opening database at", this._store.path);
  46. yield this._store.load();
  47. // The import from previous versions operates the first time
  48. // that this built-in storage back-end is used. This may be
  49. // later than expected, in case add-ons have registered an
  50. // alternate storage that disabled the default one.
  51. try {
  52. if (Services.prefs.getBoolPref("signon.importedFromSqlite")) {
  53. return;
  54. }
  55. } catch (ex) {
  56. // If the preference does not exist, we need to import.
  57. }
  58. // Import only happens asynchronously.
  59. let sqlitePath = OS.Path.join(OS.Constants.Path.profileDir,
  60. "signons.sqlite");
  61. if (yield OS.File.exists(sqlitePath)) {
  62. let loginImport = new LoginImport(this._store, sqlitePath);
  63. // Failures during import, for example due to a corrupt
  64. // file or a schema version that is too old, will not
  65. // prevent us from marking the operation as completed.
  66. // At the next startup, we will not try the import again.
  67. yield loginImport.import().catch(Cu.reportError);
  68. this._store.saveSoon();
  69. }
  70. // We won't attempt import again on next startup.
  71. Services.prefs.setBoolPref("signon.importedFromSqlite", true);
  72. }.bind(this)).catch(Cu.reportError);
  73. } catch (e) {
  74. this.log("Initialization failed:", e);
  75. throw new Error("Initialization failed");
  76. }
  77. },
  78. /**
  79. * Internal method used by regression tests only. It is called before
  80. * replacing this storage module with a new instance.
  81. */
  82. terminate() {
  83. this._store._saver.disarm();
  84. return this._store._save();
  85. },
  86. addLogin(login) {
  87. this._store.ensureDataReady();
  88. // Throws if there are bogus values.
  89. LoginHelper.checkLoginValues(login);
  90. let [encUsername, encPassword, encType] = this._encryptLogin(login);
  91. // Clone the login, so we don't modify the caller's object.
  92. let loginClone = login.clone();
  93. // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
  94. loginClone.QueryInterface(Ci.nsILoginMetaInfo);
  95. if (loginClone.guid) {
  96. if (!this._isGuidUnique(loginClone.guid))
  97. throw new Error("specified GUID already exists");
  98. } else {
  99. loginClone.guid = gUUIDGenerator.generateUUID().toString();
  100. }
  101. // Set timestamps
  102. let currentTime = Date.now();
  103. if (!loginClone.timeCreated)
  104. loginClone.timeCreated = currentTime;
  105. if (!loginClone.timeLastUsed)
  106. loginClone.timeLastUsed = currentTime;
  107. if (!loginClone.timePasswordChanged)
  108. loginClone.timePasswordChanged = currentTime;
  109. if (!loginClone.timesUsed)
  110. loginClone.timesUsed = 1;
  111. this._store.data.logins.push({
  112. id: this._store.data.nextId++,
  113. hostname: loginClone.hostname,
  114. httpRealm: loginClone.httpRealm,
  115. formSubmitURL: loginClone.formSubmitURL,
  116. usernameField: loginClone.usernameField,
  117. passwordField: loginClone.passwordField,
  118. encryptedUsername: encUsername,
  119. encryptedPassword: encPassword,
  120. guid: loginClone.guid,
  121. encType: encType,
  122. timeCreated: loginClone.timeCreated,
  123. timeLastUsed: loginClone.timeLastUsed,
  124. timePasswordChanged: loginClone.timePasswordChanged,
  125. timesUsed: loginClone.timesUsed
  126. });
  127. this._store.saveSoon();
  128. // Send a notification that a login was added.
  129. LoginHelper.notifyStorageChanged("addLogin", loginClone);
  130. return loginClone;
  131. },
  132. removeLogin(login) {
  133. this._store.ensureDataReady();
  134. let [idToDelete, storedLogin] = this._getIdForLogin(login);
  135. if (!idToDelete)
  136. throw new Error("No matching logins");
  137. let foundIndex = this._store.data.logins.findIndex(l => l.id == idToDelete);
  138. if (foundIndex != -1) {
  139. this._store.data.logins.splice(foundIndex, 1);
  140. this._store.saveSoon();
  141. }
  142. LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
  143. },
  144. modifyLogin(oldLogin, newLoginData) {
  145. this._store.ensureDataReady();
  146. let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
  147. if (!idToModify)
  148. throw new Error("No matching logins");
  149. let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
  150. // Check if the new GUID is duplicate.
  151. if (newLogin.guid != oldStoredLogin.guid &&
  152. !this._isGuidUnique(newLogin.guid)) {
  153. throw new Error("specified GUID already exists");
  154. }
  155. // Look for an existing entry in case key properties changed.
  156. if (!newLogin.matches(oldLogin, true)) {
  157. let logins = this.findLogins({}, newLogin.hostname,
  158. newLogin.formSubmitURL,
  159. newLogin.httpRealm);
  160. if (logins.some(login => newLogin.matches(login, true)))
  161. throw new Error("This login already exists.");
  162. }
  163. // Get the encrypted value of the username and password.
  164. let [encUsername, encPassword, encType] = this._encryptLogin(newLogin);
  165. for (let loginItem of this._store.data.logins) {
  166. if (loginItem.id == idToModify) {
  167. loginItem.hostname = newLogin.hostname;
  168. loginItem.httpRealm = newLogin.httpRealm;
  169. loginItem.formSubmitURL = newLogin.formSubmitURL;
  170. loginItem.usernameField = newLogin.usernameField;
  171. loginItem.passwordField = newLogin.passwordField;
  172. loginItem.encryptedUsername = encUsername;
  173. loginItem.encryptedPassword = encPassword;
  174. loginItem.guid = newLogin.guid;
  175. loginItem.encType = encType;
  176. loginItem.timeCreated = newLogin.timeCreated;
  177. loginItem.timeLastUsed = newLogin.timeLastUsed;
  178. loginItem.timePasswordChanged = newLogin.timePasswordChanged;
  179. loginItem.timesUsed = newLogin.timesUsed;
  180. this._store.saveSoon();
  181. break;
  182. }
  183. }
  184. LoginHelper.notifyStorageChanged("modifyLogin", [oldStoredLogin, newLogin]);
  185. },
  186. /**
  187. * @return {nsILoginInfo[]}
  188. */
  189. getAllLogins(count) {
  190. let [logins, ids] = this._searchLogins({});
  191. // decrypt entries for caller.
  192. logins = this._decryptLogins(logins);
  193. this.log("_getAllLogins: returning", logins.length, "logins.");
  194. if (count)
  195. count.value = logins.length; // needed for XPCOM
  196. return logins;
  197. },
  198. /**
  199. * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
  200. * JavaScript object and decrypt the results.
  201. *
  202. * @return {nsILoginInfo[]} which are decrypted.
  203. */
  204. searchLogins(count, matchData) {
  205. let realMatchData = {};
  206. let options = {};
  207. // Convert nsIPropertyBag to normal JS object
  208. let propEnum = matchData.enumerator;
  209. while (propEnum.hasMoreElements()) {
  210. let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
  211. switch (prop.name) {
  212. // Some property names aren't field names but are special options to affect the search.
  213. case "schemeUpgrades": {
  214. options[prop.name] = prop.value;
  215. break;
  216. }
  217. default: {
  218. realMatchData[prop.name] = prop.value;
  219. break;
  220. }
  221. }
  222. }
  223. let [logins, ids] = this._searchLogins(realMatchData, options);
  224. // Decrypt entries found for the caller.
  225. logins = this._decryptLogins(logins);
  226. count.value = logins.length; // needed for XPCOM
  227. return logins;
  228. },
  229. /**
  230. * Private method to perform arbitrary searches on any field. Decryption is
  231. * left to the caller.
  232. *
  233. * Returns [logins, ids] for logins that match the arguments, where logins
  234. * is an array of encrypted nsLoginInfo and ids is an array of associated
  235. * ids in the database.
  236. */
  237. _searchLogins(matchData, aOptions = {
  238. schemeUpgrades: false,
  239. }) {
  240. this._store.ensureDataReady();
  241. function match(aLogin) {
  242. for (let field in matchData) {
  243. let wantedValue = matchData[field];
  244. switch (field) {
  245. case "formSubmitURL":
  246. if (wantedValue != null) {
  247. // Historical compatibility requires this special case
  248. if (aLogin.formSubmitURL == "") {
  249. break;
  250. }
  251. if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
  252. return false;
  253. }
  254. break;
  255. }
  256. // fall through
  257. case "hostname":
  258. if (wantedValue != null) { // needed for formSubmitURL fall through
  259. if (!LoginHelper.isOriginMatching(aLogin[field], wantedValue, aOptions)) {
  260. return false;
  261. }
  262. break;
  263. }
  264. // fall through
  265. // Normal cases.
  266. case "httpRealm":
  267. case "id":
  268. case "usernameField":
  269. case "passwordField":
  270. case "encryptedUsername":
  271. case "encryptedPassword":
  272. case "guid":
  273. case "encType":
  274. case "timeCreated":
  275. case "timeLastUsed":
  276. case "timePasswordChanged":
  277. case "timesUsed":
  278. if (wantedValue == null && aLogin[field]) {
  279. return false;
  280. } else if (aLogin[field] != wantedValue) {
  281. return false;
  282. }
  283. break;
  284. // Fail if caller requests an unknown property.
  285. default:
  286. throw new Error("Unexpected field: " + field);
  287. }
  288. }
  289. return true;
  290. }
  291. let foundLogins = [], foundIds = [];
  292. for (let loginItem of this._store.data.logins) {
  293. if (match(loginItem)) {
  294. // Create the new nsLoginInfo object, push to array
  295. let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
  296. createInstance(Ci.nsILoginInfo);
  297. login.init(loginItem.hostname, loginItem.formSubmitURL,
  298. loginItem.httpRealm, loginItem.encryptedUsername,
  299. loginItem.encryptedPassword, loginItem.usernameField,
  300. loginItem.passwordField);
  301. // set nsILoginMetaInfo values
  302. login.QueryInterface(Ci.nsILoginMetaInfo);
  303. login.guid = loginItem.guid;
  304. login.timeCreated = loginItem.timeCreated;
  305. login.timeLastUsed = loginItem.timeLastUsed;
  306. login.timePasswordChanged = loginItem.timePasswordChanged;
  307. login.timesUsed = loginItem.timesUsed;
  308. foundLogins.push(login);
  309. foundIds.push(loginItem.id);
  310. }
  311. }
  312. this.log("_searchLogins: returning", foundLogins.length, "logins for", matchData,
  313. "with options", aOptions);
  314. return [foundLogins, foundIds];
  315. },
  316. /**
  317. * Removes all logins from storage.
  318. */
  319. removeAllLogins() {
  320. this._store.ensureDataReady();
  321. this.log("Removing all logins");
  322. this._store.data.logins = [];
  323. this._store.saveSoon();
  324. LoginHelper.notifyStorageChanged("removeAllLogins", null);
  325. },
  326. findLogins(count, hostname, formSubmitURL, httpRealm) {
  327. let loginData = {
  328. hostname: hostname,
  329. formSubmitURL: formSubmitURL,
  330. httpRealm: httpRealm
  331. };
  332. let matchData = { };
  333. for (let field of ["hostname", "formSubmitURL", "httpRealm"])
  334. if (loginData[field] != '')
  335. matchData[field] = loginData[field];
  336. let [logins, ids] = this._searchLogins(matchData);
  337. // Decrypt entries found for the caller.
  338. logins = this._decryptLogins(logins);
  339. this.log("_findLogins: returning", logins.length, "logins");
  340. count.value = logins.length; // needed for XPCOM
  341. return logins;
  342. },
  343. countLogins(hostname, formSubmitURL, httpRealm) {
  344. let loginData = {
  345. hostname: hostname,
  346. formSubmitURL: formSubmitURL,
  347. httpRealm: httpRealm
  348. };
  349. let matchData = { };
  350. for (let field of ["hostname", "formSubmitURL", "httpRealm"])
  351. if (loginData[field] != '')
  352. matchData[field] = loginData[field];
  353. let [logins, ids] = this._searchLogins(matchData);
  354. this.log("_countLogins: counted logins:", logins.length);
  355. return logins.length;
  356. },
  357. get uiBusy() {
  358. return this._crypto.uiBusy;
  359. },
  360. get isLoggedIn() {
  361. return this._crypto.isLoggedIn;
  362. },
  363. /**
  364. * Returns an array with two items: [id, login]. If the login was not
  365. * found, both items will be null. The returned login contains the actual
  366. * stored login (useful for looking at the actual nsILoginMetaInfo values).
  367. */
  368. _getIdForLogin(login) {
  369. let matchData = { };
  370. for (let field of ["hostname", "formSubmitURL", "httpRealm"])
  371. if (login[field] != '')
  372. matchData[field] = login[field];
  373. let [logins, ids] = this._searchLogins(matchData);
  374. let id = null;
  375. let foundLogin = null;
  376. // The specified login isn't encrypted, so we need to ensure
  377. // the logins we're comparing with are decrypted. We decrypt one entry
  378. // at a time, lest _decryptLogins return fewer entries and screw up
  379. // indices between the two.
  380. for (let i = 0; i < logins.length; i++) {
  381. let [decryptedLogin] = this._decryptLogins([logins[i]]);
  382. if (!decryptedLogin || !decryptedLogin.equals(login))
  383. continue;
  384. // We've found a match, set id and break
  385. foundLogin = decryptedLogin;
  386. id = ids[i];
  387. break;
  388. }
  389. return [id, foundLogin];
  390. },
  391. /**
  392. * Checks to see if the specified GUID already exists.
  393. */
  394. _isGuidUnique(guid) {
  395. this._store.ensureDataReady();
  396. return this._store.data.logins.every(l => l.guid != guid);
  397. },
  398. /**
  399. * Returns the encrypted username, password, and encrypton type for the specified
  400. * login. Can throw if the user cancels a master password entry.
  401. */
  402. _encryptLogin(login) {
  403. let encUsername = this._crypto.encrypt(login.username);
  404. let encPassword = this._crypto.encrypt(login.password);
  405. let encType = this._crypto.defaultEncType;
  406. return [encUsername, encPassword, encType];
  407. },
  408. /**
  409. * Decrypts username and password fields in the provided array of
  410. * logins.
  411. *
  412. * The entries specified by the array will be decrypted, if possible.
  413. * An array of successfully decrypted logins will be returned. The return
  414. * value should be given to external callers (since still-encrypted
  415. * entries are useless), whereas internal callers generally don't want
  416. * to lose unencrypted entries (eg, because the user clicked Cancel
  417. * instead of entering their master password)
  418. */
  419. _decryptLogins(logins) {
  420. let result = [];
  421. for (let login of logins) {
  422. try {
  423. login.username = this._crypto.decrypt(login.username);
  424. login.password = this._crypto.decrypt(login.password);
  425. } catch (e) {
  426. // If decryption failed (corrupt entry?), just skip it.
  427. // Rethrow other errors (like canceling entry of a master pw)
  428. if (e.result == Cr.NS_ERROR_FAILURE)
  429. continue;
  430. throw e;
  431. }
  432. result.push(login);
  433. }
  434. return result;
  435. },
  436. };
  437. XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_json.prototype, "log", () => {
  438. let logger = LoginHelper.createLogger("Login storage");
  439. return logger.log.bind(logger);
  440. });
  441. this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManagerStorage_json]);