services/sync/modules/identity.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["IdentityManager"];
michael@0 8
michael@0 9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
michael@0 10
michael@0 11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 12 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 13 Cu.import("resource://services-sync/constants.js");
michael@0 14 Cu.import("resource://gre/modules/Log.jsm");
michael@0 15 Cu.import("resource://services-sync/util.js");
michael@0 16
michael@0 17 // Lazy import to prevent unnecessary load on startup.
michael@0 18 for (let symbol of ["BulkKeyBundle", "SyncKeyBundle"]) {
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, symbol,
michael@0 20 "resource://services-sync/keys.js",
michael@0 21 symbol);
michael@0 22 }
michael@0 23
michael@0 24 /**
michael@0 25 * Manages "legacy" identity and authentication for Sync.
michael@0 26 * See browserid_identity for the Firefox Accounts based identity manager.
michael@0 27 *
michael@0 28 * The following entities are managed:
michael@0 29 *
michael@0 30 * account - The main Sync/services account. This is typically an email
michael@0 31 * address.
michael@0 32 * username - A normalized version of your account. This is what's
michael@0 33 * transmitted to the server.
michael@0 34 * basic password - UTF-8 password used for authenticating when using HTTP
michael@0 35 * basic authentication.
michael@0 36 * sync key - The main encryption key used by Sync.
michael@0 37 * sync key bundle - A representation of your sync key.
michael@0 38 *
michael@0 39 * When changes are made to entities that are stored in the password manager
michael@0 40 * (basic password, sync key), those changes are merely staged. To commit them
michael@0 41 * to the password manager, you'll need to call persistCredentials().
michael@0 42 *
michael@0 43 * This type also manages authenticating Sync's network requests. Sync's
michael@0 44 * network code calls into getRESTRequestAuthenticator and
michael@0 45 * getResourceAuthenticator (depending on the network layer being used). Each
michael@0 46 * returns a function which can be used to add authentication information to an
michael@0 47 * outgoing request.
michael@0 48 *
michael@0 49 * In theory, this type supports arbitrary identity and authentication
michael@0 50 * mechanisms. You can add support for them by monkeypatching the global
michael@0 51 * instance of this type. Specifically, you'll need to redefine the
michael@0 52 * aforementioned network code functions to do whatever your authentication
michael@0 53 * mechanism needs them to do. In addition, you may wish to install custom
michael@0 54 * functions to support your API. Although, that is certainly not required.
michael@0 55 * If you do monkeypatch, please be advised that Sync expects the core
michael@0 56 * attributes to have values. You will need to carry at least account and
michael@0 57 * username forward. If you do not wish to support one of the built-in
michael@0 58 * authentication mechanisms, you'll probably want to redefine currentAuthState
michael@0 59 * and any other function that involves the built-in functionality.
michael@0 60 */
michael@0 61 this.IdentityManager = function IdentityManager() {
michael@0 62 this._log = Log.repository.getLogger("Sync.Identity");
michael@0 63 this._log.Level = Log.Level[Svc.Prefs.get("log.logger.identity")];
michael@0 64
michael@0 65 this._basicPassword = null;
michael@0 66 this._basicPasswordAllowLookup = true;
michael@0 67 this._basicPasswordUpdated = false;
michael@0 68 this._syncKey = null;
michael@0 69 this._syncKeyAllowLookup = true;
michael@0 70 this._syncKeySet = false;
michael@0 71 this._syncKeyBundle = null;
michael@0 72 }
michael@0 73 IdentityManager.prototype = {
michael@0 74 _log: null,
michael@0 75
michael@0 76 _basicPassword: null,
michael@0 77 _basicPasswordAllowLookup: true,
michael@0 78 _basicPasswordUpdated: false,
michael@0 79
michael@0 80 _syncKey: null,
michael@0 81 _syncKeyAllowLookup: true,
michael@0 82 _syncKeySet: false,
michael@0 83
michael@0 84 _syncKeyBundle: null,
michael@0 85
michael@0 86 /**
michael@0 87 * Initialize the identity provider. Returns a promise that is resolved
michael@0 88 * when initialization is complete and the provider can be queried for
michael@0 89 * its state
michael@0 90 */
michael@0 91 initialize: function() {
michael@0 92 // Nothing to do for this identity provider.
michael@0 93 return Promise.resolve();
michael@0 94 },
michael@0 95
michael@0 96 finalize: function() {
michael@0 97 // Nothing to do for this identity provider.
michael@0 98 return Promise.resolve();
michael@0 99 },
michael@0 100
michael@0 101 /**
michael@0 102 * Called whenever Service.logout() is called.
michael@0 103 */
michael@0 104 logout: function() {
michael@0 105 // nothing to do for this identity provider.
michael@0 106 },
michael@0 107
michael@0 108 /**
michael@0 109 * Ensure the user is logged in. Returns a promise that resolves when
michael@0 110 * the user is logged in, or is rejected if the login attempt has failed.
michael@0 111 */
michael@0 112 ensureLoggedIn: function() {
michael@0 113 // nothing to do for this identity provider
michael@0 114 return Promise.resolve();
michael@0 115 },
michael@0 116
michael@0 117 /**
michael@0 118 * Indicates if the identity manager is still initializing
michael@0 119 */
michael@0 120 get readyToAuthenticate() {
michael@0 121 // We initialize in a fully sync manner, so we are always finished.
michael@0 122 return true;
michael@0 123 },
michael@0 124
michael@0 125 get account() {
michael@0 126 return Svc.Prefs.get("account", this.username);
michael@0 127 },
michael@0 128
michael@0 129 /**
michael@0 130 * Sets the active account name.
michael@0 131 *
michael@0 132 * This should almost always be called in favor of setting username, as
michael@0 133 * username is derived from account.
michael@0 134 *
michael@0 135 * Changing the account name has the side-effect of wiping out stored
michael@0 136 * credentials. Keep in mind that persistCredentials() will need to be called
michael@0 137 * to flush the changes to disk.
michael@0 138 *
michael@0 139 * Set this value to null to clear out identity information.
michael@0 140 */
michael@0 141 set account(value) {
michael@0 142 if (value) {
michael@0 143 value = value.toLowerCase();
michael@0 144 Svc.Prefs.set("account", value);
michael@0 145 } else {
michael@0 146 Svc.Prefs.reset("account");
michael@0 147 }
michael@0 148
michael@0 149 this.username = this.usernameFromAccount(value);
michael@0 150 },
michael@0 151
michael@0 152 get username() {
michael@0 153 return Svc.Prefs.get("username", null);
michael@0 154 },
michael@0 155
michael@0 156 /**
michael@0 157 * Set the username value.
michael@0 158 *
michael@0 159 * Changing the username has the side-effect of wiping credentials.
michael@0 160 */
michael@0 161 set username(value) {
michael@0 162 if (value) {
michael@0 163 value = value.toLowerCase();
michael@0 164
michael@0 165 if (value == this.username) {
michael@0 166 return;
michael@0 167 }
michael@0 168
michael@0 169 Svc.Prefs.set("username", value);
michael@0 170 } else {
michael@0 171 Svc.Prefs.reset("username");
michael@0 172 }
michael@0 173
michael@0 174 // If we change the username, we interpret this as a major change event
michael@0 175 // and wipe out the credentials.
michael@0 176 this._log.info("Username changed. Removing stored credentials.");
michael@0 177 this.resetCredentials();
michael@0 178 },
michael@0 179
michael@0 180 /**
michael@0 181 * Resets/Drops all credentials we hold for the current user.
michael@0 182 */
michael@0 183 resetCredentials: function() {
michael@0 184 this.basicPassword = null;
michael@0 185 this.resetSyncKey();
michael@0 186 },
michael@0 187
michael@0 188 /**
michael@0 189 * Resets/Drops the sync key we hold for the current user.
michael@0 190 */
michael@0 191 resetSyncKey: function() {
michael@0 192 this.syncKey = null;
michael@0 193 // syncKeyBundle cleared as a result of setting syncKey.
michael@0 194 },
michael@0 195
michael@0 196 /**
michael@0 197 * Obtains the HTTP Basic auth password.
michael@0 198 *
michael@0 199 * Returns a string if set or null if it is not set.
michael@0 200 */
michael@0 201 get basicPassword() {
michael@0 202 if (this._basicPasswordAllowLookup) {
michael@0 203 // We need a username to find the credentials.
michael@0 204 let username = this.username;
michael@0 205 if (!username) {
michael@0 206 return null;
michael@0 207 }
michael@0 208
michael@0 209 for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
michael@0 210 if (login.username.toLowerCase() == username) {
michael@0 211 // It should already be UTF-8 encoded, but we don't take any chances.
michael@0 212 this._basicPassword = Utils.encodeUTF8(login.password);
michael@0 213 }
michael@0 214 }
michael@0 215
michael@0 216 this._basicPasswordAllowLookup = false;
michael@0 217 }
michael@0 218
michael@0 219 return this._basicPassword;
michael@0 220 },
michael@0 221
michael@0 222 /**
michael@0 223 * Set the HTTP basic password to use.
michael@0 224 *
michael@0 225 * Changes will not persist unless persistSyncCredentials() is called.
michael@0 226 */
michael@0 227 set basicPassword(value) {
michael@0 228 // Wiping out value.
michael@0 229 if (!value) {
michael@0 230 this._log.info("Basic password has no value. Removing.");
michael@0 231 this._basicPassword = null;
michael@0 232 this._basicPasswordUpdated = true;
michael@0 233 this._basicPasswordAllowLookup = false;
michael@0 234 return;
michael@0 235 }
michael@0 236
michael@0 237 let username = this.username;
michael@0 238 if (!username) {
michael@0 239 throw new Error("basicPassword cannot be set before username.");
michael@0 240 }
michael@0 241
michael@0 242 this._log.info("Basic password being updated.");
michael@0 243 this._basicPassword = Utils.encodeUTF8(value);
michael@0 244 this._basicPasswordUpdated = true;
michael@0 245 },
michael@0 246
michael@0 247 /**
michael@0 248 * Obtain the Sync Key.
michael@0 249 *
michael@0 250 * This returns a 26 character "friendly" Base32 encoded string on success or
michael@0 251 * null if no Sync Key could be found.
michael@0 252 *
michael@0 253 * If the Sync Key hasn't been set in this session, this will look in the
michael@0 254 * password manager for the sync key.
michael@0 255 */
michael@0 256 get syncKey() {
michael@0 257 if (this._syncKeyAllowLookup) {
michael@0 258 let username = this.username;
michael@0 259 if (!username) {
michael@0 260 return null;
michael@0 261 }
michael@0 262
michael@0 263 for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
michael@0 264 if (login.username.toLowerCase() == username) {
michael@0 265 this._syncKey = login.password;
michael@0 266 }
michael@0 267 }
michael@0 268
michael@0 269 this._syncKeyAllowLookup = false;
michael@0 270 }
michael@0 271
michael@0 272 return this._syncKey;
michael@0 273 },
michael@0 274
michael@0 275 /**
michael@0 276 * Set the active Sync Key.
michael@0 277 *
michael@0 278 * If being set to null, the Sync Key and its derived SyncKeyBundle are
michael@0 279 * removed. However, the Sync Key won't be deleted from the password manager
michael@0 280 * until persistSyncCredentials() is called.
michael@0 281 *
michael@0 282 * If a value is provided, it should be a 26 or 32 character "friendly"
michael@0 283 * Base32 string for which Utils.isPassphrase() returns true.
michael@0 284 *
michael@0 285 * A side-effect of setting the Sync Key is that a SyncKeyBundle is
michael@0 286 * generated. For historical reasons, this will silently error out if the
michael@0 287 * value is not a proper Sync Key (!Utils.isPassphrase()). This should be
michael@0 288 * fixed in the future (once service.js is more sane) to throw if the passed
michael@0 289 * value is not valid.
michael@0 290 */
michael@0 291 set syncKey(value) {
michael@0 292 if (!value) {
michael@0 293 this._log.info("Sync Key has no value. Deleting.");
michael@0 294 this._syncKey = null;
michael@0 295 this._syncKeyBundle = null;
michael@0 296 this._syncKeyUpdated = true;
michael@0 297 return;
michael@0 298 }
michael@0 299
michael@0 300 if (!this.username) {
michael@0 301 throw new Error("syncKey cannot be set before username.");
michael@0 302 }
michael@0 303
michael@0 304 this._log.info("Sync Key being updated.");
michael@0 305 this._syncKey = value;
michael@0 306
michael@0 307 // Clear any cached Sync Key Bundle and regenerate it.
michael@0 308 this._syncKeyBundle = null;
michael@0 309 let bundle = this.syncKeyBundle;
michael@0 310
michael@0 311 this._syncKeyUpdated = true;
michael@0 312 },
michael@0 313
michael@0 314 /**
michael@0 315 * Obtain the active SyncKeyBundle.
michael@0 316 *
michael@0 317 * This returns a SyncKeyBundle representing a key pair derived from the
michael@0 318 * Sync Key on success. If no Sync Key is present or if the Sync Key is not
michael@0 319 * valid, this returns null.
michael@0 320 *
michael@0 321 * The SyncKeyBundle should be treated as immutable.
michael@0 322 */
michael@0 323 get syncKeyBundle() {
michael@0 324 // We can't obtain a bundle without a username set.
michael@0 325 if (!this.username) {
michael@0 326 this._log.warn("Attempted to obtain Sync Key Bundle with no username set!");
michael@0 327 return null;
michael@0 328 }
michael@0 329
michael@0 330 if (!this.syncKey) {
michael@0 331 this._log.warn("Attempted to obtain Sync Key Bundle with no Sync Key " +
michael@0 332 "set!");
michael@0 333 return null;
michael@0 334 }
michael@0 335
michael@0 336 if (!this._syncKeyBundle) {
michael@0 337 try {
michael@0 338 this._syncKeyBundle = new SyncKeyBundle(this.username, this.syncKey);
michael@0 339 } catch (ex) {
michael@0 340 this._log.warn(Utils.exceptionStr(ex));
michael@0 341 return null;
michael@0 342 }
michael@0 343 }
michael@0 344
michael@0 345 return this._syncKeyBundle;
michael@0 346 },
michael@0 347
michael@0 348 /**
michael@0 349 * The current state of the auth credentials.
michael@0 350 *
michael@0 351 * This essentially validates that enough credentials are available to use
michael@0 352 * Sync.
michael@0 353 */
michael@0 354 get currentAuthState() {
michael@0 355 if (!this.username) {
michael@0 356 return LOGIN_FAILED_NO_USERNAME;
michael@0 357 }
michael@0 358
michael@0 359 if (Utils.mpLocked()) {
michael@0 360 return STATUS_OK;
michael@0 361 }
michael@0 362
michael@0 363 if (!this.basicPassword) {
michael@0 364 return LOGIN_FAILED_NO_PASSWORD;
michael@0 365 }
michael@0 366
michael@0 367 if (!this.syncKey) {
michael@0 368 return LOGIN_FAILED_NO_PASSPHRASE;
michael@0 369 }
michael@0 370
michael@0 371 // If we have a Sync Key but no bundle, bundle creation failed, which
michael@0 372 // implies a bad Sync Key.
michael@0 373 if (!this.syncKeyBundle) {
michael@0 374 return LOGIN_FAILED_INVALID_PASSPHRASE;
michael@0 375 }
michael@0 376
michael@0 377 return STATUS_OK;
michael@0 378 },
michael@0 379
michael@0 380 /**
michael@0 381 * Persist credentials to password store.
michael@0 382 *
michael@0 383 * When credentials are updated, they are changed in memory only. This will
michael@0 384 * need to be called to save them to the underlying password store.
michael@0 385 *
michael@0 386 * If the password store is locked (e.g. if the master password hasn't been
michael@0 387 * entered), this could throw an exception.
michael@0 388 */
michael@0 389 persistCredentials: function persistCredentials(force) {
michael@0 390 if (this._basicPasswordUpdated || force) {
michael@0 391 if (this._basicPassword) {
michael@0 392 this._setLogin(PWDMGR_PASSWORD_REALM, this.username,
michael@0 393 this._basicPassword);
michael@0 394 } else {
michael@0 395 for each (let login in this._getLogins(PWDMGR_PASSWORD_REALM)) {
michael@0 396 Services.logins.removeLogin(login);
michael@0 397 }
michael@0 398 }
michael@0 399
michael@0 400 this._basicPasswordUpdated = false;
michael@0 401 }
michael@0 402
michael@0 403 if (this._syncKeyUpdated || force) {
michael@0 404 if (this._syncKey) {
michael@0 405 this._setLogin(PWDMGR_PASSPHRASE_REALM, this.username, this._syncKey);
michael@0 406 } else {
michael@0 407 for each (let login in this._getLogins(PWDMGR_PASSPHRASE_REALM)) {
michael@0 408 Services.logins.removeLogin(login);
michael@0 409 }
michael@0 410 }
michael@0 411
michael@0 412 this._syncKeyUpdated = false;
michael@0 413 }
michael@0 414
michael@0 415 },
michael@0 416
michael@0 417 /**
michael@0 418 * Deletes the Sync Key from the system.
michael@0 419 */
michael@0 420 deleteSyncKey: function deleteSyncKey() {
michael@0 421 this.syncKey = null;
michael@0 422 this.persistCredentials();
michael@0 423 },
michael@0 424
michael@0 425 hasBasicCredentials: function hasBasicCredentials() {
michael@0 426 // Because JavaScript.
michael@0 427 return this.username && this.basicPassword && true;
michael@0 428 },
michael@0 429
michael@0 430 /**
michael@0 431 * Obtains the array of basic logins from nsiPasswordManager.
michael@0 432 */
michael@0 433 _getLogins: function _getLogins(realm) {
michael@0 434 return Services.logins.findLogins({}, PWDMGR_HOST, null, realm);
michael@0 435 },
michael@0 436
michael@0 437 /**
michael@0 438 * Set a login in the password manager.
michael@0 439 *
michael@0 440 * This has the side-effect of deleting any other logins for the specified
michael@0 441 * realm.
michael@0 442 */
michael@0 443 _setLogin: function _setLogin(realm, username, password) {
michael@0 444 let exists = false;
michael@0 445 for each (let login in this._getLogins(realm)) {
michael@0 446 if (login.username == username && login.password == password) {
michael@0 447 exists = true;
michael@0 448 } else {
michael@0 449 this._log.debug("Pruning old login for " + username + " from " + realm);
michael@0 450 Services.logins.removeLogin(login);
michael@0 451 }
michael@0 452 }
michael@0 453
michael@0 454 if (exists) {
michael@0 455 return;
michael@0 456 }
michael@0 457
michael@0 458 this._log.debug("Updating saved password for " + username + " in " +
michael@0 459 realm);
michael@0 460
michael@0 461 let loginInfo = new Components.Constructor(
michael@0 462 "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
michael@0 463 let login = new loginInfo(PWDMGR_HOST, null, realm, username,
michael@0 464 password, "", "");
michael@0 465 Services.logins.addLogin(login);
michael@0 466 },
michael@0 467
michael@0 468 /**
michael@0 469 * Deletes Sync credentials from the password manager.
michael@0 470 */
michael@0 471 deleteSyncCredentials: function deleteSyncCredentials() {
michael@0 472 for (let host of Utils.getSyncCredentialsHosts()) {
michael@0 473 let logins = Services.logins.findLogins({}, host, "", "");
michael@0 474 for each (let login in logins) {
michael@0 475 Services.logins.removeLogin(login);
michael@0 476 }
michael@0 477 }
michael@0 478
michael@0 479 // Wait until after store is updated in case it fails.
michael@0 480 this._basicPassword = null;
michael@0 481 this._basicPasswordAllowLookup = true;
michael@0 482 this._basicPasswordUpdated = false;
michael@0 483
michael@0 484 this._syncKey = null;
michael@0 485 // this._syncKeyBundle is nullified as part of _syncKey setter.
michael@0 486 this._syncKeyAllowLookup = true;
michael@0 487 this._syncKeyUpdated = false;
michael@0 488 },
michael@0 489
michael@0 490 usernameFromAccount: function usernameFromAccount(value) {
michael@0 491 // If we encounter characters not allowed by the API (as found for
michael@0 492 // instance in an email address), hash the value.
michael@0 493 if (value && value.match(/[^A-Z0-9._-]/i)) {
michael@0 494 return Utils.sha1Base32(value.toLowerCase()).toLowerCase();
michael@0 495 }
michael@0 496
michael@0 497 return value ? value.toLowerCase() : value;
michael@0 498 },
michael@0 499
michael@0 500 /**
michael@0 501 * Obtain a function to be used for adding auth to Resource HTTP requests.
michael@0 502 */
michael@0 503 getResourceAuthenticator: function getResourceAuthenticator() {
michael@0 504 if (this.hasBasicCredentials()) {
michael@0 505 return this._onResourceRequestBasic.bind(this);
michael@0 506 }
michael@0 507
michael@0 508 return null;
michael@0 509 },
michael@0 510
michael@0 511 /**
michael@0 512 * Helper method to return an authenticator for basic Resource requests.
michael@0 513 */
michael@0 514 getBasicResourceAuthenticator:
michael@0 515 function getBasicResourceAuthenticator(username, password) {
michael@0 516
michael@0 517 return function basicAuthenticator(resource) {
michael@0 518 let value = "Basic " + btoa(username + ":" + password);
michael@0 519 return {headers: {authorization: value}};
michael@0 520 };
michael@0 521 },
michael@0 522
michael@0 523 _onResourceRequestBasic: function _onResourceRequestBasic(resource) {
michael@0 524 let value = "Basic " + btoa(this.username + ":" + this.basicPassword);
michael@0 525 return {headers: {authorization: value}};
michael@0 526 },
michael@0 527
michael@0 528 _onResourceRequestMAC: function _onResourceRequestMAC(resource, method) {
michael@0 529 // TODO Get identifier and key from somewhere.
michael@0 530 let identifier;
michael@0 531 let key;
michael@0 532 let result = Utils.computeHTTPMACSHA1(identifier, key, method, resource.uri);
michael@0 533
michael@0 534 return {headers: {authorization: result.header}};
michael@0 535 },
michael@0 536
michael@0 537 /**
michael@0 538 * Obtain a function to be used for adding auth to RESTRequest instances.
michael@0 539 */
michael@0 540 getRESTRequestAuthenticator: function getRESTRequestAuthenticator() {
michael@0 541 if (this.hasBasicCredentials()) {
michael@0 542 return this.onRESTRequestBasic.bind(this);
michael@0 543 }
michael@0 544
michael@0 545 return null;
michael@0 546 },
michael@0 547
michael@0 548 onRESTRequestBasic: function onRESTRequestBasic(request) {
michael@0 549 let up = this.username + ":" + this.basicPassword;
michael@0 550 request.setHeader("authorization", "Basic " + btoa(up));
michael@0 551 },
michael@0 552
michael@0 553 createClusterManager: function(service) {
michael@0 554 Cu.import("resource://services-sync/stages/cluster.js");
michael@0 555 return new ClusterManager(service);
michael@0 556 },
michael@0 557
michael@0 558 offerSyncOptions: function () {
michael@0 559 // Do nothing for Sync 1.1.
michael@0 560 return {accepted: true};
michael@0 561 },
michael@0 562 };

mercurial