From 19c7270342563b5613f5d3b75be40e56b6eff516 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Wed, 18 Jun 2014 10:42:15 +0100 Subject: [PATCH] Bug 1020859 Part 1 - Make HawkClient return all the response details for a request, and make deriveHawkCredentials common code. r=jparsons --- services/common/hawkclient.js | 6 +-- services/common/hawkrequest.js | 43 ++++++++++++++++ services/common/tests/unit/test_hawkclient.js | 10 ++-- .../common/tests/unit/test_hawkrequest.js | 25 +++++++++ services/fxaccounts/FxAccountsClient.jsm | 51 ++++--------------- .../fxaccounts/tests/xpcshell/test_client.js | 24 +-------- services/mobileid/MobileIdentityClient.jsm | 20 +++----- services/mobileid/MobileIdentityCommon.jsm | 2 +- 8 files changed, 95 insertions(+), 86 deletions(-) diff --git a/services/common/hawkclient.js b/services/common/hawkclient.js index 3d751ba97ae..cb96cbbf411 100644 --- a/services/common/hawkclient.js +++ b/services/common/hawkclient.js @@ -176,7 +176,7 @@ this.HawkClient.prototype = { * An object that can be encodable as JSON as the payload of the * request * @return Promise - * Returns a promise that resolves to the text response of the API call, + * Returns a promise that resolves to the response of the API call, * or is rejected with an error. If the server response can be parsed * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. @@ -239,8 +239,8 @@ this.HawkClient.prototype = { return deferred.reject(self._constructError(restResponse, "Request failed")); } // It's up to the caller to know how to decode the response. - // We just return the raw text. - deferred.resolve(this.response.body); + // We just return the whole response. + deferred.resolve(this.response); }; function onComplete(error) { diff --git a/services/common/hawkrequest.js b/services/common/hawkrequest.js index baafeefddeb..04dd3ceedc8 100644 --- a/services/common/hawkrequest.js +++ b/services/common/hawkrequest.js @@ -8,6 +8,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; this.EXPORTED_SYMBOLS = [ "HAWKAuthenticatedRESTRequest", + "deriveHawkCredentials" ]; Cu.import("resource://gre/modules/Preferences.jsm"); @@ -15,6 +16,8 @@ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://services-common/rest.js"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://gre/modules/Credentials.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", "resource://services-crypto/utils.js"); @@ -90,6 +93,46 @@ HAWKAuthenticatedRESTRequest.prototype = { } }; + +/** + * Generic function to derive Hawk credentials. + * + * Hawk credentials are derived using shared secrets, which depend on the token + * in use. + * + * @param tokenHex + * The current session token encoded in hex + * @param context + * A context for the credentials. A protocol version will be prepended + * to the context, see Credentials.keyWord for more information. + * @param size + * The size in bytes of the expected derived buffer, + * defaults to 3 * 32. + * @return credentials + * Returns an object: + * { + * algorithm: sha256 + * id: the Hawk id (from the first 32 bytes derived) + * key: the Hawk key (from bytes 32 to 64) + * extra: size - 64 extra bytes (if size > 64) + * } + */ +function deriveHawkCredentials(tokenHex, context, size=96) { + let token = CommonUtils.hexToBytes(tokenHex); + let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size); + + let result = { + algorithm: "sha256", + key: out.slice(32, 64), + id: CommonUtils.bytesAsHex(out.slice(0, 32)) + }; + if (size > 64) { + result.extra = out.slice(64); + } + + return result; +} + // With hawk request, we send the user's accepted-languages with each request. // To keep the number of times we read this pref at a minimum, maintain the // preference in a stateful object that notices and updates itself when the diff --git a/services/common/tests/unit/test_hawkclient.js b/services/common/tests/unit/test_hawkclient.js index e28cf0562cc..e66bfdb4824 100644 --- a/services/common/tests/unit/test_hawkclient.js +++ b/services/common/tests/unit/test_hawkclient.js @@ -59,7 +59,7 @@ add_task(function test_authenticated_get_request() { let client = new HawkClient(server.baseURI); let response = yield client.request("/foo", method, TEST_CREDS); - let result = JSON.parse(response); + let result = JSON.parse(response.body); do_check_eq("Great Success!", result.msg); @@ -81,7 +81,7 @@ add_task(function test_authenticated_post_request() { let client = new HawkClient(server.baseURI); let response = yield client.request("/foo", method, TEST_CREDS, {foo: "bar"}); - let result = JSON.parse(response); + let result = JSON.parse(response.body); do_check_eq("bar", result.foo); @@ -103,7 +103,7 @@ add_task(function test_credentials_optional() { let client = new HawkClient(server.baseURI); let result = yield client.request("/foo", method); // credentials undefined - do_check_eq(JSON.parse(result).msg, "you're in the friend zone"); + do_check_eq(JSON.parse(result.body).msg, "you're in the friend zone"); yield deferredStop(server); }); @@ -242,7 +242,7 @@ add_task(function test_2xx_success() { let response = yield client.request("/foo", method, credentials); // Shouldn't be any content in a 202 - do_check_eq(response, ""); + do_check_eq(response.body, ""); yield deferredStop(server); }); @@ -297,7 +297,7 @@ add_task(function test_retry_request_on_fail() { // Request will have bad timestamp; client will retry once let response = yield client.request("/maybe", method, credentials); - do_check_eq(response, "i love you!!!"); + do_check_eq(response.body, "i love you!!!"); yield deferredStop(server); }); diff --git a/services/common/tests/unit/test_hawkrequest.js b/services/common/tests/unit/test_hawkrequest.js index 067287843fe..ac42d5a7bb3 100644 --- a/services/common/tests/unit/test_hawkrequest.js +++ b/services/common/tests/unit/test_hawkrequest.js @@ -7,6 +7,18 @@ Cu.import("resource://gre/modules/Log.jsm"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/hawkrequest.js"); +// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc +let SESSION_KEYS = { + sessionToken: h("a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf"+ + "b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"), + + tokenID: h("c0a29dcf46174973 da1378696e4c82ae"+ + "10f723cf4f4d9f75 e39f4ae3851595ab"), + + reqHMACkey: h("9d8f22998ee7f579 8b887042466b72d5"+ + "3e56ab0c094388bf 65831f702d2febc0"), +}; + function do_register_cleanup() { Services.prefs.resetUserPrefs(); @@ -201,3 +213,16 @@ add_test(function test_hawk_language_pref_changed() { } }); +add_task(function test_deriveHawkCredentials() { + let credentials = deriveHawkCredentials( + SESSION_KEYS.sessionToken, "sessionToken"); + + do_check_eq(credentials.algorithm, "sha256"); + do_check_eq(credentials.id, SESSION_KEYS.tokenID); + do_check_eq(CommonUtils.bytesAsHex(credentials.key), SESSION_KEYS.reqHMACkey); +}); + +// turn formatted test vectors into normal hex strings +function h(hexStr) { + return hexStr.replace(/\s+/g, ""); +} diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm index 3affeea4561..840a7a87c2b 100644 --- a/services/fxaccounts/FxAccountsClient.jsm +++ b/services/fxaccounts/FxAccountsClient.jsm @@ -11,6 +11,7 @@ Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-common/hawkclient.js"); +Cu.import("resource://services-common/hawkrequest.js"); Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://gre/modules/FxAccountsCommon.js"); Cu.import("resource://gre/modules/Credentials.jsm"); @@ -157,7 +158,7 @@ this.FxAccountsClient.prototype = { */ signOut: function (sessionTokenHex) { return this._request("/session/destroy", "POST", - this._deriveHawkCredentials(sessionTokenHex, "sessionToken")); + deriveHawkCredentials(sessionTokenHex, "sessionToken")); }, /** @@ -169,7 +170,7 @@ this.FxAccountsClient.prototype = { */ recoveryEmailStatus: function (sessionTokenHex) { return this._request("/recovery_email/status", "GET", - this._deriveHawkCredentials(sessionTokenHex, "sessionToken")); + deriveHawkCredentials(sessionTokenHex, "sessionToken")); }, /** @@ -181,7 +182,7 @@ this.FxAccountsClient.prototype = { */ resendVerificationEmail: function(sessionTokenHex) { return this._request("/recovery_email/resend_code", "POST", - this._deriveHawkCredentials(sessionTokenHex, "sessionToken")); + deriveHawkCredentials(sessionTokenHex, "sessionToken")); }, /** @@ -198,7 +199,7 @@ this.FxAccountsClient.prototype = { * } */ accountKeys: function (keyFetchTokenHex) { - let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken"); + let creds = deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken"); let keyRequestKey = creds.extra.slice(0, 32); let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined, Credentials.keyWord("account/keys"), 3 * 32); @@ -247,7 +248,7 @@ this.FxAccountsClient.prototype = { * https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12 */ signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) { - let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken"); + let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken"); let body = { publicKey: serializedPublicKey, duration: lifetime }; @@ -309,38 +310,6 @@ this.FxAccountsClient.prototype = { ); }, - /** - * The FxA auth server expects requests to certain endpoints to be authorized using Hawk. - * Hawk credentials are derived using shared secrets, which depend on the context - * (e.g. sessionToken vs. keyFetchToken). - * - * @param tokenHex - * The current session token encoded in hex - * @param context - * A context for the credentials - * @param size - * The size in bytes of the expected derived buffer - * @return credentials - * Returns an object: - * { - * algorithm: sha256 - * id: the Hawk id (from the first 32 bytes derived) - * key: the Hawk key (from bytes 32 to 64) - * extra: size - 64 extra bytes - * } - */ - _deriveHawkCredentials: function (tokenHex, context, size) { - let token = CommonUtils.hexToBytes(tokenHex); - let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size || 3 * 32); - - return { - algorithm: "sha256", - key: out.slice(32, 64), - extra: out.slice(64), - id: CommonUtils.bytesAsHex(out.slice(0, 32)) - }; - }, - _clearBackoff: function() { this.backoffError = null; }, @@ -379,12 +348,12 @@ this.FxAccountsClient.prototype = { } this.hawk.request(path, method, credentials, jsonPayload).then( - (responseText) => { + (response) => { try { - let response = JSON.parse(responseText); - deferred.resolve(response); + let responseObj = JSON.parse(response.body); + deferred.resolve(responseObj); } catch (err) { - log.error("json parse error on response: " + responseText); + log.error("json parse error on response: " + response.body); deferred.reject({error: err}); } }, diff --git a/services/fxaccounts/tests/xpcshell/test_client.js b/services/fxaccounts/tests/xpcshell/test_client.js index b6344b12108..fad064f49d3 100644 --- a/services/fxaccounts/tests/xpcshell/test_client.js +++ b/services/fxaccounts/tests/xpcshell/test_client.js @@ -6,6 +6,7 @@ Cu.import("resource://gre/modules/FxAccountsClient.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-common/hawkrequest.js"); Cu.import("resource://services-crypto/utils.js"); const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"; @@ -33,18 +34,6 @@ let ACCOUNT_KEYS = { "5051525354555657 58595a5b5c5d5e5f"), }; -// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc -let SESSION_KEYS = { - sessionToken: h("a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf"+ - "b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"), - - tokenID: h("c0a29dcf46174973 da1378696e4c82ae"+ - "10f723cf4f4d9f75 e39f4ae3851595ab"), - - reqHMACkey: h("9d8f22998ee7f579 8b887042466b72d5"+ - "3e56ab0c094388bf 65831f702d2febc0"), -}; - function deferredStop(server) { let deferred = Promise.defer(); server.stop(deferred.resolve); @@ -670,17 +659,6 @@ add_task(function test_email_case() { yield deferredStop(server); }); -add_task(function test__deriveHawkCredentials() { - let client = new FxAccountsClient("https://example.org"); - - let credentials = client._deriveHawkCredentials( - SESSION_KEYS.sessionToken, "sessionToken"); - - do_check_eq(credentials.algorithm, "sha256"); - do_check_eq(credentials.id, SESSION_KEYS.tokenID); - do_check_eq(CommonUtils.bytesAsHex(credentials.key), SESSION_KEYS.reqHMACkey); -}); - // turn formatted test vectors into normal hex strings function h(hexStr) { return hexStr.replace(/\s+/g, ""); diff --git a/services/mobileid/MobileIdentityClient.jsm b/services/mobileid/MobileIdentityClient.jsm index af1f95e00c9..25a05a9862c 100644 --- a/services/mobileid/MobileIdentityClient.jsm +++ b/services/mobileid/MobileIdentityClient.jsm @@ -12,6 +12,7 @@ this.EXPORTED_SYMBOLS = ["MobileIdentityClient"]; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://services-common/hawkclient.js"); +Cu.import("resource://services-common/hawkrequest.js"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://gre/modules/MobileIdentityCommon.jsm"); @@ -104,15 +105,8 @@ this.MobileIdentityClient.prototype = { * } */ _deriveHawkCredentials: function(aSessionToken) { - let token = CommonUtils.hexToBytes(aSessionToken); - let out = CryptoUtils.hkdf(token, undefined, - CREDENTIALS_DERIVATION_INFO, - CREDENTIALS_DERIVATION_SIZE); - return { - algorithm: "sha256", - key: CommonUtils.bytesAsHex(out.slice(32, 64)), - id: CommonUtils.bytesAsHex(out.slice(0, 32)) - }; + return deriveHawkCredentials(aSessionToken, CREDENTIALS_DERIVATION_INFO, + CREDENTIALS_DERIVATION_SIZE); }, /** @@ -136,11 +130,11 @@ this.MobileIdentityClient.prototype = { let deferred = Promise.defer(); this.hawk.request(path, method, credentials, jsonPayload).then( - (responseText) => { - log.debug("MobileIdentityClient -> responseText " + responseText); + (response) => { + log.debug("MobileIdentityClient -> response.body " + response.body); try { - let response = JSON.parse(responseText); - deferred.resolve(response); + let responseObj = JSON.parse(response.body); + deferred.resolve(responseObj); } catch (err) { deferred.reject({error: err}); } diff --git a/services/mobileid/MobileIdentityCommon.jsm b/services/mobileid/MobileIdentityCommon.jsm index ed2b04ed109..df4c7ea1231 100644 --- a/services/mobileid/MobileIdentityCommon.jsm +++ b/services/mobileid/MobileIdentityCommon.jsm @@ -52,7 +52,7 @@ this.UNREGISTER = "/unregister"; // Server consts. this.SERVER_URL = Services.prefs.getCharPref("services.mobileid.server.uri"); -this.CREDENTIALS_DERIVATION_INFO = "identity.mozilla.com/picl/v1/sessionToken"; +this.CREDENTIALS_DERIVATION_INFO = "sessionToken"; this.CREDENTIALS_DERIVATION_SIZE = 2 * 32; this.SILENT_SMS_RECEIVED_TOPIC = "silent-sms-received";