1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/identity.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,562 @@ 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 file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["IdentityManager"]; 1.11 + 1.12 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.13 + 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 +Cu.import("resource://gre/modules/Promise.jsm"); 1.16 +Cu.import("resource://services-sync/constants.js"); 1.17 +Cu.import("resource://gre/modules/Log.jsm"); 1.18 +Cu.import("resource://services-sync/util.js"); 1.19 + 1.20 +// Lazy import to prevent unnecessary load on startup. 1.21 +for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) { 1.22 + XPCOMUtils.defineLazyModuleGetter(this, symbol, 1.23 + "resource://services-sync/keys.js", 1.24 + symbol); 1.25 +} 1.26 + 1.27 +/** 1.28 + * Manages "legacy" identity and authentication for Sync. 1.29 + * See browserid_identity for the Firefox Accounts based identity manager. 1.30 + * 1.31 + * The following entities are managed: 1.32 + * 1.33 + * account - The main Sync/services account. This is typically an email 1.34 + * address. 1.35 + * username - A normalized version of your account. This is what's 1.36 + * transmitted to the server. 1.37 + * basic password - UTF-8 password used for authenticating when using HTTP 1.38 + * basic authentication. 1.39 + * sync key - The main encryption key used by Sync. 1.40 + * sync key bundle - A representation of your sync key. 1.41 + * 1.42 + * When changes are made to entities that are stored in the password manager 1.43 + * (basic password, sync key), those changes are merely staged. To commit them 1.44 + * to the password manager, you'll need to call persistCredentials(). 1.45 + * 1.46 + * This type also manages authenticating Sync's network requests. Sync's 1.47 + * network code calls into getRESTRequestAuthenticator and 1.48 + * getResourceAuthenticator (depending on the network layer being used). Each 1.49 + * returns a function which can be used to add authentication information to an 1.50 + * outgoing request. 1.51 + * 1.52 + * In theory, this type supports arbitrary identity and authentication 1.53 + * mechanisms. You can add support for them by monkeypatching the global 1.54 + * instance of this type. Specifically, you'll need to redefine the 1.55 + * aforementioned network code functions to do whatever your authentication 1.56 + * mechanism needs them to do. In addition, you may wish to install custom 1.57 + * functions to support your API. Although, that is certainly not required. 1.58 + * If you do monkeypatch, please be advised that Sync expects the core 1.59 + * attributes to have values. You will need to carry at least account and 1.60 + * username forward. If you do not wish to support one of the built-in 1.61 + * authentication mechanisms, you'll probably want to redefine currentAuthState 1.62 + * and any other function that involves the built-in functionality. 1.63 + */ 1.64 +this.IdentityManager = function IdentityManager() { 1.65 + this._log = Log.repository.getLogger("Sync.Identity"); 1.66 + this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")]; 1.67 + 1.68 + this._basicPassword = null; 1.69 + this._basicPasswordAllowLookup = true; 1.70 + this._basicPasswordUpdated = false; 1.71 + this._syncKey = null; 1.72 + this._syncKeyAllowLookup = true; 1.73 + this._syncKeySet = false; 1.74 + this._syncKeyBundle = null; 1.75 +} 1.76 +IdentityManager.prototype = { 1.77 + _log: null, 1.78 + 1.79 + _basicPassword: null, 1.80 + _basicPasswordAllowLookup: true, 1.81 + _basicPasswordUpdated: false, 1.82 + 1.83 + _syncKey: null, 1.84 + _syncKeyAllowLookup: true, 1.85 + _syncKeySet: false, 1.86 + 1.87 + _syncKeyBundle: null, 1.88 + 1.89 + /** 1.90 + * Initialize the identity provider. Returns a promise that is resolved 1.91 + * when initialization is complete and the provider can be queried for 1.92 + * its state 1.93 + */ 1.94 + initialize: function() { 1.95 + // Nothing to do for this identity provider. 1.96 + return Promise.resolve(); 1.97 + }, 1.98 + 1.99 + finalize: function() { 1.100 + // Nothing to do for this identity provider. 1.101 + return Promise.resolve(); 1.102 + }, 1.103 + 1.104 + /** 1.105 + * Called whenever Service.logout() is called. 1.106 + */ 1.107 + logout: function() { 1.108 + // nothing to do for this identity provider. 1.109 + }, 1.110 + 1.111 + /** 1.112 + * Ensure the user is logged in. Returns a promise that resolves when 1.113 + * the user is logged in, or is rejected if the login attempt has failed. 1.114 + */ 1.115 + ensureLoggedIn: function() { 1.116 + // nothing to do for this identity provider 1.117 + return Promise.resolve(); 1.118 + }, 1.119 + 1.120 + /** 1.121 + * Indicates if the identity manager is still initializing 1.122 + */ 1.123 + get readyToAuthenticate() { 1.124 + // We initialize in a fully sync manner, so we are always finished. 1.125 + return true; 1.126 + }, 1.127 + 1.128 + get account() { 1.129 + return Svc.Prefs.get("account", this.username); 1.130 + }, 1.131 + 1.132 + /** 1.133 + * Sets the active account name. 1.134 + * 1.135 + * This should almost always be called in favor of setting username, as 1.136 + * username is derived from account. 1.137 + * 1.138 + * Changing the account name has the side-effect of wiping out stored 1.139 + * credentials. Keep in mind that persistCredentials() will need to be called 1.140 + * to flush the changes to disk. 1.141 + * 1.142 + * Set this value to null to clear out identity information. 1.143 + */ 1.144 + set account(value) { 1.145 + if (value) { 1.146 + value = value.toLowerCase(); 1.147 + Svc.Prefs.set("account", value); 1.148 + } else { 1.149 + Svc.Prefs.reset("account"); 1.150 + } 1.151 + 1.152 + this.username = this.usernameFromAccount(value); 1.153 + }, 1.154 + 1.155 + get username() { 1.156 + return Svc.Prefs.get("username", null); 1.157 + }, 1.158 + 1.159 + /** 1.160 + * Set the username value. 1.161 + * 1.162 + * Changing the username has the side-effect of wiping credentials. 1.163 + */ 1.164 + set username(value) { 1.165 + if (value) { 1.166 + value = value.toLowerCase(); 1.167 + 1.168 + if (value == this.username) { 1.169 + return; 1.170 + } 1.171 + 1.172 + Svc.Prefs.set("username", value); 1.173 + } else { 1.174 + Svc.Prefs.reset("username"); 1.175 + } 1.176 + 1.177 + // If we change the username, we interpret this as a major change event 1.178 + // and wipe out the credentials. 1.179 + this._log.info("Username changed. Removing stored credentials."); 1.180 + this.resetCredentials(); 1.181 + }, 1.182 + 1.183 + /** 1.184 + * Resets/Drops all credentials we hold for the current user. 1.185 + */ 1.186 + resetCredentials: function() { 1.187 + this.basicPassword = null; 1.188 + this.resetSyncKey(); 1.189 + }, 1.190 + 1.191 + /** 1.192 + * Resets/Drops the sync key we hold for the current user. 1.193 + */ 1.194 + resetSyncKey: function() { 1.195 + this.syncKey = null; 1.196 + // syncKeyBundle cleared as a result of setting syncKey. 1.197 + }, 1.198 + 1.199 + /** 1.200 + * Obtains the HTTP Basic auth password. 1.201 + * 1.202 + * Returns a string if set or null if it is not set. 1.203 + */ 1.204 + get basicPassword() { 1.205 + if (this._basicPasswordAllowLookup) { 1.206 + // We need a username to find the credentials. 1.207 + let username = this.username; 1.208 + if (!username) { 1.209 + return null; 1.210 + } 1.211 + 1.212 + for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) { 1.213 + if (login.username.toLowerCase() == username) { 1.214 + // It should already be UTF-8 encoded, but we don't take any chances. 1.215 + this._basicPassword = Utils.encodeUTF8(login.password); 1.216 + } 1.217 + } 1.218 + 1.219 + this._basicPasswordAllowLookup = false; 1.220 + } 1.221 + 1.222 + return this._basicPassword; 1.223 + }, 1.224 + 1.225 + /** 1.226 + * Set the HTTP basic password to use. 1.227 + * 1.228 + * Changes will not persist unless persistSyncCredentials() is called. 1.229 + */ 1.230 + set basicPassword(value) { 1.231 + // Wiping out value. 1.232 + if (!value) { 1.233 + this._log.info("Basic password has no value. Removing."); 1.234 + this._basicPassword = null; 1.235 + this._basicPasswordUpdated = true; 1.236 + this._basicPasswordAllowLookup = false; 1.237 + return; 1.238 + } 1.239 + 1.240 + let username = this.username; 1.241 + if (!username) { 1.242 + throw new Error("basicPassword cannot be set before username."); 1.243 + } 1.244 + 1.245 + this._log.info("Basic password being updated."); 1.246 + this._basicPassword = Utils.encodeUTF8(value); 1.247 + this._basicPasswordUpdated = true; 1.248 + }, 1.249 + 1.250 + /** 1.251 + * Obtain the Sync Key. 1.252 + * 1.253 + * This returns a 26 character "friendly" Base32 encoded string on success or 1.254 + * null if no Sync Key could be found. 1.255 + * 1.256 + * If the Sync Key hasn't been set in this session, this will look in the 1.257 + * password manager for the sync key. 1.258 + */ 1.259 + get syncKey() { 1.260 + if (this._syncKeyAllowLookup) { 1.261 + let username = this.username; 1.262 + if (!username) { 1.263 + return null; 1.264 + } 1.265 + 1.266 + for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) { 1.267 + if (login.username.toLowerCase() == username) { 1.268 + this._syncKey = login.password; 1.269 + } 1.270 + } 1.271 + 1.272 + this._syncKeyAllowLookup = false; 1.273 + } 1.274 + 1.275 + return this._syncKey; 1.276 + }, 1.277 + 1.278 + /** 1.279 + * Set the active Sync Key. 1.280 + * 1.281 + * If being set to null, the Sync Key and its derived SyncKeyBundle are 1.282 + * removed. However, the Sync Key won't be deleted from the password manager 1.283 + * until persistSyncCredentials() is called. 1.284 + * 1.285 + * If a value is provided, it should be a 26 or 32 character "friendly" 1.286 + * Base32 string for which Utils.isPassphrase() returns true. 1.287 + * 1.288 + * A side-effect of setting the Sync Key is that a SyncKeyBundle is 1.289 + * generated. For historical reasons, this will silently error out if the 1.290 + * value is not a proper Sync Key (!Utils.isPassphrase()). This should be 1.291 + * fixed in the future (once service.js is more sane) to throw if the passed 1.292 + * value is not valid. 1.293 + */ 1.294 + set syncKey(value) { 1.295 + if (!value) { 1.296 + this._log.info("Sync Key has no value. Deleting."); 1.297 + this._syncKey = null; 1.298 + this._syncKeyBundle = null; 1.299 + this._syncKeyUpdated = true; 1.300 + return; 1.301 + } 1.302 + 1.303 + if (!this.username) { 1.304 + throw new Error("syncKey cannot be set before username."); 1.305 + } 1.306 + 1.307 + this._log.info("Sync Key being updated."); 1.308 + this._syncKey = value; 1.309 + 1.310 + // Clear any cached Sync Key Bundle and regenerate it. 1.311 + this._syncKeyBundle = null; 1.312 + let bundle = this.syncKeyBundle; 1.313 + 1.314 + this._syncKeyUpdated = true; 1.315 + }, 1.316 + 1.317 + /** 1.318 + * Obtain the active SyncKeyBundle. 1.319 + * 1.320 + * This returns a SyncKeyBundle representing a key pair derived from the 1.321 + * Sync Key on success. If no Sync Key is present or if the Sync Key is not 1.322 + * valid, this returns null. 1.323 + * 1.324 + * The SyncKeyBundle should be treated as immutable. 1.325 + */ 1.326 + get syncKeyBundle() { 1.327 + // We can't obtain a bundle without a username set. 1.328 + if (!this.username) { 1.329 + this._log.warn("Attempted to obtain Sync Key Bundle with no username set!"); 1.330 + return null; 1.331 + } 1.332 + 1.333 + if (!this.syncKey) { 1.334 + this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " + 1.335 + "set!"); 1.336 + return null; 1.337 + } 1.338 + 1.339 + if (!this._syncKeyBundle) { 1.340 + try { 1.341 + this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey); 1.342 + } catch (ex) { 1.343 + this._log.warn(Utils.exceptionStr(ex)); 1.344 + return null; 1.345 + } 1.346 + } 1.347 + 1.348 + return this._syncKeyBundle; 1.349 + }, 1.350 + 1.351 + /** 1.352 + * The current state of the auth credentials. 1.353 + * 1.354 + * This essentially validates that enough credentials are available to use 1.355 + * Sync. 1.356 + */ 1.357 + get currentAuthState() { 1.358 + if (!this.username) { 1.359 + return LOGIN_FAILED_NO_USERNAME; 1.360 + } 1.361 + 1.362 + if (Utils.mpLocked()) { 1.363 + return STATUS_OK; 1.364 + } 1.365 + 1.366 + if (!this.basicPassword) { 1.367 + return LOGIN_FAILED_NO_PASSWORD; 1.368 + } 1.369 + 1.370 + if (!this.syncKey) { 1.371 + return LOGIN_FAILED_NO_PASSPHRASE; 1.372 + } 1.373 + 1.374 + // If we have a Sync Key but no bundle, bundle creation failed, which 1.375 + // implies a bad Sync Key. 1.376 + if (!this.syncKeyBundle) { 1.377 + return LOGIN_FAILED_INVALID_PASSPHRASE; 1.378 + } 1.379 + 1.380 + return STATUS_OK; 1.381 + }, 1.382 + 1.383 + /** 1.384 + * Persist credentials to password store. 1.385 + * 1.386 + * When credentials are updated, they are changed in memory only. This will 1.387 + * need to be called to save them to the underlying password store. 1.388 + * 1.389 + * If the password store is locked (e.g. if the master password hasn't been 1.390 + * entered), this could throw an exception. 1.391 + */ 1.392 + persistCredentials: function persistCredentials(force) { 1.393 + if (this._basicPasswordUpdated || force) { 1.394 + if (this._basicPassword) { 1.395 + this._setLogin(PWDMGR_PASSWORD_REALM, this.username, 1.396 + this._basicPassword); 1.397 + } else { 1.398 + for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) { 1.399 + Services.logins.removeLogin(login); 1.400 + } 1.401 + } 1.402 + 1.403 + this._basicPasswordUpdated = false; 1.404 + } 1.405 + 1.406 + if (this._syncKeyUpdated || force) { 1.407 + if (this._syncKey) { 1.408 + this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey); 1.409 + } else { 1.410 + for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) { 1.411 + Services.logins.removeLogin(login); 1.412 + } 1.413 + } 1.414 + 1.415 + this._syncKeyUpdated = false; 1.416 + } 1.417 + 1.418 + }, 1.419 + 1.420 + /** 1.421 + * Deletes the Sync Key from the system. 1.422 + */ 1.423 + deleteSyncKey: function deleteSyncKey() { 1.424 + this.syncKey = null; 1.425 + this.persistCredentials(); 1.426 + }, 1.427 + 1.428 + hasBasicCredentials: function hasBasicCredentials() { 1.429 + // Because JavaScript. 1.430 + return this.username && this.basicPassword && true; 1.431 + }, 1.432 + 1.433 + /** 1.434 + * Obtains the array of basic logins from nsiPasswordManager. 1.435 + */ 1.436 + _getLogins: function _getLogins(realm) { 1.437 + return Services.logins.findLogins({}, PWDMGR_HOST, null, realm); 1.438 + }, 1.439 + 1.440 + /** 1.441 + * Set a login in the password manager. 1.442 + * 1.443 + * This has the side-effect of deleting any other logins for the specified 1.444 + * realm. 1.445 + */ 1.446 + _setLogin: function _setLogin(realm, username, password) { 1.447 + let exists = false; 1.448 + for each (let login in this._getLogins(realm)) { 1.449 + if (login.username == username && login.password == password) { 1.450 + exists = true; 1.451 + } else { 1.452 + this._log.debug("Pruning old login for " + username + " from " + realm); 1.453 + Services.logins.removeLogin(login); 1.454 + } 1.455 + } 1.456 + 1.457 + if (exists) { 1.458 + return; 1.459 + } 1.460 + 1.461 + this._log.debug("Updating saved password for " + username + " in " + 1.462 + realm); 1.463 + 1.464 + let loginInfo = new Components.Constructor( 1.465 + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init"); 1.466 + let login = new loginInfo(PWDMGR_HOST, null, realm, username, 1.467 + password, "", ""); 1.468 + Services.logins.addLogin(login); 1.469 + }, 1.470 + 1.471 + /** 1.472 + * Deletes Sync credentials from the password manager. 1.473 + */ 1.474 + deleteSyncCredentials: function deleteSyncCredentials() { 1.475 + for (let host of Utils.getSyncCredentialsHosts()) { 1.476 + let logins = Services.logins.findLogins({}, host, "", ""); 1.477 + for each (let login in logins) { 1.478 + Services.logins.removeLogin(login); 1.479 + } 1.480 + } 1.481 + 1.482 + // Wait until after store is updated in case it fails. 1.483 + this._basicPassword = null; 1.484 + this._basicPasswordAllowLookup = true; 1.485 + this._basicPasswordUpdated = false; 1.486 + 1.487 + this._syncKey = null; 1.488 + // this._syncKeyBundle is nullified as part of _syncKey setter. 1.489 + this._syncKeyAllowLookup = true; 1.490 + this._syncKeyUpdated = false; 1.491 + }, 1.492 + 1.493 + usernameFromAccount: function usernameFromAccount(value) { 1.494 + // If we encounter characters not allowed by the API (as found for 1.495 + // instance in an email address), hash the value. 1.496 + if (value && value.match(/[^A-Z0-9._-]/i)) { 1.497 + return Utils.sha1Base32(value.toLowerCase()).toLowerCase(); 1.498 + } 1.499 + 1.500 + return value ? value.toLowerCase() : value; 1.501 + }, 1.502 + 1.503 + /** 1.504 + * Obtain a function to be used for adding auth to Resource HTTP requests. 1.505 + */ 1.506 + getResourceAuthenticator: function getResourceAuthenticator() { 1.507 + if (this.hasBasicCredentials()) { 1.508 + return this._onResourceRequestBasic.bind(this); 1.509 + } 1.510 + 1.511 + return null; 1.512 + }, 1.513 + 1.514 + /** 1.515 + * Helper method to return an authenticator for basic Resource requests. 1.516 + */ 1.517 + getBasicResourceAuthenticator: 1.518 + function getBasicResourceAuthenticator(username, password) { 1.519 + 1.520 + return function basicAuthenticator(resource) { 1.521 + let value = "Basic " + btoa(username + ":" + password); 1.522 + return {headers: {authorization: value}}; 1.523 + }; 1.524 + }, 1.525 + 1.526 + _onResourceRequestBasic: function _onResourceRequestBasic(resource) { 1.527 + let value = "Basic " + btoa(this.username + ":" + this.basicPassword); 1.528 + return {headers: {authorization: value}}; 1.529 + }, 1.530 + 1.531 + _onResourceRequestMAC: function _onResourceRequestMAC(resource, method) { 1.532 + // TODO Get identifier and key from somewhere. 1.533 + let identifier; 1.534 + let key; 1.535 + let result = Utils.computeHTTPMACSHA1(identifier, key, method, resource.uri); 1.536 + 1.537 + return {headers: {authorization: result.header}}; 1.538 + }, 1.539 + 1.540 + /** 1.541 + * Obtain a function to be used for adding auth to RESTRequest instances. 1.542 + */ 1.543 + getRESTRequestAuthenticator: function getRESTRequestAuthenticator() { 1.544 + if (this.hasBasicCredentials()) { 1.545 + return this.onRESTRequestBasic.bind(this); 1.546 + } 1.547 + 1.548 + return null; 1.549 + }, 1.550 + 1.551 + onRESTRequestBasic: function onRESTRequestBasic(request) { 1.552 + let up = this.username + ":" + this.basicPassword; 1.553 + request.setHeader("authorization", "Basic " + btoa(up)); 1.554 + }, 1.555 + 1.556 + createClusterManager: function(service) { 1.557 + Cu.import("resource://services-sync/stages/cluster.js"); 1.558 + return new ClusterManager(service); 1.559 + }, 1.560 + 1.561 + offerSyncOptions: function () { 1.562 + // Do nothing for Sync 1.1. 1.563 + return {accepted: true}; 1.564 + }, 1.565 +};