1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/engines/passwords.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,340 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = ['PasswordEngine', 'LoginRec']; 1.9 + 1.10 +const Cu = Components.utils; 1.11 +const Cc = Components.classes; 1.12 +const Ci = Components.interfaces; 1.13 +const Cr = Components.results; 1.14 + 1.15 +Cu.import("resource://services-sync/record.js"); 1.16 +Cu.import("resource://services-sync/constants.js"); 1.17 +Cu.import("resource://services-sync/engines.js"); 1.18 +Cu.import("resource://services-sync/util.js"); 1.19 + 1.20 +this.LoginRec = function LoginRec(collection, id) { 1.21 + CryptoWrapper.call(this, collection, id); 1.22 +} 1.23 +LoginRec.prototype = { 1.24 + __proto__: CryptoWrapper.prototype, 1.25 + _logName: "Sync.Record.Login", 1.26 +}; 1.27 + 1.28 +Utils.deferGetSet(LoginRec, "cleartext", ["hostname", "formSubmitURL", 1.29 + "httpRealm", "username", "password", "usernameField", "passwordField"]); 1.30 + 1.31 + 1.32 +this.PasswordEngine = function PasswordEngine(service) { 1.33 + SyncEngine.call(this, "Passwords", service); 1.34 +} 1.35 +PasswordEngine.prototype = { 1.36 + __proto__: SyncEngine.prototype, 1.37 + _storeObj: PasswordStore, 1.38 + _trackerObj: PasswordTracker, 1.39 + _recordObj: LoginRec, 1.40 + applyIncomingBatchSize: PASSWORDS_STORE_BATCH_SIZE, 1.41 + 1.42 + get isAllowed() { 1.43 + return Cc["@mozilla.org/weave/service;1"] 1.44 + .getService(Ci.nsISupports) 1.45 + .wrappedJSObject 1.46 + .allowPasswordsEngine; 1.47 + }, 1.48 + 1.49 + get enabled() { 1.50 + // If we are disabled due to !isAllowed(), we must take care to ensure the 1.51 + // engine has actually had the enabled setter called which reflects this state. 1.52 + let prefVal = SyncEngine.prototype.__lookupGetter__("enabled").call(this); 1.53 + let newVal = this.isAllowed && prefVal; 1.54 + if (newVal != prefVal) { 1.55 + this.enabled = newVal; 1.56 + } 1.57 + return newVal; 1.58 + }, 1.59 + 1.60 + set enabled(val) { 1.61 + SyncEngine.prototype.__lookupSetter__("enabled").call(this, this.isAllowed && val); 1.62 + }, 1.63 + 1.64 + _syncFinish: function _syncFinish() { 1.65 + SyncEngine.prototype._syncFinish.call(this); 1.66 + 1.67 + // Delete the weave credentials from the server once 1.68 + if (!Svc.Prefs.get("deletePwdFxA", false)) { 1.69 + try { 1.70 + let ids = []; 1.71 + for (let host of Utils.getSyncCredentialsHosts()) { 1.72 + for (let info of Services.logins.findLogins({}, host, "", "")) { 1.73 + ids.push(info.QueryInterface(Components.interfaces.nsILoginMetaInfo).guid); 1.74 + } 1.75 + } 1.76 + if (ids.length) { 1.77 + let coll = new Collection(this.engineURL, null, this.service); 1.78 + coll.ids = ids; 1.79 + let ret = coll.delete(); 1.80 + this._log.debug("Delete result: " + ret); 1.81 + if (!ret.success && ret.status != 400) { 1.82 + // A non-400 failure means try again next time. 1.83 + return; 1.84 + } 1.85 + } else { 1.86 + this._log.debug("Didn't find any passwords to delete"); 1.87 + } 1.88 + // If there were no ids to delete, or we succeeded, or got a 400, 1.89 + // record success. 1.90 + Svc.Prefs.set("deletePwdFxA", true); 1.91 + Svc.Prefs.reset("deletePwd"); // The old prefname we previously used. 1.92 + } 1.93 + catch(ex) { 1.94 + this._log.debug("Password deletes failed: " + Utils.exceptionStr(ex)); 1.95 + } 1.96 + } 1.97 + }, 1.98 + 1.99 + _findDupe: function _findDupe(item) { 1.100 + let login = this._store._nsLoginInfoFromRecord(item); 1.101 + if (!login) 1.102 + return; 1.103 + 1.104 + let logins = Services.logins.findLogins( 1.105 + {}, login.hostname, login.formSubmitURL, login.httpRealm); 1.106 + this._store._sleep(0); // Yield back to main thread after synchronous operation. 1.107 + 1.108 + // Look for existing logins that match the hostname but ignore the password 1.109 + for each (let local in logins) 1.110 + if (login.matches(local, true) && local instanceof Ci.nsILoginMetaInfo) 1.111 + return local.guid; 1.112 + } 1.113 +}; 1.114 + 1.115 +function PasswordStore(name, engine) { 1.116 + Store.call(this, name, engine); 1.117 + this._nsLoginInfo = new Components.Constructor( 1.118 + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); 1.119 + 1.120 + XPCOMUtils.defineLazyGetter(this, "DBConnection", function() { 1.121 + return Services.logins.QueryInterface(Ci.nsIInterfaceRequestor) 1.122 + .getInterface(Ci.mozIStorageConnection); 1.123 + }); 1.124 +} 1.125 +PasswordStore.prototype = { 1.126 + __proto__: Store.prototype, 1.127 + 1.128 + _nsLoginInfoFromRecord: function PasswordStore__nsLoginInfoRec(record) { 1.129 + if (record.formSubmitURL && 1.130 + record.httpRealm) { 1.131 + this._log.warn("Record " + record.id + 1.132 + " has both formSubmitURL and httpRealm. Skipping."); 1.133 + return null; 1.134 + } 1.135 + 1.136 + // Passing in "undefined" results in an empty string, which later 1.137 + // counts as a value. Explicitly `|| null` these fields according to JS 1.138 + // truthiness. Records with empty strings or null will be unmolested. 1.139 + function nullUndefined(x) (x == undefined) ? null : x; 1.140 + let info = new this._nsLoginInfo(record.hostname, 1.141 + nullUndefined(record.formSubmitURL), 1.142 + nullUndefined(record.httpRealm), 1.143 + record.username, 1.144 + record.password, 1.145 + record.usernameField, 1.146 + record.passwordField); 1.147 + info.QueryInterface(Ci.nsILoginMetaInfo); 1.148 + info.guid = record.id; 1.149 + return info; 1.150 + }, 1.151 + 1.152 + _getLoginFromGUID: function PasswordStore__getLoginFromGUID(id) { 1.153 + let prop = Cc["@mozilla.org/hash-property-bag;1"]. 1.154 + createInstance(Ci.nsIWritablePropertyBag2); 1.155 + prop.setPropertyAsAUTF8String("guid", id); 1.156 + 1.157 + let logins = Services.logins.searchLogins({}, prop); 1.158 + this._sleep(0); // Yield back to main thread after synchronous operation. 1.159 + if (logins.length > 0) { 1.160 + this._log.trace(logins.length + " items matching " + id + " found."); 1.161 + return logins[0]; 1.162 + } else { 1.163 + this._log.trace("No items matching " + id + " found. Ignoring"); 1.164 + } 1.165 + return null; 1.166 + }, 1.167 + 1.168 + applyIncomingBatch: function applyIncomingBatch(records) { 1.169 + if (!this.DBConnection) { 1.170 + return Store.prototype.applyIncomingBatch.call(this, records); 1.171 + } 1.172 + 1.173 + return Utils.runInTransaction(this.DBConnection, function() { 1.174 + return Store.prototype.applyIncomingBatch.call(this, records); 1.175 + }, this); 1.176 + }, 1.177 + 1.178 + applyIncoming: function applyIncoming(record) { 1.179 + Store.prototype.applyIncoming.call(this, record); 1.180 + this._sleep(0); // Yield back to main thread after synchronous operation. 1.181 + }, 1.182 + 1.183 + getAllIDs: function PasswordStore__getAllIDs() { 1.184 + let items = {}; 1.185 + let logins = Services.logins.getAllLogins({}); 1.186 + 1.187 + for (let i = 0; i < logins.length; i++) { 1.188 + // Skip over Weave password/passphrase entries 1.189 + let metaInfo = logins[i].QueryInterface(Ci.nsILoginMetaInfo); 1.190 + if (Utils.getSyncCredentialsHosts().has(metaInfo.hostname)) { 1.191 + continue; 1.192 + } 1.193 + 1.194 + items[metaInfo.guid] = metaInfo; 1.195 + } 1.196 + 1.197 + return items; 1.198 + }, 1.199 + 1.200 + changeItemID: function PasswordStore__changeItemID(oldID, newID) { 1.201 + this._log.trace("Changing item ID: " + oldID + " to " + newID); 1.202 + 1.203 + let oldLogin = this._getLoginFromGUID(oldID); 1.204 + if (!oldLogin) { 1.205 + this._log.trace("Can't change item ID: item doesn't exist"); 1.206 + return; 1.207 + } 1.208 + if (this._getLoginFromGUID(newID)) { 1.209 + this._log.trace("Can't change item ID: new ID already in use"); 1.210 + return; 1.211 + } 1.212 + 1.213 + let prop = Cc["@mozilla.org/hash-property-bag;1"]. 1.214 + createInstance(Ci.nsIWritablePropertyBag2); 1.215 + prop.setPropertyAsAUTF8String("guid", newID); 1.216 + 1.217 + Services.logins.modifyLogin(oldLogin, prop); 1.218 + }, 1.219 + 1.220 + itemExists: function PasswordStore__itemExists(id) { 1.221 + if (this._getLoginFromGUID(id)) 1.222 + return true; 1.223 + return false; 1.224 + }, 1.225 + 1.226 + createRecord: function createRecord(id, collection) { 1.227 + let record = new LoginRec(collection, id); 1.228 + let login = this._getLoginFromGUID(id); 1.229 + 1.230 + if (login) { 1.231 + record.hostname = login.hostname; 1.232 + record.formSubmitURL = login.formSubmitURL; 1.233 + record.httpRealm = login.httpRealm; 1.234 + record.username = login.username; 1.235 + record.password = login.password; 1.236 + record.usernameField = login.usernameField; 1.237 + record.passwordField = login.passwordField; 1.238 + } 1.239 + else 1.240 + record.deleted = true; 1.241 + return record; 1.242 + }, 1.243 + 1.244 + create: function PasswordStore__create(record) { 1.245 + let login = this._nsLoginInfoFromRecord(record); 1.246 + if (!login) 1.247 + return; 1.248 + this._log.debug("Adding login for " + record.hostname); 1.249 + this._log.trace("httpRealm: " + JSON.stringify(login.httpRealm) + "; " + 1.250 + "formSubmitURL: " + JSON.stringify(login.formSubmitURL)); 1.251 + try { 1.252 + Services.logins.addLogin(login); 1.253 + } catch(ex) { 1.254 + this._log.debug("Adding record " + record.id + 1.255 + " resulted in exception " + Utils.exceptionStr(ex)); 1.256 + } 1.257 + }, 1.258 + 1.259 + remove: function PasswordStore__remove(record) { 1.260 + this._log.trace("Removing login " + record.id); 1.261 + 1.262 + let loginItem = this._getLoginFromGUID(record.id); 1.263 + if (!loginItem) { 1.264 + this._log.trace("Asked to remove record that doesn't exist, ignoring"); 1.265 + return; 1.266 + } 1.267 + 1.268 + Services.logins.removeLogin(loginItem); 1.269 + }, 1.270 + 1.271 + update: function PasswordStore__update(record) { 1.272 + let loginItem = this._getLoginFromGUID(record.id); 1.273 + if (!loginItem) { 1.274 + this._log.debug("Skipping update for unknown item: " + record.hostname); 1.275 + return; 1.276 + } 1.277 + 1.278 + this._log.debug("Updating " + record.hostname); 1.279 + let newinfo = this._nsLoginInfoFromRecord(record); 1.280 + if (!newinfo) 1.281 + return; 1.282 + try { 1.283 + Services.logins.modifyLogin(loginItem, newinfo); 1.284 + } catch(ex) { 1.285 + this._log.debug("Modifying record " + record.id + 1.286 + " resulted in exception " + Utils.exceptionStr(ex) + 1.287 + ". Not modifying."); 1.288 + } 1.289 + }, 1.290 + 1.291 + wipe: function PasswordStore_wipe() { 1.292 + Services.logins.removeAllLogins(); 1.293 + } 1.294 +}; 1.295 + 1.296 +function PasswordTracker(name, engine) { 1.297 + Tracker.call(this, name, engine); 1.298 + Svc.Obs.add("weave:engine:start-tracking", this); 1.299 + Svc.Obs.add("weave:engine:stop-tracking", this); 1.300 +} 1.301 +PasswordTracker.prototype = { 1.302 + __proto__: Tracker.prototype, 1.303 + 1.304 + startTracking: function() { 1.305 + Svc.Obs.add("passwordmgr-storage-changed", this); 1.306 + }, 1.307 + 1.308 + stopTracking: function() { 1.309 + Svc.Obs.remove("passwordmgr-storage-changed", this); 1.310 + }, 1.311 + 1.312 + observe: function(subject, topic, data) { 1.313 + Tracker.prototype.observe.call(this, subject, topic, data); 1.314 + 1.315 + if (this.ignoreAll) { 1.316 + return; 1.317 + } 1.318 + 1.319 + // A single add, remove or change or removing all items 1.320 + // will trigger a sync for MULTI_DEVICE. 1.321 + switch (data) { 1.322 + case "modifyLogin": 1.323 + subject = subject.QueryInterface(Ci.nsIArray).queryElementAt(1, Ci.nsILoginMetaInfo); 1.324 + // fallthrough 1.325 + case "addLogin": 1.326 + case "removeLogin": 1.327 + // Skip over Weave password/passphrase changes. 1.328 + subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo); 1.329 + if (Utils.getSyncCredentialsHosts().has(subject.hostname)) { 1.330 + break; 1.331 + } 1.332 + 1.333 + this.score += SCORE_INCREMENT_XLARGE; 1.334 + this._log.trace(data + ": " + subject.guid); 1.335 + this.addChangedID(subject.guid); 1.336 + break; 1.337 + case "removeAllLogins": 1.338 + this._log.trace(data); 1.339 + this.score += SCORE_INCREMENT_XLARGE; 1.340 + break; 1.341 + } 1.342 + } 1.343 +};