services/fxaccounts/FxAccountsClient.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

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

mercurial