services/sync/modules/identity.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial