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/** 6 * Handles serialization of the data and persistence into a file. 7 * 8 * The file is stored in JSON format, without indentation, using UTF-8 encoding. 9 * With indentation applied, the file would look like this: 10 * 11 * { 12 * "logins": [ 13 * { 14 * "id": 2, 15 * "hostname": "http://www.example.com", 16 * "httpRealm": null, 17 * "formSubmitURL": "http://www.example.com", 18 * "usernameField": "username_field", 19 * "passwordField": "password_field", 20 * "encryptedUsername": "...", 21 * "encryptedPassword": "...", 22 * "guid": "...", 23 * "encType": 1, 24 * "timeCreated": 1262304000000, 25 * "timeLastUsed": 1262304000000, 26 * "timePasswordChanged": 1262476800000, 27 * "timesUsed": 1 28 * }, 29 * { 30 * "id": 4, 31 * (...) 32 * } 33 * ], 34 * "nextId": 10, 35 * "version": 1 36 * } 37 */ 38 39"use strict"; 40 41const EXPORTED_SYMBOLS = ["LoginStore"]; 42 43// Globals 44 45const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 46ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); 47ChromeUtils.defineModuleGetter( 48 this, 49 "JSONFile", 50 "resource://gre/modules/JSONFile.jsm" 51); 52const { XPCOMUtils } = ChromeUtils.import( 53 "resource://gre/modules/XPCOMUtils.jsm" 54); 55 56XPCOMUtils.defineLazyModuleGetters(this, { 57 FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js", 58 FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js", 59}); 60 61/** 62 * Current data version assigned by the code that last touched the data. 63 * 64 * This number should be updated only when it is important to understand whether 65 * an old version of the code has touched the data, for example to execute an 66 * update logic. In most cases, this number should not be changed, in 67 * particular when no special one-time update logic is needed. 68 * 69 * For example, this number should NOT be changed when a new optional field is 70 * added to a login entry. 71 */ 72const kDataVersion = 3; 73 74// The permission type we store in the permission manager. 75const PERMISSION_SAVE_LOGINS = "login-saving"; 76 77const MAX_DATE_MS = 8640000000000000; 78 79// LoginStore 80 81/** 82 * Inherits from JSONFile and handles serialization of login-related data and 83 * persistence into a file. 84 * 85 * @param aPath 86 * String containing the file path where data should be saved. 87 */ 88function LoginStore(aPath, aBackupPath = "") { 89 JSONFile.call(this, { 90 path: aPath, 91 dataPostProcessor: this._dataPostProcessor.bind(this), 92 backupTo: aBackupPath, 93 }); 94} 95 96LoginStore.prototype = Object.create(JSONFile.prototype); 97LoginStore.prototype.constructor = LoginStore; 98 99LoginStore.prototype._save = async function() { 100 await JSONFile.prototype._save.call(this); 101 // Notify tests that writes to the login store is complete. 102 Services.obs.notifyObservers(null, "password-storage-updated"); 103 104 if (this._options.backupTo) { 105 await this._backupHandler(); 106 } 107}; 108 109/** 110 * Delete logins backup file if the last saved login was removed using 111 * removeLogin() or if all logins were removed at once using removeAllUserFacingLogins(). 112 * Note that if the user has a fxa key stored as a login, we just update the 113 * backup to only store the key when the last saved user facing login is removed. 114 */ 115LoginStore.prototype._backupHandler = async function() { 116 const logins = this._data.logins; 117 // Return early if more than one login is stored because the backup won't need 118 // updating in this case. 119 if (logins.length > 1) { 120 return; 121 } 122 123 // If one login is stored and it's a fxa sync key, we update the backup to store the 124 // key only. 125 if ( 126 logins.length && 127 logins[0].hostname == FXA_PWDMGR_HOST && 128 logins[0].httpRealm == FXA_PWDMGR_REALM 129 ) { 130 try { 131 await OS.File.copy(this.path, this._options.backupTo); 132 133 // This notification is specifically sent out for a test. 134 Services.obs.notifyObservers(null, "logins-backup-updated"); 135 } catch (ex) { 136 Cu.reportError(ex); 137 } 138 } else if (!logins.length) { 139 // If no logins are stored anymore, delete backup. 140 await OS.File.remove(this._options.backupTo, { 141 ignoreAbsent: true, 142 }); 143 } 144}; 145 146/** 147 * Synchronously work on the data just loaded into memory. 148 */ 149LoginStore.prototype._dataPostProcessor = function(data) { 150 if (data.nextId === undefined) { 151 data.nextId = 1; 152 } 153 154 // Create any arrays that are not present in the saved file. 155 if (!data.logins) { 156 data.logins = []; 157 } 158 159 if (!data.potentiallyVulnerablePasswords) { 160 data.potentiallyVulnerablePasswords = []; 161 } 162 163 if (!data.dismissedBreachAlertsByLoginGUID) { 164 data.dismissedBreachAlertsByLoginGUID = {}; 165 } 166 167 // sanitize dates in logins 168 if (!("version" in data) || data.version < 3) { 169 let dateProperties = ["timeCreated", "timeLastUsed", "timePasswordChanged"]; 170 let now = Date.now(); 171 function getEarliestDate(login, defaultDate) { 172 let earliestDate = dateProperties.reduce((earliest, pname) => { 173 let ts = login[pname]; 174 return !ts ? earliest : Math.min(ts, earliest); 175 }, defaultDate); 176 return earliestDate; 177 } 178 for (let login of data.logins) { 179 for (let pname of dateProperties) { 180 let earliestDate; 181 if (!login[pname] || login[pname] > MAX_DATE_MS) { 182 login[pname] = 183 earliestDate || (earliestDate = getEarliestDate(login, now)); 184 } 185 } 186 } 187 } 188 189 // Indicate that the current version of the code has touched the file. 190 data.version = kDataVersion; 191 192 return data; 193}; 194