Wed, 31 Dec 2014 06:09:35 +0100
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 |
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 |