services/fxaccounts/FxAccountsClient.jsm

Wed, 31 Dec 2014 07:53:36 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:53:36 +0100
branch
TOR_BUG_3246
changeset 5
4ab42b5ab56c
permissions
-rw-r--r--

Correct small whitespace inconsistency, lost while renaming variables.

     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
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
     7 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
     9 Cu.import("resource://gre/modules/Log.jsm");
    10 Cu.import("resource://gre/modules/Promise.jsm");
    11 Cu.import("resource://gre/modules/Services.jsm");
    12 Cu.import("resource://services-common/utils.js");
    13 Cu.import("resource://services-common/hawkclient.js");
    14 Cu.import("resource://services-crypto/utils.js");
    15 Cu.import("resource://gre/modules/FxAccountsCommon.js");
    16 Cu.import("resource://gre/modules/Credentials.jsm");
    18 const HOST = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
    20 this.FxAccountsClient = function(host = HOST) {
    21   this.host = host;
    23   // The FxA auth server expects requests to certain endpoints to be authorized
    24   // using Hawk.
    25   this.hawk = new HawkClient(host);
    26   this.hawk.observerPrefix = "FxA:hawk";
    28   // Manage server backoff state. C.f.
    29   // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol
    30   this.backoffError = null;
    31 };
    33 this.FxAccountsClient.prototype = {
    35   /**
    36    * Return client clock offset, in milliseconds, as determined by hawk client.
    37    * Provided because callers should not have to know about hawk
    38    * implementation.
    39    *
    40    * The offset is the number of milliseconds that must be added to the client
    41    * clock to make it equal to the server clock.  For example, if the client is
    42    * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
    43    */
    44   get localtimeOffsetMsec() {
    45     return this.hawk.localtimeOffsetMsec;
    46   },
    48   /*
    49    * Return current time in milliseconds
    50    *
    51    * Not used by this module, but made available to the FxAccounts.jsm
    52    * that uses this client.
    53    */
    54   now: function() {
    55     return this.hawk.now();
    56   },
    58   /**
    59    * Create a new Firefox Account and authenticate
    60    *
    61    * @param email
    62    *        The email address for the account (utf8)
    63    * @param password
    64    *        The user's password
    65    * @return Promise
    66    *        Returns a promise that resolves to an object:
    67    *        {
    68    *          uid: the user's unique ID (hex)
    69    *          sessionToken: a session token (hex)
    70    *          keyFetchToken: a key fetch token (hex)
    71    *        }
    72    */
    73   signUp: function(email, password) {
    74     return Credentials.setup(email, password).then((creds) => {
    75       let data = {
    76         email: creds.emailUTF8,
    77         authPW: CommonUtils.bytesAsHex(creds.authPW),
    78       };
    79       return this._request("/account/create", "POST", null, data);
    80     });
    81   },
    83   /**
    84    * Authenticate and create a new session with the Firefox Account API server
    85    *
    86    * @param email
    87    *        The email address for the account (utf8)
    88    * @param password
    89    *        The user's password
    90    * @param [getKeys=false]
    91    *        If set to true the keyFetchToken will be retrieved
    92    * @param [retryOK=true]
    93    *        If capitalization of the email is wrong and retryOK is set to true,
    94    *        we will retry with the suggested capitalization from the server
    95    * @return Promise
    96    *        Returns a promise that resolves to an object:
    97    *        {
    98    *          authAt: authentication time for the session (seconds since epoch)
    99    *          email: the primary email for this account
   100    *          keyFetchToken: a key fetch token (hex)
   101    *          sessionToken: a session token (hex)
   102    *          uid: the user's unique ID (hex)
   103    *          unwrapBKey: used to unwrap kB, derived locally from the
   104    *                      password (not revealed to the FxA server)
   105    *          verified: flag indicating verification status of the email
   106    *        }
   107    */
   108   signIn: function signIn(email, password, getKeys=false, retryOK=true) {
   109     return Credentials.setup(email, password).then((creds) => {
   110       let data = {
   111         authPW: CommonUtils.bytesAsHex(creds.authPW),
   112         email: creds.emailUTF8,
   113       };
   114       let keys = getKeys ? "?keys=true" : "";
   116       return this._request("/account/login" + keys, "POST", null, data).then(
   117         // Include the canonical capitalization of the email in the response so
   118         // the caller can set its signed-in user state accordingly.
   119         result => {
   120           result.email = data.email;
   121           result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey);
   123           return result;
   124         },
   125         error => {
   126           log.debug("signIn error: " + JSON.stringify(error));
   127           // If the user entered an email with different capitalization from
   128           // what's stored in the database (e.g., Greta.Garbo@gmail.COM as
   129           // opposed to greta.garbo@gmail.com), the server will respond with a
   130           // errno 120 (code 400) and the expected capitalization of the email.
   131           // We retry with this email exactly once.  If successful, we use the
   132           // server's version of the email as the signed-in-user's email. This
   133           // is necessary because the email also serves as salt; so we must be
   134           // in agreement with the server on capitalization.
   135           //
   136           // API reference:
   137           // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md
   138           if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) {
   139             if (!error.email) {
   140               log.error("Server returned errno 120 but did not provide email");
   141               throw error;
   142             }
   143             return this.signIn(error.email, password, getKeys, false);
   144           }
   145           throw error;
   146         }
   147       );
   148     });
   149   },
   151   /**
   152    * Destroy the current session with the Firefox Account API server
   153    *
   154    * @param sessionTokenHex
   155    *        The session token encoded in hex
   156    * @return Promise
   157    */
   158   signOut: function (sessionTokenHex) {
   159     return this._request("/session/destroy", "POST",
   160       this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
   161   },
   163   /**
   164    * Check the verification status of the user's FxA email address
   165    *
   166    * @param sessionTokenHex
   167    *        The current session token encoded in hex
   168    * @return Promise
   169    */
   170   recoveryEmailStatus: function (sessionTokenHex) {
   171     return this._request("/recovery_email/status", "GET",
   172       this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
   173   },
   175   /**
   176    * Resend the verification email for the user
   177    *
   178    * @param sessionTokenHex
   179    *        The current token encoded in hex
   180    * @return Promise
   181    */
   182   resendVerificationEmail: function(sessionTokenHex) {
   183     return this._request("/recovery_email/resend_code", "POST",
   184       this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
   185   },
   187   /**
   188    * Retrieve encryption keys
   189    *
   190    * @param keyFetchTokenHex
   191    *        A one-time use key fetch token encoded in hex
   192    * @return Promise
   193    *        Returns a promise that resolves to an object:
   194    *        {
   195    *          kA: an encryption key for recevorable data (bytes)
   196    *          wrapKB: an encryption key that requires knowledge of the
   197    *                  user's password (bytes)
   198    *        }
   199    */
   200   accountKeys: function (keyFetchTokenHex) {
   201     let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
   202     let keyRequestKey = creds.extra.slice(0, 32);
   203     let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
   204                                      Credentials.keyWord("account/keys"), 3 * 32);
   205     let respHMACKey = morecreds.slice(0, 32);
   206     let respXORKey = morecreds.slice(32, 96);
   208     return this._request("/account/keys", "GET", creds).then(resp => {
   209       if (!resp.bundle) {
   210         throw new Error("failed to retrieve keys");
   211       }
   213       let bundle = CommonUtils.hexToBytes(resp.bundle);
   214       let mac = bundle.slice(-32);
   216       let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
   217         CryptoUtils.makeHMACKey(respHMACKey));
   219       let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
   220       if (mac !== bundleMAC) {
   221         throw new Error("error unbundling encryption keys");
   222       }
   224       let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
   226       return {
   227         kA: keyAWrapB.slice(0, 32),
   228         wrapKB: keyAWrapB.slice(32)
   229       };
   230     });
   231   },
   233   /**
   234    * Sends a public key to the FxA API server and returns a signed certificate
   235    *
   236    * @param sessionTokenHex
   237    *        The current session token encoded in hex
   238    * @param serializedPublicKey
   239    *        A public key (usually generated by jwcrypto)
   240    * @param lifetime
   241    *        The lifetime of the certificate
   242    * @return Promise
   243    *        Returns a promise that resolves to the signed certificate. The certificate
   244    *        can be used to generate a Persona assertion.
   245    */
   246   signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
   247     let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken");
   249     let body = { publicKey: serializedPublicKey,
   250                  duration: lifetime };
   251     return Promise.resolve()
   252       .then(_ => this._request("/certificate/sign", "POST", creds, body))
   253       .then(resp => resp.cert,
   254             err => {
   255               log.error("HAWK.signCertificate error: " + JSON.stringify(err));
   256               throw err;
   257             });
   258   },
   260   /**
   261    * Determine if an account exists
   262    *
   263    * @param email
   264    *        The email address to check
   265    * @return Promise
   266    *        The promise resolves to true if the account exists, or false
   267    *        if it doesn't. The promise is rejected on other errors.
   268    */
   269   accountExists: function (email) {
   270     return this.signIn(email, "").then(
   271       (cantHappen) => {
   272         throw new Error("How did I sign in with an empty password?");
   273       },
   274       (expectedError) => {
   275         switch (expectedError.errno) {
   276           case ERRNO_ACCOUNT_DOES_NOT_EXIST:
   277             return false;
   278             break;
   279           case ERRNO_INCORRECT_PASSWORD:
   280             return true;
   281             break;
   282           default:
   283             // not so expected, any more ...
   284             throw expectedError;
   285             break;
   286         }
   287       }
   288     );
   289   },
   291   /**
   292    * The FxA auth server expects requests to certain endpoints to be authorized using Hawk.
   293    * Hawk credentials are derived using shared secrets, which depend on the context
   294    * (e.g. sessionToken vs. keyFetchToken).
   295    *
   296    * @param tokenHex
   297    *        The current session token encoded in hex
   298    * @param context
   299    *        A context for the credentials
   300    * @param size
   301    *        The size in bytes of the expected derived buffer
   302    * @return credentials
   303    *        Returns an object:
   304    *        {
   305    *          algorithm: sha256
   306    *          id: the Hawk id (from the first 32 bytes derived)
   307    *          key: the Hawk key (from bytes 32 to 64)
   308    *          extra: size - 64 extra bytes
   309    *        }
   310    */
   311   _deriveHawkCredentials: function (tokenHex, context, size) {
   312     let token = CommonUtils.hexToBytes(tokenHex);
   313     let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size || 3 * 32);
   315     return {
   316       algorithm: "sha256",
   317       key: out.slice(32, 64),
   318       extra: out.slice(64),
   319       id: CommonUtils.bytesAsHex(out.slice(0, 32))
   320     };
   321   },
   323   _clearBackoff: function() {
   324       this.backoffError = null;
   325   },
   327   /**
   328    * A general method for sending raw API calls to the FxA auth server.
   329    * All request bodies and responses are JSON.
   330    *
   331    * @param path
   332    *        API endpoint path
   333    * @param method
   334    *        The HTTP request method
   335    * @param credentials
   336    *        Hawk credentials
   337    * @param jsonPayload
   338    *        A JSON payload
   339    * @return Promise
   340    *        Returns a promise that resolves to the JSON response of the API call,
   341    *        or is rejected with an error. Error responses have the following properties:
   342    *        {
   343    *          "code": 400, // matches the HTTP status code
   344    *          "errno": 107, // stable application-level error number
   345    *          "error": "Bad Request", // string description of the error type
   346    *          "message": "the value of salt is not allowed to be undefined",
   347    *          "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
   348    *        }
   349    */
   350   _request: function hawkRequest(path, method, credentials, jsonPayload) {
   351     let deferred = Promise.defer();
   353     // We were asked to back off.
   354     if (this.backoffError) {
   355       log.debug("Received new request during backoff, re-rejecting.");
   356       deferred.reject(this.backoffError);
   357       return deferred.promise;
   358     }
   360     this.hawk.request(path, method, credentials, jsonPayload).then(
   361       (responseText) => {
   362         try {
   363           let response = JSON.parse(responseText);
   364           deferred.resolve(response);
   365         } catch (err) {
   366           log.error("json parse error on response: " + responseText);
   367           deferred.reject({error: err});
   368         }
   369       },
   371       (error) => {
   372         log.error("error " + method + "ing " + path + ": " + JSON.stringify(error));
   373         if (error.retryAfter) {
   374           log.debug("Received backoff response; caching error as flag.");
   375           this.backoffError = error;
   376           // Schedule clearing of cached-error-as-flag.
   377           CommonUtils.namedTimer(
   378             this._clearBackoff,
   379             error.retryAfter * 1000,
   380             this,
   381             "fxaBackoffTimer"
   382            );
   383 	}
   384         deferred.reject(error);
   385       }
   386     );
   388     return deferred.promise;
   389   },
   390 };

mercurial