services/sync/modules/identity.js

changeset 0
6474c204b198
     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 +};

mercurial