diff options
Diffstat (limited to 'services/common/hawkrequest.js')
-rw-r--r-- | services/common/hawkrequest.js | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/services/common/hawkrequest.js b/services/common/hawkrequest.js new file mode 100644 index 000000000..454960b7b --- /dev/null +++ b/services/common/hawkrequest.js @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +this.EXPORTED_SYMBOLS = [ + "HAWKAuthenticatedRESTRequest", + "deriveHawkCredentials" +]; + +Cu.import("resource://gre/modules/Preferences.jsm"); +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"); + +const Prefs = new Preferences("services.common.rest."); + +/** + * Single-use HAWK-authenticated HTTP requests to RESTish resources. + * + * @param uri + * (String) URI for the RESTRequest constructor + * + * @param credentials + * (Object) Optional credentials for computing HAWK authentication + * header. + * + * @param payloadObj + * (Object) Optional object to be converted to JSON payload + * + * @param extra + * (Object) Optional extra params for HAWK header computation. + * Valid properties are: + * + * now: <current time in milliseconds>, + * localtimeOffsetMsec: <local clock offset vs server>, + * headers: <An object with header/value pairs to be sent + * as headers on the request> + * + * extra.localtimeOffsetMsec is the value in milliseconds that must be added to + * the local clock to make it agree with the server's clock. For instance, if + * the local clock is two minutes ahead of the server, the time offset in + * milliseconds will be -120000. + */ + +this.HAWKAuthenticatedRESTRequest = + function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) { + RESTRequest.call(this, uri); + + this.credentials = credentials; + this.now = extra.now || Date.now(); + this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; + this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec)); + this.extraHeaders = extra.headers || {}; + + // Expose for testing + this._intl = getIntl(); +}; +HAWKAuthenticatedRESTRequest.prototype = { + __proto__: RESTRequest.prototype, + + dispatch: function dispatch(method, data, onComplete, onProgress) { + let contentType = "text/plain"; + if (method == "POST" || method == "PUT" || method == "PATCH") { + contentType = "application/json"; + } + if (this.credentials) { + let options = { + now: this.now, + localtimeOffsetMsec: this.localtimeOffsetMsec, + credentials: this.credentials, + payload: data && JSON.stringify(data) || "", + contentType: contentType, + }; + let header = CryptoUtils.computeHAWK(this.uri, method, options); + this.setHeader("Authorization", header.field); + this._log.trace("hawk auth header: " + header.field); + } + + for (let header in this.extraHeaders) { + this.setHeader(header, this.extraHeaders[header]); + } + + this.setHeader("Content-Type", contentType); + + this.setHeader("Accept-Language", this._intl.accept_languages); + + return RESTRequest.prototype.dispatch.call( + this, method, data, onComplete, onProgress + ); + } +}; + + +/** + * 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) + * } + */ +this.deriveHawkCredentials = function deriveHawkCredentials(tokenHex, + context, + size = 96, + hexKey = false) { + let token = CommonUtils.hexToBytes(tokenHex); + let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size); + + let result = { + algorithm: "sha256", + key: hexKey ? CommonUtils.bytesAsHex(out.slice(32, 64)) : 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 +// pref is changed. +this.Intl = function Intl() { + // We won't actually query the pref until the first time we need it + this._accepted = ""; + this._everRead = false; + this._log = Log.repository.getLogger("Services.common.RESTRequest"); + this._log.level = Log.Level[Prefs.get("log.logger.rest.request")]; + this.init(); +}; + +this.Intl.prototype = { + init: function() { + Services.prefs.addObserver("intl.accept_languages", this, false); + }, + + uninit: function() { + Services.prefs.removeObserver("intl.accept_languages", this); + }, + + observe: function(subject, topic, data) { + this.readPref(); + }, + + readPref: function() { + this._everRead = true; + try { + this._accepted = Services.prefs.getComplexValue( + "intl.accept_languages", Ci.nsIPrefLocalizedString).data; + } catch (err) { + this._log.error("Error reading intl.accept_languages pref", err); + } + }, + + get accept_languages() { + if (!this._everRead) { + this.readPref(); + } + return this._accepted; + }, +}; + +// Singleton getter for Intl, creating an instance only when we first need it. +var intl = null; +function getIntl() { + if (!intl) { + intl = new Intl(); + } + return intl; +} + |