diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js index 6b92bee2ccd1..efe7ee09d900 100644 --- a/services/sync/modules/util.js +++ b/services/sync/modules/util.js @@ -755,6 +755,130 @@ let Utils = { return Utils.encodeKeyBase32(atob(encodedKey)); }, + /** + * Compute the HTTP MAC SHA-1 for an HTTP request. + * + * @param identifier + * (string) MAC Key Identifier. + * @param key + * (string) MAC Key. + * @param method + * (string) HTTP request method. + * @param URI + * (nsIURI) HTTP request URI. + * @param extra + * (object) Optional extra parameters. Valid keys are: + * nonce_bytes - How many bytes the nonce should be. This defaults + * to 8. Note that this many bytes are Base64 encoded, so the + * string length of the nonce will be longer than this value. + * ts - Timestamp to use. Should only be defined for testing. + * nonce - String nonce. Should only be defined for testing as this + * function will generate a cryptographically secure random one + * if not defined. + * ext - Extra string to be included in MAC. Per the HTTP MAC spec, + * the format is undefined and thus application specific. + * @returns + * (object) Contains results of operation and input arguments (for + * symmetry). The object has the following keys: + * + * identifier - (string) MAC Key Identifier (from arguments). + * key - (string) MAC Key (from arguments). + * method - (string) HTTP request method (from arguments). + * hostname - (string) HTTP hostname used (derived from arguments). + * port - (string) HTTP port number used (derived from arguments). + * mac - (string) Raw HMAC digest bytes. + * getHeader - (function) Call to obtain the string Authorization + * header value for this invocation. + * nonce - (string) Nonce value used. + * ts - (number) Integer seconds since Unix epoch that was used. + */ + computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method, + uri, extra) { + let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000); + let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8; + + // We are allowed to use more than the Base64 alphabet if we want. + let nonce = (extra && extra.nonce) + ? extra.nonce + : btoa(Utils.generateRandomBytes(nonce_bytes)); + + let host = uri.asciiHost; + let port; + let usedMethod = method.toUpperCase(); + + if (uri.port != -1) { + port = uri.port; + } else if (uri.scheme == "http") { + port = "80"; + } else if (uri.scheme == "https") { + port = "443"; + } else { + throw new Error("Unsupported URI scheme: " + uri.scheme); + } + + let ext = (extra && extra.ext) ? extra.ext : ""; + + let requestString = ts.toString(10) + "\n" + + nonce + "\n" + + usedMethod + "\n" + + uri.path + "\n" + + host + "\n" + + port + "\n" + + ext + "\n"; + + let hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, + Utils.makeHMACKey(key)); + let mac = Utils.digestBytes(requestString, hasher); + + function getHeader() { + return Utils.getHTTPMACSHA1Header(this.identifier, this.ts, this.nonce, + this.mac, this.ext); + } + + return { + identifier: identifier, + key: key, + method: usedMethod, + hostname: host, + port: port, + mac: mac, + nonce: nonce, + ts: ts, + ext: ext, + getHeader: getHeader + }; + }, + + /** + * Obtain the HTTP MAC Authorization header value from fields. + * + * @param identifier + * (string) MAC key identifier. + * @param ts + * (number) Integer seconds since Unix epoch. + * @param nonce + * (string) Nonce value. + * @param mac + * (string) Computed HMAC digest (raw bytes). + * @param ext + * (optional) (string) Extra string content. + * @returns + * (string) Value to put in Authorization header. + */ + getHTTPMACSHA1Header: function getHTTPMACSHA1Header(identifier, ts, nonce, + mac, ext) { + let header ='MAC id="' + identifier + '", ' + + 'ts="' + ts + '", ' + + 'nonce="' + nonce + '", ' + + 'mac="' + btoa(mac) + '"'; + + if (!ext) { + return header; + } + + return header += ', ext="' + ext +'"'; + }, + makeURI: function Weave_makeURI(URIString) { if (!URIString) return null; diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js index 7aaa2322b327..cee2ec034ed3 100644 --- a/services/sync/tests/unit/head_helpers.js +++ b/services/sync/tests/unit/head_helpers.js @@ -5,7 +5,8 @@ Cu.import("resource://services-sync/async.js"); Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-sync/engines.js"); -var btoa; +let btoa; +let atob; let provider = { getFile: function(prop, persistent) { @@ -39,6 +40,7 @@ function waitForZeroTimer(callback) { } btoa = Cu.import("resource://services-sync/log4moz.js").btoa; +atob = Cu.import("resource://services-sync/log4moz.js").atob; function getTestLogger(component) { return Log4Moz.repository.getLogger("Testing"); } diff --git a/services/sync/tests/unit/test_utils_httpmac.js b/services/sync/tests/unit/test_utils_httpmac.js new file mode 100644 index 000000000000..813a1a4c2df7 --- /dev/null +++ b/services/sync/tests/unit/test_utils_httpmac.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://services-sync/util.js"); + +function run_test() { + initTestLogging(); + + run_next_test(); +} + +add_test(function test_sha1() { + _("Ensure HTTP MAC SHA1 generation works as expected."); + + let id = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7"; + let key = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz"; + let ts = 1329181221; + let method = "GET"; + let nonce = "wGX71"; + let uri = Utils.makeURI("http://10.250.2.176/alias/"); + + let result = Utils.computeHTTPMACSHA1(id, key, method, uri, {ts: ts, + nonce: nonce}); + + do_check_eq(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck="); + + do_check_eq(result.getHeader(), + 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' + + 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="'); + + let ext = "EXTRA DATA; foo,bar=1"; + + let result = Utils.computeHTTPMACSHA1(id, key, method, uri, {ts: ts, + nonce: nonce, + ext: ext}); + do_check_eq(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68="); + do_check_eq(result.getHeader(), + 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' + + 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' + + 'ext="EXTRA DATA; foo,bar=1"'); + + run_next_test(); +}); + +add_test(function test_nonce_length() { + _("Ensure custom nonce lengths are honoured."); + + function get_mac(length) { + let uri = Utils.makeURI("http://example.com/"); + return Utils.computeHTTPMACSHA1("foo", "bar", "GET", uri, { + nonce_bytes: length + }); + } + + let result = get_mac(12); + do_check_eq(12, atob(result.nonce).length); + + let result = get_mac(2); + do_check_eq(2, atob(result.nonce).length); + + let result = get_mac(0); + do_check_eq(8, atob(result.nonce).length); + + let result = get_mac(-1); + do_check_eq(8, atob(result.nonce).length); + + run_next_test(); +}); diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini index 39b155ae8c6b..4feb471177fc 100644 --- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -115,6 +115,7 @@ skip-if = os == "android" [test_utils_getErrorString.js] [test_utils_getIcon.js] [test_utils_hkdfExpand.js] +[test_utils_httpmac.js] [test_utils_json.js] [test_utils_lazyStrings.js] [test_utils_lock.js]