1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/service.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1575 @@ 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 = ["Service"]; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cr = Components.results; 1.13 +const Cu = Components.utils; 1.14 + 1.15 +// How long before refreshing the cluster 1.16 +const CLUSTER_BACKOFF = 5 * 60 * 1000; // 5 minutes 1.17 + 1.18 +// How long a key to generate from an old passphrase. 1.19 +const PBKDF2_KEY_BYTES = 16; 1.20 + 1.21 +const CRYPTO_COLLECTION = "crypto"; 1.22 +const KEYS_WBO = "keys"; 1.23 + 1.24 +Cu.import("resource://gre/modules/Preferences.jsm"); 1.25 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.26 +Cu.import("resource://gre/modules/Log.jsm"); 1.27 +Cu.import("resource://services-common/utils.js"); 1.28 +Cu.import("resource://services-sync/constants.js"); 1.29 +Cu.import("resource://services-sync/engines.js"); 1.30 +Cu.import("resource://services-sync/engines/clients.js"); 1.31 +Cu.import("resource://services-sync/identity.js"); 1.32 +Cu.import("resource://services-sync/policies.js"); 1.33 +Cu.import("resource://services-sync/record.js"); 1.34 +Cu.import("resource://services-sync/resource.js"); 1.35 +Cu.import("resource://services-sync/rest.js"); 1.36 +Cu.import("resource://services-sync/stages/enginesync.js"); 1.37 +Cu.import("resource://services-sync/stages/declined.js"); 1.38 +Cu.import("resource://services-sync/status.js"); 1.39 +Cu.import("resource://services-sync/userapi.js"); 1.40 +Cu.import("resource://services-sync/util.js"); 1.41 + 1.42 +const ENGINE_MODULES = { 1.43 + Addons: "addons.js", 1.44 + Bookmarks: "bookmarks.js", 1.45 + Form: "forms.js", 1.46 + History: "history.js", 1.47 + Password: "passwords.js", 1.48 + Prefs: "prefs.js", 1.49 + Tab: "tabs.js", 1.50 +}; 1.51 + 1.52 +const STORAGE_INFO_TYPES = [INFO_COLLECTIONS, 1.53 + INFO_COLLECTION_USAGE, 1.54 + INFO_COLLECTION_COUNTS, 1.55 + INFO_QUOTA]; 1.56 + 1.57 + 1.58 +function Sync11Service() { 1.59 + this._notify = Utils.notify("weave:service:"); 1.60 +} 1.61 +Sync11Service.prototype = { 1.62 + 1.63 + _lock: Utils.lock, 1.64 + _locked: false, 1.65 + _loggedIn: false, 1.66 + 1.67 + infoURL: null, 1.68 + storageURL: null, 1.69 + metaURL: null, 1.70 + cryptoKeyURL: null, 1.71 + 1.72 + get serverURL() Svc.Prefs.get("serverURL"), 1.73 + set serverURL(value) { 1.74 + if (!value.endsWith("/")) { 1.75 + value += "/"; 1.76 + } 1.77 + 1.78 + // Only do work if it's actually changing 1.79 + if (value == this.serverURL) 1.80 + return; 1.81 + 1.82 + // A new server most likely uses a different cluster, so clear that 1.83 + Svc.Prefs.set("serverURL", value); 1.84 + Svc.Prefs.reset("clusterURL"); 1.85 + }, 1.86 + 1.87 + get clusterURL() Svc.Prefs.get("clusterURL", ""), 1.88 + set clusterURL(value) { 1.89 + Svc.Prefs.set("clusterURL", value); 1.90 + this._updateCachedURLs(); 1.91 + }, 1.92 + 1.93 + get miscAPI() { 1.94 + // Append to the serverURL if it's a relative fragment 1.95 + let misc = Svc.Prefs.get("miscURL"); 1.96 + if (misc.indexOf(":") == -1) 1.97 + misc = this.serverURL + misc; 1.98 + return misc + MISC_API_VERSION + "/"; 1.99 + }, 1.100 + 1.101 + /** 1.102 + * The URI of the User API service. 1.103 + * 1.104 + * This is the base URI of the service as applicable to all users up to 1.105 + * and including the server version path component, complete with trailing 1.106 + * forward slash. 1.107 + */ 1.108 + get userAPIURI() { 1.109 + // Append to the serverURL if it's a relative fragment. 1.110 + let url = Svc.Prefs.get("userURL"); 1.111 + if (!url.contains(":")) { 1.112 + url = this.serverURL + url; 1.113 + } 1.114 + 1.115 + return url + USER_API_VERSION + "/"; 1.116 + }, 1.117 + 1.118 + get pwResetURL() { 1.119 + return this.serverURL + "weave-password-reset"; 1.120 + }, 1.121 + 1.122 + get syncID() { 1.123 + // Generate a random syncID id we don't have one 1.124 + let syncID = Svc.Prefs.get("client.syncID", ""); 1.125 + return syncID == "" ? this.syncID = Utils.makeGUID() : syncID; 1.126 + }, 1.127 + set syncID(value) { 1.128 + Svc.Prefs.set("client.syncID", value); 1.129 + }, 1.130 + 1.131 + get isLoggedIn() { return this._loggedIn; }, 1.132 + 1.133 + get locked() { return this._locked; }, 1.134 + lock: function lock() { 1.135 + if (this._locked) 1.136 + return false; 1.137 + this._locked = true; 1.138 + return true; 1.139 + }, 1.140 + unlock: function unlock() { 1.141 + this._locked = false; 1.142 + }, 1.143 + 1.144 + // A specialized variant of Utils.catch. 1.145 + // This provides a more informative error message when we're already syncing: 1.146 + // see Bug 616568. 1.147 + _catch: function _catch(func) { 1.148 + function lockExceptions(ex) { 1.149 + if (Utils.isLockException(ex)) { 1.150 + // This only happens if we're syncing already. 1.151 + this._log.info("Cannot start sync: already syncing?"); 1.152 + } 1.153 + } 1.154 + 1.155 + return Utils.catch.call(this, func, lockExceptions); 1.156 + }, 1.157 + 1.158 + get userBaseURL() { 1.159 + if (!this._clusterManager) { 1.160 + return null; 1.161 + } 1.162 + return this._clusterManager.getUserBaseURL(); 1.163 + }, 1.164 + 1.165 + _updateCachedURLs: function _updateCachedURLs() { 1.166 + // Nothing to cache yet if we don't have the building blocks 1.167 + if (!this.clusterURL || !this.identity.username) 1.168 + return; 1.169 + 1.170 + this._log.debug("Caching URLs under storage user base: " + this.userBaseURL); 1.171 + 1.172 + // Generate and cache various URLs under the storage API for this user 1.173 + this.infoURL = this.userBaseURL + "info/collections"; 1.174 + this.storageURL = this.userBaseURL + "storage/"; 1.175 + this.metaURL = this.storageURL + "meta/global"; 1.176 + this.cryptoKeysURL = this.storageURL + CRYPTO_COLLECTION + "/" + KEYS_WBO; 1.177 + }, 1.178 + 1.179 + _checkCrypto: function _checkCrypto() { 1.180 + let ok = false; 1.181 + 1.182 + try { 1.183 + let iv = Svc.Crypto.generateRandomIV(); 1.184 + if (iv.length == 24) 1.185 + ok = true; 1.186 + 1.187 + } catch (e) { 1.188 + this._log.debug("Crypto check failed: " + e); 1.189 + } 1.190 + 1.191 + return ok; 1.192 + }, 1.193 + 1.194 + /** 1.195 + * Here is a disgusting yet reasonable way of handling HMAC errors deep in 1.196 + * the guts of Sync. The astute reader will note that this is a hacky way of 1.197 + * implementing something like continuable conditions. 1.198 + * 1.199 + * A handler function is glued to each engine. If the engine discovers an 1.200 + * HMAC failure, we fetch keys from the server and update our keys, just as 1.201 + * we would on startup. 1.202 + * 1.203 + * If our key collection changed, we signal to the engine (via our return 1.204 + * value) that it should retry decryption. 1.205 + * 1.206 + * If our key collection did not change, it means that we already had the 1.207 + * correct keys... and thus a different client has the wrong ones. Reupload 1.208 + * the bundle that we fetched, which will bump the modified time on the 1.209 + * server and (we hope) prompt a broken client to fix itself. 1.210 + * 1.211 + * We keep track of the time at which we last applied this reasoning, because 1.212 + * thrashing doesn't solve anything. We keep a reasonable interval between 1.213 + * these remedial actions. 1.214 + */ 1.215 + lastHMACEvent: 0, 1.216 + 1.217 + /* 1.218 + * Returns whether to try again. 1.219 + */ 1.220 + handleHMACEvent: function handleHMACEvent() { 1.221 + let now = Date.now(); 1.222 + 1.223 + // Leave a sizable delay between HMAC recovery attempts. This gives us 1.224 + // time for another client to fix themselves if we touch the record. 1.225 + if ((now - this.lastHMACEvent) < HMAC_EVENT_INTERVAL) 1.226 + return false; 1.227 + 1.228 + this._log.info("Bad HMAC event detected. Attempting recovery " + 1.229 + "or signaling to other clients."); 1.230 + 1.231 + // Set the last handled time so that we don't act again. 1.232 + this.lastHMACEvent = now; 1.233 + 1.234 + // Fetch keys. 1.235 + let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); 1.236 + try { 1.237 + let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; 1.238 + 1.239 + // Save out the ciphertext for when we reupload. If there's a bug in 1.240 + // CollectionKeyManager, this will prevent us from uploading junk. 1.241 + let cipherText = cryptoKeys.ciphertext; 1.242 + 1.243 + if (!cryptoResp.success) { 1.244 + this._log.warn("Failed to download keys."); 1.245 + return false; 1.246 + } 1.247 + 1.248 + let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle, 1.249 + cryptoKeys, true); 1.250 + if (keysChanged) { 1.251 + // Did they change? If so, carry on. 1.252 + this._log.info("Suggesting retry."); 1.253 + return true; // Try again. 1.254 + } 1.255 + 1.256 + // If not, reupload them and continue the current sync. 1.257 + cryptoKeys.ciphertext = cipherText; 1.258 + cryptoKeys.cleartext = null; 1.259 + 1.260 + let uploadResp = cryptoKeys.upload(this.resource(this.cryptoKeysURL)); 1.261 + if (uploadResp.success) 1.262 + this._log.info("Successfully re-uploaded keys. Continuing sync."); 1.263 + else 1.264 + this._log.warn("Got error response re-uploading keys. " + 1.265 + "Continuing sync; let's try again later."); 1.266 + 1.267 + return false; // Don't try again: same keys. 1.268 + 1.269 + } catch (ex) { 1.270 + this._log.warn("Got exception \"" + ex + "\" fetching and handling " + 1.271 + "crypto keys. Will try again later."); 1.272 + return false; 1.273 + } 1.274 + }, 1.275 + 1.276 + handleFetchedKeys: function handleFetchedKeys(syncKey, cryptoKeys, skipReset) { 1.277 + // Don't want to wipe if we're just starting up! 1.278 + let wasBlank = this.collectionKeys.isClear; 1.279 + let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys); 1.280 + 1.281 + if (keysChanged && !wasBlank) { 1.282 + this._log.debug("Keys changed: " + JSON.stringify(keysChanged)); 1.283 + 1.284 + if (!skipReset) { 1.285 + this._log.info("Resetting client to reflect key change."); 1.286 + 1.287 + if (keysChanged.length) { 1.288 + // Collection keys only. Reset individual engines. 1.289 + this.resetClient(keysChanged); 1.290 + } 1.291 + else { 1.292 + // Default key changed: wipe it all. 1.293 + this.resetClient(); 1.294 + } 1.295 + 1.296 + this._log.info("Downloaded new keys, client reset. Proceeding."); 1.297 + } 1.298 + return true; 1.299 + } 1.300 + return false; 1.301 + }, 1.302 + 1.303 + /** 1.304 + * Prepare to initialize the rest of Weave after waiting a little bit 1.305 + */ 1.306 + onStartup: function onStartup() { 1.307 + this._migratePrefs(); 1.308 + 1.309 + // Status is instantiated before us and is the first to grab an instance of 1.310 + // the IdentityManager. We use that instance because IdentityManager really 1.311 + // needs to be a singleton. Ideally, the longer-lived object would spawn 1.312 + // this service instance. 1.313 + if (!Status || !Status._authManager) { 1.314 + throw new Error("Status or Status._authManager not initialized."); 1.315 + } 1.316 + 1.317 + this.status = Status; 1.318 + this.identity = Status._authManager; 1.319 + this.collectionKeys = new CollectionKeyManager(); 1.320 + 1.321 + this.errorHandler = new ErrorHandler(this); 1.322 + 1.323 + this._log = Log.repository.getLogger("Sync.Service"); 1.324 + this._log.level = 1.325 + Log.Level[Svc.Prefs.get("log.logger.service.main")]; 1.326 + 1.327 + this._log.info("Loading Weave " + WEAVE_VERSION); 1.328 + 1.329 + this._clusterManager = this.identity.createClusterManager(this); 1.330 + this.recordManager = new RecordManager(this); 1.331 + 1.332 + this.enabled = true; 1.333 + 1.334 + this._registerEngines(); 1.335 + 1.336 + let ua = Cc["@mozilla.org/network/protocol;1?name=http"]. 1.337 + getService(Ci.nsIHttpProtocolHandler).userAgent; 1.338 + this._log.info(ua); 1.339 + 1.340 + if (!this._checkCrypto()) { 1.341 + this.enabled = false; 1.342 + this._log.info("Could not load the Weave crypto component. Disabling " + 1.343 + "Weave, since it will not work correctly."); 1.344 + } 1.345 + 1.346 + Svc.Obs.add("weave:service:setup-complete", this); 1.347 + Svc.Prefs.observe("engine.", this); 1.348 + 1.349 + this.scheduler = new SyncScheduler(this); 1.350 + 1.351 + if (!this.enabled) { 1.352 + this._log.info("Firefox Sync disabled."); 1.353 + } 1.354 + 1.355 + this._updateCachedURLs(); 1.356 + 1.357 + let status = this._checkSetup(); 1.358 + if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) { 1.359 + Svc.Obs.notify("weave:engine:start-tracking"); 1.360 + } 1.361 + 1.362 + // Send an event now that Weave service is ready. We don't do this 1.363 + // synchronously so that observers can import this module before 1.364 + // registering an observer. 1.365 + Utils.nextTick(function onNextTick() { 1.366 + this.status.ready = true; 1.367 + 1.368 + // UI code uses the flag on the XPCOM service so it doesn't have 1.369 + // to load a bunch of modules. 1.370 + let xps = Cc["@mozilla.org/weave/service;1"] 1.371 + .getService(Ci.nsISupports) 1.372 + .wrappedJSObject; 1.373 + xps.ready = true; 1.374 + 1.375 + Svc.Obs.notify("weave:service:ready"); 1.376 + }.bind(this)); 1.377 + }, 1.378 + 1.379 + _checkSetup: function _checkSetup() { 1.380 + if (!this.enabled) { 1.381 + return this.status.service = STATUS_DISABLED; 1.382 + } 1.383 + return this.status.checkSetup(); 1.384 + }, 1.385 + 1.386 + _migratePrefs: function _migratePrefs() { 1.387 + // Migrate old debugLog prefs. 1.388 + let logLevel = Svc.Prefs.get("log.appender.debugLog"); 1.389 + if (logLevel) { 1.390 + Svc.Prefs.set("log.appender.file.level", logLevel); 1.391 + Svc.Prefs.reset("log.appender.debugLog"); 1.392 + } 1.393 + if (Svc.Prefs.get("log.appender.debugLog.enabled")) { 1.394 + Svc.Prefs.set("log.appender.file.logOnSuccess", true); 1.395 + Svc.Prefs.reset("log.appender.debugLog.enabled"); 1.396 + } 1.397 + 1.398 + // Migrate old extensions.weave.* prefs if we haven't already tried. 1.399 + if (Svc.Prefs.get("migrated", false)) 1.400 + return; 1.401 + 1.402 + // Grab the list of old pref names 1.403 + let oldPrefBranch = "extensions.weave."; 1.404 + let oldPrefNames = Cc["@mozilla.org/preferences-service;1"]. 1.405 + getService(Ci.nsIPrefService). 1.406 + getBranch(oldPrefBranch). 1.407 + getChildList("", {}); 1.408 + 1.409 + // Map each old pref to the current pref branch 1.410 + let oldPref = new Preferences(oldPrefBranch); 1.411 + for each (let pref in oldPrefNames) 1.412 + Svc.Prefs.set(pref, oldPref.get(pref)); 1.413 + 1.414 + // Remove all the old prefs and remember that we've migrated 1.415 + oldPref.resetBranch(""); 1.416 + Svc.Prefs.set("migrated", true); 1.417 + }, 1.418 + 1.419 + /** 1.420 + * Register the built-in engines for certain applications 1.421 + */ 1.422 + _registerEngines: function _registerEngines() { 1.423 + this.engineManager = new EngineManager(this); 1.424 + 1.425 + let engines = []; 1.426 + // Applications can provide this preference (comma-separated list) 1.427 + // to specify which engines should be registered on startup. 1.428 + let pref = Svc.Prefs.get("registerEngines"); 1.429 + if (pref) { 1.430 + engines = pref.split(","); 1.431 + } 1.432 + 1.433 + let declined = []; 1.434 + pref = Svc.Prefs.get("declinedEngines"); 1.435 + if (pref) { 1.436 + declined = pref.split(","); 1.437 + } 1.438 + 1.439 + this.clientsEngine = new ClientEngine(this); 1.440 + 1.441 + for (let name of engines) { 1.442 + if (!name in ENGINE_MODULES) { 1.443 + this._log.info("Do not know about engine: " + name); 1.444 + continue; 1.445 + } 1.446 + 1.447 + let ns = {}; 1.448 + try { 1.449 + Cu.import("resource://services-sync/engines/" + ENGINE_MODULES[name], ns); 1.450 + 1.451 + let engineName = name + "Engine"; 1.452 + if (!(engineName in ns)) { 1.453 + this._log.warn("Could not find exported engine instance: " + engineName); 1.454 + continue; 1.455 + } 1.456 + 1.457 + this.engineManager.register(ns[engineName]); 1.458 + } catch (ex) { 1.459 + this._log.warn("Could not register engine " + name + ": " + 1.460 + CommonUtils.exceptionStr(ex)); 1.461 + } 1.462 + } 1.463 + 1.464 + this.engineManager.setDeclined(declined); 1.465 + }, 1.466 + 1.467 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, 1.468 + Ci.nsISupportsWeakReference]), 1.469 + 1.470 + // nsIObserver 1.471 + 1.472 + observe: function observe(subject, topic, data) { 1.473 + switch (topic) { 1.474 + case "weave:service:setup-complete": 1.475 + let status = this._checkSetup(); 1.476 + if (status != STATUS_DISABLED && status != CLIENT_NOT_CONFIGURED) 1.477 + Svc.Obs.notify("weave:engine:start-tracking"); 1.478 + break; 1.479 + case "nsPref:changed": 1.480 + if (this._ignorePrefObserver) 1.481 + return; 1.482 + let engine = data.slice((PREFS_BRANCH + "engine.").length); 1.483 + this._handleEngineStatusChanged(engine); 1.484 + break; 1.485 + } 1.486 + }, 1.487 + 1.488 + _handleEngineStatusChanged: function handleEngineDisabled(engine) { 1.489 + this._log.trace("Status for " + engine + " engine changed."); 1.490 + if (Svc.Prefs.get("engineStatusChanged." + engine, false)) { 1.491 + // The enabled status being changed back to what it was before. 1.492 + Svc.Prefs.reset("engineStatusChanged." + engine); 1.493 + } else { 1.494 + // Remember that the engine status changed locally until the next sync. 1.495 + Svc.Prefs.set("engineStatusChanged." + engine, true); 1.496 + } 1.497 + }, 1.498 + 1.499 + /** 1.500 + * Obtain a Resource instance with authentication credentials. 1.501 + */ 1.502 + resource: function resource(url) { 1.503 + let res = new Resource(url); 1.504 + res.authenticator = this.identity.getResourceAuthenticator(); 1.505 + 1.506 + return res; 1.507 + }, 1.508 + 1.509 + /** 1.510 + * Obtain a SyncStorageRequest instance with authentication credentials. 1.511 + */ 1.512 + getStorageRequest: function getStorageRequest(url) { 1.513 + let request = new SyncStorageRequest(url); 1.514 + request.authenticator = this.identity.getRESTRequestAuthenticator(); 1.515 + 1.516 + return request; 1.517 + }, 1.518 + 1.519 + /** 1.520 + * Perform the info fetch as part of a login or key fetch, or 1.521 + * inside engine sync. 1.522 + */ 1.523 + _fetchInfo: function (url) { 1.524 + let infoURL = url || this.infoURL; 1.525 + 1.526 + this._log.trace("In _fetchInfo: " + infoURL); 1.527 + let info; 1.528 + try { 1.529 + info = this.resource(infoURL).get(); 1.530 + } catch (ex) { 1.531 + this.errorHandler.checkServerError(ex); 1.532 + throw ex; 1.533 + } 1.534 + 1.535 + // Always check for errors; this is also where we look for X-Weave-Alert. 1.536 + this.errorHandler.checkServerError(info); 1.537 + if (!info.success) { 1.538 + throw "Aborting sync: failed to get collections."; 1.539 + } 1.540 + return info; 1.541 + }, 1.542 + 1.543 + verifyAndFetchSymmetricKeys: function verifyAndFetchSymmetricKeys(infoResponse) { 1.544 + 1.545 + this._log.debug("Fetching and verifying -- or generating -- symmetric keys."); 1.546 + 1.547 + // Don't allow empty/missing passphrase. 1.548 + // Furthermore, we assume that our sync key is already upgraded, 1.549 + // and fail if that assumption is invalidated. 1.550 + 1.551 + if (!this.identity.syncKey) { 1.552 + this.status.login = LOGIN_FAILED_NO_PASSPHRASE; 1.553 + this.status.sync = CREDENTIALS_CHANGED; 1.554 + return false; 1.555 + } 1.556 + 1.557 + let syncKeyBundle = this.identity.syncKeyBundle; 1.558 + if (!syncKeyBundle) { 1.559 + this._log.error("Sync Key Bundle not set. Invalid Sync Key?"); 1.560 + 1.561 + this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; 1.562 + this.status.sync = CREDENTIALS_CHANGED; 1.563 + return false; 1.564 + } 1.565 + 1.566 + try { 1.567 + if (!infoResponse) 1.568 + infoResponse = this._fetchInfo(); // Will throw an exception on failure. 1.569 + 1.570 + // This only applies when the server is already at version 4. 1.571 + if (infoResponse.status != 200) { 1.572 + this._log.warn("info/collections returned non-200 response. Failing key fetch."); 1.573 + this.status.login = LOGIN_FAILED_SERVER_ERROR; 1.574 + this.errorHandler.checkServerError(infoResponse); 1.575 + return false; 1.576 + } 1.577 + 1.578 + let infoCollections = infoResponse.obj; 1.579 + 1.580 + this._log.info("Testing info/collections: " + JSON.stringify(infoCollections)); 1.581 + 1.582 + if (this.collectionKeys.updateNeeded(infoCollections)) { 1.583 + this._log.info("collection keys reports that a key update is needed."); 1.584 + 1.585 + // Don't always set to CREDENTIALS_CHANGED -- we will probably take care of this. 1.586 + 1.587 + // Fetch storage/crypto/keys. 1.588 + let cryptoKeys; 1.589 + 1.590 + if (infoCollections && (CRYPTO_COLLECTION in infoCollections)) { 1.591 + try { 1.592 + cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); 1.593 + let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; 1.594 + 1.595 + if (cryptoResp.success) { 1.596 + let keysChanged = this.handleFetchedKeys(syncKeyBundle, cryptoKeys); 1.597 + return true; 1.598 + } 1.599 + else if (cryptoResp.status == 404) { 1.600 + // On failure, ask to generate new keys and upload them. 1.601 + // Fall through to the behavior below. 1.602 + this._log.warn("Got 404 for crypto/keys, but 'crypto' in info/collections. Regenerating."); 1.603 + cryptoKeys = null; 1.604 + } 1.605 + else { 1.606 + // Some other problem. 1.607 + this.status.login = LOGIN_FAILED_SERVER_ERROR; 1.608 + this.errorHandler.checkServerError(cryptoResp); 1.609 + this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys."); 1.610 + return false; 1.611 + } 1.612 + } 1.613 + catch (ex) { 1.614 + this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys."); 1.615 + // TODO: Um, what exceptions might we get here? Should we re-throw any? 1.616 + 1.617 + // One kind of exception: HMAC failure. 1.618 + if (Utils.isHMACMismatch(ex)) { 1.619 + this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE; 1.620 + this.status.sync = CREDENTIALS_CHANGED; 1.621 + } 1.622 + else { 1.623 + // In the absence of further disambiguation or more precise 1.624 + // failure constants, just report failure. 1.625 + this.status.login = LOGIN_FAILED; 1.626 + } 1.627 + return false; 1.628 + } 1.629 + } 1.630 + else { 1.631 + this._log.info("... 'crypto' is not a reported collection. Generating new keys."); 1.632 + } 1.633 + 1.634 + if (!cryptoKeys) { 1.635 + this._log.info("No keys! Generating new ones."); 1.636 + 1.637 + // Better make some and upload them, and wipe the server to ensure 1.638 + // consistency. This is all achieved via _freshStart. 1.639 + // If _freshStart fails to clear the server or upload keys, it will 1.640 + // throw. 1.641 + this._freshStart(); 1.642 + return true; 1.643 + } 1.644 + 1.645 + // Last-ditch case. 1.646 + return false; 1.647 + } 1.648 + else { 1.649 + // No update needed: we're good! 1.650 + return true; 1.651 + } 1.652 + 1.653 + } catch (ex) { 1.654 + // This means no keys are present, or there's a network error. 1.655 + this._log.debug("Failed to fetch and verify keys: " 1.656 + + Utils.exceptionStr(ex)); 1.657 + this.errorHandler.checkServerError(ex); 1.658 + return false; 1.659 + } 1.660 + }, 1.661 + 1.662 + verifyLogin: function verifyLogin(allow40XRecovery = true) { 1.663 + // If the identity isn't ready it might not know the username... 1.664 + if (!this.identity.readyToAuthenticate) { 1.665 + this._log.info("Not ready to authenticate in verifyLogin."); 1.666 + this.status.login = LOGIN_FAILED_NOT_READY; 1.667 + return false; 1.668 + } 1.669 + 1.670 + if (!this.identity.username) { 1.671 + this._log.warn("No username in verifyLogin."); 1.672 + this.status.login = LOGIN_FAILED_NO_USERNAME; 1.673 + return false; 1.674 + } 1.675 + 1.676 + // Unlock master password, or return. 1.677 + // Attaching auth credentials to a request requires access to 1.678 + // passwords, which means that Resource.get can throw MP-related 1.679 + // exceptions! 1.680 + // Try to fetch the passphrase first, while we still have control. 1.681 + try { 1.682 + this.identity.syncKey; 1.683 + } catch (ex) { 1.684 + this._log.debug("Fetching passphrase threw " + ex + 1.685 + "; assuming master password locked."); 1.686 + this.status.login = MASTER_PASSWORD_LOCKED; 1.687 + return false; 1.688 + } 1.689 + 1.690 + try { 1.691 + // Make sure we have a cluster to verify against. 1.692 + // This is a little weird, if we don't get a node we pretend 1.693 + // to succeed, since that probably means we just don't have storage. 1.694 + if (this.clusterURL == "" && !this._clusterManager.setCluster()) { 1.695 + this.status.sync = NO_SYNC_NODE_FOUND; 1.696 + return true; 1.697 + } 1.698 + 1.699 + // Fetch collection info on every startup. 1.700 + let test = this.resource(this.infoURL).get(); 1.701 + 1.702 + switch (test.status) { 1.703 + case 200: 1.704 + // The user is authenticated. 1.705 + 1.706 + // We have no way of verifying the passphrase right now, 1.707 + // so wait until remoteSetup to do so. 1.708 + // Just make the most trivial checks. 1.709 + if (!this.identity.syncKey) { 1.710 + this._log.warn("No passphrase in verifyLogin."); 1.711 + this.status.login = LOGIN_FAILED_NO_PASSPHRASE; 1.712 + return false; 1.713 + } 1.714 + 1.715 + // Go ahead and do remote setup, so that we can determine 1.716 + // conclusively that our passphrase is correct. 1.717 + if (this._remoteSetup()) { 1.718 + // Username/password verified. 1.719 + this.status.login = LOGIN_SUCCEEDED; 1.720 + return true; 1.721 + } 1.722 + 1.723 + this._log.warn("Remote setup failed."); 1.724 + // Remote setup must have failed. 1.725 + return false; 1.726 + 1.727 + case 401: 1.728 + this._log.warn("401: login failed."); 1.729 + // Fall through to the 404 case. 1.730 + 1.731 + case 404: 1.732 + // Check that we're verifying with the correct cluster 1.733 + if (allow40XRecovery && this._clusterManager.setCluster()) { 1.734 + return this.verifyLogin(false); 1.735 + } 1.736 + 1.737 + // We must have the right cluster, but the server doesn't expect us 1.738 + this.status.login = LOGIN_FAILED_LOGIN_REJECTED; 1.739 + return false; 1.740 + 1.741 + default: 1.742 + // Server didn't respond with something that we expected 1.743 + this.status.login = LOGIN_FAILED_SERVER_ERROR; 1.744 + this.errorHandler.checkServerError(test); 1.745 + return false; 1.746 + } 1.747 + } catch (ex) { 1.748 + // Must have failed on some network issue 1.749 + this._log.debug("verifyLogin failed: " + Utils.exceptionStr(ex)); 1.750 + this.status.login = LOGIN_FAILED_NETWORK_ERROR; 1.751 + this.errorHandler.checkServerError(ex); 1.752 + return false; 1.753 + } 1.754 + }, 1.755 + 1.756 + generateNewSymmetricKeys: function generateNewSymmetricKeys() { 1.757 + this._log.info("Generating new keys WBO..."); 1.758 + let wbo = this.collectionKeys.generateNewKeysWBO(); 1.759 + this._log.info("Encrypting new key bundle."); 1.760 + wbo.encrypt(this.identity.syncKeyBundle); 1.761 + 1.762 + this._log.info("Uploading..."); 1.763 + let uploadRes = wbo.upload(this.resource(this.cryptoKeysURL)); 1.764 + if (uploadRes.status != 200) { 1.765 + this._log.warn("Got status " + uploadRes.status + " uploading new keys. What to do? Throw!"); 1.766 + this.errorHandler.checkServerError(uploadRes); 1.767 + throw new Error("Unable to upload symmetric keys."); 1.768 + } 1.769 + this._log.info("Got status " + uploadRes.status + " uploading keys."); 1.770 + let serverModified = uploadRes.obj; // Modified timestamp according to server. 1.771 + this._log.debug("Server reports crypto modified: " + serverModified); 1.772 + 1.773 + // Now verify that info/collections shows them! 1.774 + this._log.debug("Verifying server collection records."); 1.775 + let info = this._fetchInfo(); 1.776 + this._log.debug("info/collections is: " + info); 1.777 + 1.778 + if (info.status != 200) { 1.779 + this._log.warn("Non-200 info/collections response. Aborting."); 1.780 + throw new Error("Unable to upload symmetric keys."); 1.781 + } 1.782 + 1.783 + info = info.obj; 1.784 + if (!(CRYPTO_COLLECTION in info)) { 1.785 + this._log.error("Consistency failure: info/collections excludes " + 1.786 + "crypto after successful upload."); 1.787 + throw new Error("Symmetric key upload failed."); 1.788 + } 1.789 + 1.790 + // Can't check against local modified: clock drift. 1.791 + if (info[CRYPTO_COLLECTION] < serverModified) { 1.792 + this._log.error("Consistency failure: info/collections crypto entry " + 1.793 + "is stale after successful upload."); 1.794 + throw new Error("Symmetric key upload failed."); 1.795 + } 1.796 + 1.797 + // Doesn't matter if the timestamp is ahead. 1.798 + 1.799 + // Download and install them. 1.800 + let cryptoKeys = new CryptoWrapper(CRYPTO_COLLECTION, KEYS_WBO); 1.801 + let cryptoResp = cryptoKeys.fetch(this.resource(this.cryptoKeysURL)).response; 1.802 + if (cryptoResp.status != 200) { 1.803 + this._log.warn("Failed to download keys."); 1.804 + throw new Error("Symmetric key download failed."); 1.805 + } 1.806 + let keysChanged = this.handleFetchedKeys(this.identity.syncKeyBundle, 1.807 + cryptoKeys, true); 1.808 + if (keysChanged) { 1.809 + this._log.info("Downloaded keys differed, as expected."); 1.810 + } 1.811 + }, 1.812 + 1.813 + changePassword: function changePassword(newPassword) { 1.814 + let client = new UserAPI10Client(this.userAPIURI); 1.815 + let cb = Async.makeSpinningCallback(); 1.816 + client.changePassword(this.identity.username, 1.817 + this.identity.basicPassword, newPassword, cb); 1.818 + 1.819 + try { 1.820 + cb.wait(); 1.821 + } catch (ex) { 1.822 + this._log.debug("Password change failed: " + 1.823 + CommonUtils.exceptionStr(ex)); 1.824 + return false; 1.825 + } 1.826 + 1.827 + // Save the new password for requests and login manager. 1.828 + this.identity.basicPassword = newPassword; 1.829 + this.persistLogin(); 1.830 + return true; 1.831 + }, 1.832 + 1.833 + changePassphrase: function changePassphrase(newphrase) { 1.834 + return this._catch(function doChangePasphrase() { 1.835 + /* Wipe. */ 1.836 + this.wipeServer(); 1.837 + 1.838 + this.logout(); 1.839 + 1.840 + /* Set this so UI is updated on next run. */ 1.841 + this.identity.syncKey = newphrase; 1.842 + this.persistLogin(); 1.843 + 1.844 + /* We need to re-encrypt everything, so reset. */ 1.845 + this.resetClient(); 1.846 + this.collectionKeys.clear(); 1.847 + 1.848 + /* Login and sync. This also generates new keys. */ 1.849 + this.sync(); 1.850 + 1.851 + Svc.Obs.notify("weave:service:change-passphrase", true); 1.852 + 1.853 + return true; 1.854 + })(); 1.855 + }, 1.856 + 1.857 + startOver: function startOver() { 1.858 + this._log.trace("Invoking Service.startOver."); 1.859 + Svc.Obs.notify("weave:engine:stop-tracking"); 1.860 + this.status.resetSync(); 1.861 + 1.862 + // Deletion doesn't make sense if we aren't set up yet! 1.863 + if (this.clusterURL != "") { 1.864 + // Clear client-specific data from the server, including disabled engines. 1.865 + for each (let engine in [this.clientsEngine].concat(this.engineManager.getAll())) { 1.866 + try { 1.867 + engine.removeClientData(); 1.868 + } catch(ex) { 1.869 + this._log.warn("Deleting client data for " + engine.name + " failed:" 1.870 + + Utils.exceptionStr(ex)); 1.871 + } 1.872 + } 1.873 + this._log.debug("Finished deleting client data."); 1.874 + } else { 1.875 + this._log.debug("Skipping client data removal: no cluster URL."); 1.876 + } 1.877 + 1.878 + // We want let UI consumers of the following notification know as soon as 1.879 + // possible, so let's fake for the CLIENT_NOT_CONFIGURED status for now 1.880 + // by emptying the passphrase (we still need the password). 1.881 + this._log.info("Service.startOver dropping sync key and logging out."); 1.882 + this.identity.resetSyncKey(); 1.883 + this.status.login = LOGIN_FAILED_NO_PASSPHRASE; 1.884 + this.logout(); 1.885 + Svc.Obs.notify("weave:service:start-over"); 1.886 + 1.887 + // Reset all engines and clear keys. 1.888 + this.resetClient(); 1.889 + this.collectionKeys.clear(); 1.890 + this.status.resetBackoff(); 1.891 + 1.892 + // Reset Weave prefs. 1.893 + this._ignorePrefObserver = true; 1.894 + Svc.Prefs.resetBranch(""); 1.895 + this._ignorePrefObserver = false; 1.896 + 1.897 + Svc.Prefs.set("lastversion", WEAVE_VERSION); 1.898 + 1.899 + this.identity.deleteSyncCredentials(); 1.900 + 1.901 + // If necessary, reset the identity manager, then re-initialize it so the 1.902 + // FxA manager is used. This is configurable via a pref - mainly for tests. 1.903 + let keepIdentity = false; 1.904 + try { 1.905 + keepIdentity = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity"); 1.906 + } catch (_) { /* no such pref */ } 1.907 + if (keepIdentity) { 1.908 + Svc.Obs.notify("weave:service:start-over:finish"); 1.909 + return; 1.910 + } 1.911 + 1.912 + this.identity.finalize().then( 1.913 + () => { 1.914 + this.identity.username = ""; 1.915 + this.status.__authManager = null; 1.916 + this.identity = Status._authManager; 1.917 + this._clusterManager = this.identity.createClusterManager(this); 1.918 + Svc.Obs.notify("weave:service:start-over:finish"); 1.919 + } 1.920 + ).then(null, 1.921 + err => { 1.922 + this._log.error("startOver failed to re-initialize the identity manager: " + err); 1.923 + // Still send the observer notification so the current state is 1.924 + // reflected in the UI. 1.925 + Svc.Obs.notify("weave:service:start-over:finish"); 1.926 + } 1.927 + ); 1.928 + }, 1.929 + 1.930 + persistLogin: function persistLogin() { 1.931 + try { 1.932 + this.identity.persistCredentials(true); 1.933 + } catch (ex) { 1.934 + this._log.info("Unable to persist credentials: " + ex); 1.935 + } 1.936 + }, 1.937 + 1.938 + login: function login(username, password, passphrase) { 1.939 + function onNotify() { 1.940 + this._loggedIn = false; 1.941 + if (Services.io.offline) { 1.942 + this.status.login = LOGIN_FAILED_NETWORK_ERROR; 1.943 + throw "Application is offline, login should not be called"; 1.944 + } 1.945 + 1.946 + let initialStatus = this._checkSetup(); 1.947 + if (username) { 1.948 + this.identity.username = username; 1.949 + } 1.950 + if (password) { 1.951 + this.identity.basicPassword = password; 1.952 + } 1.953 + if (passphrase) { 1.954 + this.identity.syncKey = passphrase; 1.955 + } 1.956 + 1.957 + if (this._checkSetup() == CLIENT_NOT_CONFIGURED) { 1.958 + throw "Aborting login, client not configured."; 1.959 + } 1.960 + 1.961 + // Ask the identity manager to explicitly login now. 1.962 + let cb = Async.makeSpinningCallback(); 1.963 + this.identity.ensureLoggedIn().then(cb, cb); 1.964 + 1.965 + // Just let any errors bubble up - they've more context than we do! 1.966 + cb.wait(); 1.967 + 1.968 + // Calling login() with parameters when the client was 1.969 + // previously not configured means setup was completed. 1.970 + if (initialStatus == CLIENT_NOT_CONFIGURED 1.971 + && (username || password || passphrase)) { 1.972 + Svc.Obs.notify("weave:service:setup-complete"); 1.973 + } 1.974 + this._log.info("Logging in the user."); 1.975 + this._updateCachedURLs(); 1.976 + 1.977 + if (!this.verifyLogin()) { 1.978 + // verifyLogin sets the failure states here. 1.979 + throw "Login failed: " + this.status.login; 1.980 + } 1.981 + 1.982 + this._loggedIn = true; 1.983 + 1.984 + return true; 1.985 + } 1.986 + 1.987 + let notifier = this._notify("login", "", onNotify.bind(this)); 1.988 + return this._catch(this._lock("service.js: login", notifier))(); 1.989 + }, 1.990 + 1.991 + logout: function logout() { 1.992 + // If we failed during login, we aren't going to have this._loggedIn set, 1.993 + // but we still want to ask the identity to logout, so it doesn't try and 1.994 + // reuse any old credentials next time we sync. 1.995 + this._log.info("Logging out"); 1.996 + this.identity.logout(); 1.997 + this._loggedIn = false; 1.998 + 1.999 + Svc.Obs.notify("weave:service:logout:finish"); 1.1000 + }, 1.1001 + 1.1002 + checkAccount: function checkAccount(account) { 1.1003 + let client = new UserAPI10Client(this.userAPIURI); 1.1004 + let cb = Async.makeSpinningCallback(); 1.1005 + 1.1006 + let username = this.identity.usernameFromAccount(account); 1.1007 + client.usernameExists(username, cb); 1.1008 + 1.1009 + try { 1.1010 + let exists = cb.wait(); 1.1011 + return exists ? "notAvailable" : "available"; 1.1012 + } catch (ex) { 1.1013 + // TODO fix API convention. 1.1014 + return this.errorHandler.errorStr(ex); 1.1015 + } 1.1016 + }, 1.1017 + 1.1018 + createAccount: function createAccount(email, password, 1.1019 + captchaChallenge, captchaResponse) { 1.1020 + let client = new UserAPI10Client(this.userAPIURI); 1.1021 + 1.1022 + // Hint to server to allow scripted user creation or otherwise 1.1023 + // ignore captcha. 1.1024 + if (Svc.Prefs.isSet("admin-secret")) { 1.1025 + client.adminSecret = Svc.Prefs.get("admin-secret", ""); 1.1026 + } 1.1027 + 1.1028 + let cb = Async.makeSpinningCallback(); 1.1029 + 1.1030 + client.createAccount(email, password, captchaChallenge, captchaResponse, 1.1031 + cb); 1.1032 + 1.1033 + try { 1.1034 + cb.wait(); 1.1035 + return null; 1.1036 + } catch (ex) { 1.1037 + return this.errorHandler.errorStr(ex.body); 1.1038 + } 1.1039 + }, 1.1040 + 1.1041 + // Stuff we need to do after login, before we can really do 1.1042 + // anything (e.g. key setup). 1.1043 + _remoteSetup: function _remoteSetup(infoResponse) { 1.1044 + let reset = false; 1.1045 + 1.1046 + this._log.debug("Fetching global metadata record"); 1.1047 + let meta = this.recordManager.get(this.metaURL); 1.1048 + 1.1049 + // Checking modified time of the meta record. 1.1050 + if (infoResponse && 1.1051 + (infoResponse.obj.meta != this.metaModified) && 1.1052 + (!meta || !meta.isNew)) { 1.1053 + 1.1054 + // Delete the cached meta record... 1.1055 + this._log.debug("Clearing cached meta record. metaModified is " + 1.1056 + JSON.stringify(this.metaModified) + ", setting to " + 1.1057 + JSON.stringify(infoResponse.obj.meta)); 1.1058 + 1.1059 + this.recordManager.del(this.metaURL); 1.1060 + 1.1061 + // ... fetch the current record from the server, and COPY THE FLAGS. 1.1062 + let newMeta = this.recordManager.get(this.metaURL); 1.1063 + 1.1064 + // If we got a 401, we do not want to create a new meta/global - we 1.1065 + // should be able to get the existing meta after we get a new node. 1.1066 + if (this.recordManager.response.status == 401) { 1.1067 + this._log.debug("Fetching meta/global record on the server returned 401."); 1.1068 + this.errorHandler.checkServerError(this.recordManager.response); 1.1069 + return false; 1.1070 + } 1.1071 + 1.1072 + if (!this.recordManager.response.success || !newMeta) { 1.1073 + this._log.debug("No meta/global record on the server. Creating one."); 1.1074 + newMeta = new WBORecord("meta", "global"); 1.1075 + newMeta.payload.syncID = this.syncID; 1.1076 + newMeta.payload.storageVersion = STORAGE_VERSION; 1.1077 + newMeta.payload.declined = this.engineManager.getDeclined(); 1.1078 + 1.1079 + newMeta.isNew = true; 1.1080 + 1.1081 + this.recordManager.set(this.metaURL, newMeta); 1.1082 + if (!newMeta.upload(this.resource(this.metaURL)).success) { 1.1083 + this._log.warn("Unable to upload new meta/global. Failing remote setup."); 1.1084 + return false; 1.1085 + } 1.1086 + } else { 1.1087 + // If newMeta, then it stands to reason that meta != null. 1.1088 + newMeta.isNew = meta.isNew; 1.1089 + newMeta.changed = meta.changed; 1.1090 + } 1.1091 + 1.1092 + // Switch in the new meta object and record the new time. 1.1093 + meta = newMeta; 1.1094 + this.metaModified = infoResponse.obj.meta; 1.1095 + } 1.1096 + 1.1097 + let remoteVersion = (meta && meta.payload.storageVersion)? 1.1098 + meta.payload.storageVersion : ""; 1.1099 + 1.1100 + this._log.debug(["Weave Version:", WEAVE_VERSION, "Local Storage:", 1.1101 + STORAGE_VERSION, "Remote Storage:", remoteVersion].join(" ")); 1.1102 + 1.1103 + // Check for cases that require a fresh start. When comparing remoteVersion, 1.1104 + // we need to convert it to a number as older clients used it as a string. 1.1105 + if (!meta || !meta.payload.storageVersion || !meta.payload.syncID || 1.1106 + STORAGE_VERSION > parseFloat(remoteVersion)) { 1.1107 + 1.1108 + this._log.info("One of: no meta, no meta storageVersion, or no meta syncID. Fresh start needed."); 1.1109 + 1.1110 + // abort the server wipe if the GET status was anything other than 404 or 200 1.1111 + let status = this.recordManager.response.status; 1.1112 + if (status != 200 && status != 404) { 1.1113 + this.status.sync = METARECORD_DOWNLOAD_FAIL; 1.1114 + this.errorHandler.checkServerError(this.recordManager.response); 1.1115 + this._log.warn("Unknown error while downloading metadata record. " + 1.1116 + "Aborting sync."); 1.1117 + return false; 1.1118 + } 1.1119 + 1.1120 + if (!meta) 1.1121 + this._log.info("No metadata record, server wipe needed"); 1.1122 + if (meta && !meta.payload.syncID) 1.1123 + this._log.warn("No sync id, server wipe needed"); 1.1124 + 1.1125 + reset = true; 1.1126 + 1.1127 + this._log.info("Wiping server data"); 1.1128 + this._freshStart(); 1.1129 + 1.1130 + if (status == 404) 1.1131 + this._log.info("Metadata record not found, server was wiped to ensure " + 1.1132 + "consistency."); 1.1133 + else // 200 1.1134 + this._log.info("Wiped server; incompatible metadata: " + remoteVersion); 1.1135 + 1.1136 + return true; 1.1137 + } 1.1138 + else if (remoteVersion > STORAGE_VERSION) { 1.1139 + this.status.sync = VERSION_OUT_OF_DATE; 1.1140 + this._log.warn("Upgrade required to access newer storage version."); 1.1141 + return false; 1.1142 + } 1.1143 + else if (meta.payload.syncID != this.syncID) { 1.1144 + 1.1145 + this._log.info("Sync IDs differ. Local is " + this.syncID + ", remote is " + meta.payload.syncID); 1.1146 + this.resetClient(); 1.1147 + this.collectionKeys.clear(); 1.1148 + this.syncID = meta.payload.syncID; 1.1149 + this._log.debug("Clear cached values and take syncId: " + this.syncID); 1.1150 + 1.1151 + if (!this.upgradeSyncKey(meta.payload.syncID)) { 1.1152 + this._log.warn("Failed to upgrade sync key. Failing remote setup."); 1.1153 + return false; 1.1154 + } 1.1155 + 1.1156 + if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { 1.1157 + this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); 1.1158 + return false; 1.1159 + } 1.1160 + 1.1161 + // bug 545725 - re-verify creds and fail sanely 1.1162 + if (!this.verifyLogin()) { 1.1163 + this.status.sync = CREDENTIALS_CHANGED; 1.1164 + this._log.info("Credentials have changed, aborting sync and forcing re-login."); 1.1165 + return false; 1.1166 + } 1.1167 + 1.1168 + return true; 1.1169 + } 1.1170 + else { 1.1171 + if (!this.upgradeSyncKey(meta.payload.syncID)) { 1.1172 + this._log.warn("Failed to upgrade sync key. Failing remote setup."); 1.1173 + return false; 1.1174 + } 1.1175 + 1.1176 + if (!this.verifyAndFetchSymmetricKeys(infoResponse)) { 1.1177 + this._log.warn("Failed to fetch symmetric keys. Failing remote setup."); 1.1178 + return false; 1.1179 + } 1.1180 + 1.1181 + return true; 1.1182 + } 1.1183 + }, 1.1184 + 1.1185 + /** 1.1186 + * Return whether we should attempt login at the start of a sync. 1.1187 + * 1.1188 + * Note that this function has strong ties to _checkSync: callers 1.1189 + * of this function should typically use _checkSync to verify that 1.1190 + * any necessary login took place. 1.1191 + */ 1.1192 + _shouldLogin: function _shouldLogin() { 1.1193 + return this.enabled && 1.1194 + !Services.io.offline && 1.1195 + !this.isLoggedIn; 1.1196 + }, 1.1197 + 1.1198 + /** 1.1199 + * Determine if a sync should run. 1.1200 + * 1.1201 + * @param ignore [optional] 1.1202 + * array of reasons to ignore when checking 1.1203 + * 1.1204 + * @return Reason for not syncing; not-truthy if sync should run 1.1205 + */ 1.1206 + _checkSync: function _checkSync(ignore) { 1.1207 + let reason = ""; 1.1208 + if (!this.enabled) 1.1209 + reason = kSyncWeaveDisabled; 1.1210 + else if (Services.io.offline) 1.1211 + reason = kSyncNetworkOffline; 1.1212 + else if (this.status.minimumNextSync > Date.now()) 1.1213 + reason = kSyncBackoffNotMet; 1.1214 + else if ((this.status.login == MASTER_PASSWORD_LOCKED) && 1.1215 + Utils.mpLocked()) 1.1216 + reason = kSyncMasterPasswordLocked; 1.1217 + else if (Svc.Prefs.get("firstSync") == "notReady") 1.1218 + reason = kFirstSyncChoiceNotMade; 1.1219 + 1.1220 + if (ignore && ignore.indexOf(reason) != -1) 1.1221 + return ""; 1.1222 + 1.1223 + return reason; 1.1224 + }, 1.1225 + 1.1226 + sync: function sync() { 1.1227 + let dateStr = new Date().toLocaleFormat(LOG_DATE_FORMAT); 1.1228 + this._log.debug("User-Agent: " + SyncStorageRequest.prototype.userAgent); 1.1229 + this._log.info("Starting sync at " + dateStr); 1.1230 + this._catch(function () { 1.1231 + // Make sure we're logged in. 1.1232 + if (this._shouldLogin()) { 1.1233 + this._log.debug("In sync: should login."); 1.1234 + if (!this.login()) { 1.1235 + this._log.debug("Not syncing: login returned false."); 1.1236 + return; 1.1237 + } 1.1238 + } 1.1239 + else { 1.1240 + this._log.trace("In sync: no need to login."); 1.1241 + } 1.1242 + return this._lockedSync.apply(this, arguments); 1.1243 + })(); 1.1244 + }, 1.1245 + 1.1246 + /** 1.1247 + * Sync up engines with the server. 1.1248 + */ 1.1249 + _lockedSync: function _lockedSync() { 1.1250 + return this._lock("service.js: sync", 1.1251 + this._notify("sync", "", function onNotify() { 1.1252 + 1.1253 + let synchronizer = new EngineSynchronizer(this); 1.1254 + let cb = Async.makeSpinningCallback(); 1.1255 + synchronizer.onComplete = cb; 1.1256 + 1.1257 + synchronizer.sync(); 1.1258 + // wait() throws if the first argument is truthy, which is exactly what 1.1259 + // we want. 1.1260 + let result = cb.wait(); 1.1261 + 1.1262 + // We successfully synchronized. Now let's update our declined engines. 1.1263 + let meta = this.recordManager.get(this.metaURL); 1.1264 + if (!meta) { 1.1265 + this._log.warn("No meta/global; can't update declined state."); 1.1266 + return; 1.1267 + } 1.1268 + 1.1269 + let declinedEngines = new DeclinedEngines(this); 1.1270 + let didChange = declinedEngines.updateDeclined(meta, this.engineManager); 1.1271 + if (!didChange) { 1.1272 + this._log.info("No change to declined engines. Not reuploading meta/global."); 1.1273 + return; 1.1274 + } 1.1275 + 1.1276 + this.uploadMetaGlobal(meta); 1.1277 + }))(); 1.1278 + }, 1.1279 + 1.1280 + /** 1.1281 + * Upload meta/global, throwing the response on failure. 1.1282 + */ 1.1283 + uploadMetaGlobal: function (meta) { 1.1284 + this._log.debug("Uploading meta/global: " + JSON.stringify(meta)); 1.1285 + 1.1286 + // It would be good to set the X-If-Unmodified-Since header to `timestamp` 1.1287 + // for this PUT to ensure at least some level of transactionality. 1.1288 + // Unfortunately, the servers don't support it after a wipe right now 1.1289 + // (bug 693893), so we're going to defer this until bug 692700. 1.1290 + let res = this.resource(this.metaURL); 1.1291 + let response = res.put(meta); 1.1292 + if (!response.success) { 1.1293 + throw response; 1.1294 + } 1.1295 + this.recordManager.set(this.metaURL, meta); 1.1296 + }, 1.1297 + 1.1298 + /** 1.1299 + * If we have a passphrase, rather than a 25-alphadigit sync key, 1.1300 + * use the provided sync ID to bootstrap it using PBKDF2. 1.1301 + * 1.1302 + * Store the new 'passphrase' back into the identity manager. 1.1303 + * 1.1304 + * We can check this as often as we want, because once it's done the 1.1305 + * check will no longer succeed. It only matters that it happens after 1.1306 + * we decide to bump the server storage version. 1.1307 + */ 1.1308 + upgradeSyncKey: function upgradeSyncKey(syncID) { 1.1309 + let p = this.identity.syncKey; 1.1310 + 1.1311 + if (!p) { 1.1312 + return false; 1.1313 + } 1.1314 + 1.1315 + // Check whether it's already a key that we generated. 1.1316 + if (Utils.isPassphrase(p)) { 1.1317 + this._log.info("Sync key is up-to-date: no need to upgrade."); 1.1318 + return true; 1.1319 + } 1.1320 + 1.1321 + // Otherwise, let's upgrade it. 1.1322 + // N.B., we persist the sync key without testing it first... 1.1323 + 1.1324 + let s = btoa(syncID); // It's what WeaveCrypto expects. *sigh* 1.1325 + let k = Utils.derivePresentableKeyFromPassphrase(p, s, PBKDF2_KEY_BYTES); // Base 32. 1.1326 + 1.1327 + if (!k) { 1.1328 + this._log.error("No key resulted from derivePresentableKeyFromPassphrase. Failing upgrade."); 1.1329 + return false; 1.1330 + } 1.1331 + 1.1332 + this._log.info("Upgrading sync key..."); 1.1333 + this.identity.syncKey = k; 1.1334 + this._log.info("Saving upgraded sync key..."); 1.1335 + this.persistLogin(); 1.1336 + this._log.info("Done saving."); 1.1337 + return true; 1.1338 + }, 1.1339 + 1.1340 + _freshStart: function _freshStart() { 1.1341 + this._log.info("Fresh start. Resetting client and considering key upgrade."); 1.1342 + this.resetClient(); 1.1343 + this.collectionKeys.clear(); 1.1344 + this.upgradeSyncKey(this.syncID); 1.1345 + 1.1346 + // Wipe the server. 1.1347 + let wipeTimestamp = this.wipeServer(); 1.1348 + 1.1349 + // Upload a new meta/global record. 1.1350 + let meta = new WBORecord("meta", "global"); 1.1351 + meta.payload.syncID = this.syncID; 1.1352 + meta.payload.storageVersion = STORAGE_VERSION; 1.1353 + meta.payload.declined = this.engineManager.getDeclined(); 1.1354 + meta.isNew = true; 1.1355 + 1.1356 + // uploadMetaGlobal throws on failure -- including race conditions. 1.1357 + // If we got into a race condition, we'll abort the sync this way, too. 1.1358 + // That's fine. We'll just wait till the next sync. The client that we're 1.1359 + // racing is probably busy uploading stuff right now anyway. 1.1360 + this.uploadMetaGlobal(meta); 1.1361 + 1.1362 + // Wipe everything we know about except meta because we just uploaded it 1.1363 + let engines = [this.clientsEngine].concat(this.engineManager.getAll()); 1.1364 + let collections = [engine.name for each (engine in engines)]; 1.1365 + // TODO: there's a bug here. We should be calling resetClient, no? 1.1366 + 1.1367 + // Generate, upload, and download new keys. Do this last so we don't wipe 1.1368 + // them... 1.1369 + this.generateNewSymmetricKeys(); 1.1370 + }, 1.1371 + 1.1372 + /** 1.1373 + * Wipe user data from the server. 1.1374 + * 1.1375 + * @param collections [optional] 1.1376 + * Array of collections to wipe. If not given, all collections are 1.1377 + * wiped by issuing a DELETE request for `storageURL`. 1.1378 + * 1.1379 + * @return the server's timestamp of the (last) DELETE. 1.1380 + */ 1.1381 + wipeServer: function wipeServer(collections) { 1.1382 + let response; 1.1383 + if (!collections) { 1.1384 + // Strip the trailing slash. 1.1385 + let res = this.resource(this.storageURL.slice(0, -1)); 1.1386 + res.setHeader("X-Confirm-Delete", "1"); 1.1387 + try { 1.1388 + response = res.delete(); 1.1389 + } catch (ex) { 1.1390 + this._log.debug("Failed to wipe server: " + CommonUtils.exceptionStr(ex)); 1.1391 + throw ex; 1.1392 + } 1.1393 + if (response.status != 200 && response.status != 404) { 1.1394 + this._log.debug("Aborting wipeServer. Server responded with " + 1.1395 + response.status + " response for " + this.storageURL); 1.1396 + throw response; 1.1397 + } 1.1398 + return response.headers["x-weave-timestamp"]; 1.1399 + } 1.1400 + 1.1401 + let timestamp; 1.1402 + for (let name of collections) { 1.1403 + let url = this.storageURL + name; 1.1404 + try { 1.1405 + response = this.resource(url).delete(); 1.1406 + } catch (ex) { 1.1407 + this._log.debug("Failed to wipe '" + name + "' collection: " + 1.1408 + Utils.exceptionStr(ex)); 1.1409 + throw ex; 1.1410 + } 1.1411 + 1.1412 + if (response.status != 200 && response.status != 404) { 1.1413 + this._log.debug("Aborting wipeServer. Server responded with " + 1.1414 + response.status + " response for " + url); 1.1415 + throw response; 1.1416 + } 1.1417 + 1.1418 + if ("x-weave-timestamp" in response.headers) { 1.1419 + timestamp = response.headers["x-weave-timestamp"]; 1.1420 + } 1.1421 + } 1.1422 + 1.1423 + return timestamp; 1.1424 + }, 1.1425 + 1.1426 + /** 1.1427 + * Wipe all local user data. 1.1428 + * 1.1429 + * @param engines [optional] 1.1430 + * Array of engine names to wipe. If not given, all engines are used. 1.1431 + */ 1.1432 + wipeClient: function wipeClient(engines) { 1.1433 + // If we don't have any engines, reset the service and wipe all engines 1.1434 + if (!engines) { 1.1435 + // Clear out any service data 1.1436 + this.resetService(); 1.1437 + 1.1438 + engines = [this.clientsEngine].concat(this.engineManager.getAll()); 1.1439 + } 1.1440 + // Convert the array of names into engines 1.1441 + else { 1.1442 + engines = this.engineManager.get(engines); 1.1443 + } 1.1444 + 1.1445 + // Fully wipe each engine if it's able to decrypt data 1.1446 + for each (let engine in engines) { 1.1447 + if (engine.canDecrypt()) { 1.1448 + engine.wipeClient(); 1.1449 + } 1.1450 + } 1.1451 + 1.1452 + // Save the password/passphrase just in-case they aren't restored by sync 1.1453 + this.persistLogin(); 1.1454 + }, 1.1455 + 1.1456 + /** 1.1457 + * Wipe all remote user data by wiping the server then telling each remote 1.1458 + * client to wipe itself. 1.1459 + * 1.1460 + * @param engines [optional] 1.1461 + * Array of engine names to wipe. If not given, all engines are used. 1.1462 + */ 1.1463 + wipeRemote: function wipeRemote(engines) { 1.1464 + try { 1.1465 + // Make sure stuff gets uploaded. 1.1466 + this.resetClient(engines); 1.1467 + 1.1468 + // Clear out any server data. 1.1469 + this.wipeServer(engines); 1.1470 + 1.1471 + // Only wipe the engines provided. 1.1472 + if (engines) { 1.1473 + engines.forEach(function(e) this.clientsEngine.sendCommand("wipeEngine", [e]), this); 1.1474 + } 1.1475 + // Tell the remote machines to wipe themselves. 1.1476 + else { 1.1477 + this.clientsEngine.sendCommand("wipeAll", []); 1.1478 + } 1.1479 + 1.1480 + // Make sure the changed clients get updated. 1.1481 + this.clientsEngine.sync(); 1.1482 + } catch (ex) { 1.1483 + this.errorHandler.checkServerError(ex); 1.1484 + throw ex; 1.1485 + } 1.1486 + }, 1.1487 + 1.1488 + /** 1.1489 + * Reset local service information like logs, sync times, caches. 1.1490 + */ 1.1491 + resetService: function resetService() { 1.1492 + this._catch(function reset() { 1.1493 + this._log.info("Service reset."); 1.1494 + 1.1495 + // Pretend we've never synced to the server and drop cached data 1.1496 + this.syncID = ""; 1.1497 + this.recordManager.clearCache(); 1.1498 + })(); 1.1499 + }, 1.1500 + 1.1501 + /** 1.1502 + * Reset the client by getting rid of any local server data and client data. 1.1503 + * 1.1504 + * @param engines [optional] 1.1505 + * Array of engine names to reset. If not given, all engines are used. 1.1506 + */ 1.1507 + resetClient: function resetClient(engines) { 1.1508 + this._catch(function doResetClient() { 1.1509 + // If we don't have any engines, reset everything including the service 1.1510 + if (!engines) { 1.1511 + // Clear out any service data 1.1512 + this.resetService(); 1.1513 + 1.1514 + engines = [this.clientsEngine].concat(this.engineManager.getAll()); 1.1515 + } 1.1516 + // Convert the array of names into engines 1.1517 + else { 1.1518 + engines = this.engineManager.get(engines); 1.1519 + } 1.1520 + 1.1521 + // Have each engine drop any temporary meta data 1.1522 + for each (let engine in engines) { 1.1523 + engine.resetClient(); 1.1524 + } 1.1525 + })(); 1.1526 + }, 1.1527 + 1.1528 + /** 1.1529 + * Fetch storage info from the server. 1.1530 + * 1.1531 + * @param type 1.1532 + * String specifying what info to fetch from the server. Must be one 1.1533 + * of the INFO_* values. See Sync Storage Server API spec for details. 1.1534 + * @param callback 1.1535 + * Callback function with signature (error, data) where `data' is 1.1536 + * the return value from the server already parsed as JSON. 1.1537 + * 1.1538 + * @return RESTRequest instance representing the request, allowing callers 1.1539 + * to cancel the request. 1.1540 + */ 1.1541 + getStorageInfo: function getStorageInfo(type, callback) { 1.1542 + if (STORAGE_INFO_TYPES.indexOf(type) == -1) { 1.1543 + throw "Invalid value for 'type': " + type; 1.1544 + } 1.1545 + 1.1546 + let info_type = "info/" + type; 1.1547 + this._log.trace("Retrieving '" + info_type + "'..."); 1.1548 + let url = this.userBaseURL + info_type; 1.1549 + return this.getStorageRequest(url).get(function onComplete(error) { 1.1550 + // Note: 'this' is the request. 1.1551 + if (error) { 1.1552 + this._log.debug("Failed to retrieve '" + info_type + "': " + 1.1553 + Utils.exceptionStr(error)); 1.1554 + return callback(error); 1.1555 + } 1.1556 + if (this.response.status != 200) { 1.1557 + this._log.debug("Failed to retrieve '" + info_type + 1.1558 + "': server responded with HTTP" + 1.1559 + this.response.status); 1.1560 + return callback(this.response); 1.1561 + } 1.1562 + 1.1563 + let result; 1.1564 + try { 1.1565 + result = JSON.parse(this.response.body); 1.1566 + } catch (ex) { 1.1567 + this._log.debug("Server returned invalid JSON for '" + info_type + 1.1568 + "': " + this.response.body); 1.1569 + return callback(ex); 1.1570 + } 1.1571 + this._log.trace("Successfully retrieved '" + info_type + "'."); 1.1572 + return callback(null, result); 1.1573 + }); 1.1574 + }, 1.1575 +}; 1.1576 + 1.1577 +this.Service = new Sync11Service(); 1.1578 +Service.onStartup();