michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["CryptoUtils"]; michael@0: michael@0: Cu.import("resource://services-common/observers.js"); michael@0: Cu.import("resource://services-common/utils.js"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: this.CryptoUtils = { michael@0: xor: function xor(a, b) { michael@0: let bytes = []; michael@0: michael@0: if (a.length != b.length) { michael@0: throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length); michael@0: } michael@0: michael@0: for (let i = 0; i < a.length; i++) { michael@0: bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i); michael@0: } michael@0: michael@0: return String.fromCharCode.apply(String, bytes); michael@0: }, michael@0: michael@0: /** michael@0: * Generate a string of random bytes. michael@0: */ michael@0: generateRandomBytes: function generateRandomBytes(length) { michael@0: let rng = Cc["@mozilla.org/security/random-generator;1"] michael@0: .createInstance(Ci.nsIRandomGenerator); michael@0: let bytes = rng.generateRandomBytes(length); michael@0: return CommonUtils.byteArrayToString(bytes); michael@0: }, michael@0: michael@0: /** michael@0: * UTF8-encode a message and hash it with the given hasher. Returns a michael@0: * string containing bytes. The hasher is reset if it's an HMAC hasher. michael@0: */ michael@0: digestUTF8: function digestUTF8(message, hasher) { michael@0: let data = this._utf8Converter.convertToByteArray(message, {}); michael@0: hasher.update(data, data.length); michael@0: let result = hasher.finish(false); michael@0: if (hasher instanceof Ci.nsICryptoHMAC) { michael@0: hasher.reset(); michael@0: } michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Treat the given message as a bytes string and hash it with the given michael@0: * hasher. Returns a string containing bytes. The hasher is reset if it's michael@0: * an HMAC hasher. michael@0: */ michael@0: digestBytes: function digestBytes(message, hasher) { michael@0: // No UTF-8 encoding for you, sunshine. michael@0: let bytes = [b.charCodeAt() for each (b in message)]; michael@0: hasher.update(bytes, bytes.length); michael@0: let result = hasher.finish(false); michael@0: if (hasher instanceof Ci.nsICryptoHMAC) { michael@0: hasher.reset(); michael@0: } michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Encode the message into UTF-8 and feed the resulting bytes into the michael@0: * given hasher. Does not return a hash. This can be called multiple times michael@0: * with a single hasher, but eventually you must extract the result michael@0: * yourself. michael@0: */ michael@0: updateUTF8: function(message, hasher) { michael@0: let bytes = this._utf8Converter.convertToByteArray(message, {}); michael@0: hasher.update(bytes, bytes.length); michael@0: }, michael@0: michael@0: /** michael@0: * UTF-8 encode a message and perform a SHA-1 over it. michael@0: * michael@0: * @param message michael@0: * (string) Buffer to perform operation on. Should be a JS string. michael@0: * It is possible to pass in a string representing an array michael@0: * of bytes. But, you probably don't want to UTF-8 encode michael@0: * such data and thus should not be using this function. michael@0: * michael@0: * @return string michael@0: * Raw bytes constituting SHA-1 hash. Value is a JS string. Each michael@0: * character is the byte value for that offset. Returned string michael@0: * always has .length == 20. michael@0: */ michael@0: UTF8AndSHA1: function UTF8AndSHA1(message) { michael@0: let hasher = Cc["@mozilla.org/security/hash;1"] michael@0: .createInstance(Ci.nsICryptoHash); michael@0: hasher.init(hasher.SHA1); michael@0: michael@0: return CryptoUtils.digestUTF8(message, hasher); michael@0: }, michael@0: michael@0: sha1: function sha1(message) { michael@0: return CommonUtils.bytesAsHex(CryptoUtils.UTF8AndSHA1(message)); michael@0: }, michael@0: michael@0: sha1Base32: function sha1Base32(message) { michael@0: return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message)); michael@0: }, michael@0: michael@0: /** michael@0: * Produce an HMAC key object from a key string. michael@0: */ michael@0: makeHMACKey: function makeHMACKey(str) { michael@0: return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str); michael@0: }, michael@0: michael@0: /** michael@0: * Produce an HMAC hasher and initialize it with the given HMAC key. michael@0: */ michael@0: makeHMACHasher: function makeHMACHasher(type, key) { michael@0: let hasher = Cc["@mozilla.org/security/hmac;1"] michael@0: .createInstance(Ci.nsICryptoHMAC); michael@0: hasher.init(type, key); michael@0: return hasher; michael@0: }, michael@0: michael@0: /** michael@0: * HMAC-based Key Derivation (RFC 5869). michael@0: */ michael@0: hkdf: function hkdf(ikm, xts, info, len) { michael@0: const BLOCKSIZE = 256 / 8; michael@0: if (typeof xts === undefined) michael@0: xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0, michael@0: 0, 0, 0, 0, 0, 0, 0, 0, michael@0: 0, 0, 0, 0, 0, 0, 0, 0, michael@0: 0, 0, 0, 0, 0, 0, 0, 0); michael@0: let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, michael@0: CryptoUtils.makeHMACKey(xts)); michael@0: let prk = CryptoUtils.digestBytes(ikm, h); michael@0: return CryptoUtils.hkdfExpand(prk, info, len); michael@0: }, michael@0: michael@0: /** michael@0: * HMAC-based Key Derivation Step 2 according to RFC 5869. michael@0: */ michael@0: hkdfExpand: function hkdfExpand(prk, info, len) { michael@0: const BLOCKSIZE = 256 / 8; michael@0: let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, michael@0: CryptoUtils.makeHMACKey(prk)); michael@0: let T = ""; michael@0: let Tn = ""; michael@0: let iterations = Math.ceil(len/BLOCKSIZE); michael@0: for (let i = 0; i < iterations; i++) { michael@0: Tn = CryptoUtils.digestBytes(Tn + info + String.fromCharCode(i + 1), h); michael@0: T += Tn; michael@0: } michael@0: return T.slice(0, len); michael@0: }, michael@0: michael@0: /** michael@0: * PBKDF2 implementation in Javascript. michael@0: * michael@0: * The arguments to this function correspond to items in michael@0: * PKCS #5, v2.0 pp. 9-10 michael@0: * michael@0: * P: the passphrase, an octet string: e.g., "secret phrase" michael@0: * S: the salt, an octet string: e.g., "DNXPzPpiwn" michael@0: * c: the number of iterations, a positive integer: e.g., 4096 michael@0: * dkLen: the length in octets of the destination michael@0: * key, a positive integer: e.g., 16 michael@0: * hmacAlg: The algorithm to use for hmac michael@0: * hmacLen: The hmac length michael@0: * michael@0: * The default value of 20 for hmacLen is appropriate for SHA1. For SHA256, michael@0: * hmacLen should be 32. michael@0: * michael@0: * The output is an octet string of length dkLen, which you michael@0: * can encode as you wish. michael@0: */ michael@0: pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen, michael@0: hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) { michael@0: michael@0: // We don't have a default in the algo itself, as NSS does. michael@0: // Use the constant. michael@0: if (!dkLen) { michael@0: dkLen = SYNC_KEY_DECODED_LENGTH; michael@0: } michael@0: michael@0: function F(S, c, i, h) { michael@0: michael@0: function XOR(a, b, isA) { michael@0: if (a.length != b.length) { michael@0: return false; michael@0: } michael@0: michael@0: let val = []; michael@0: for (let i = 0; i < a.length; i++) { michael@0: if (isA) { michael@0: val[i] = a[i] ^ b[i]; michael@0: } else { michael@0: val[i] = a.charCodeAt(i) ^ b.charCodeAt(i); michael@0: } michael@0: } michael@0: michael@0: return val; michael@0: } michael@0: michael@0: let ret; michael@0: let U = []; michael@0: michael@0: /* Encode i into 4 octets: _INT */ michael@0: let I = []; michael@0: I[0] = String.fromCharCode((i >> 24) & 0xff); michael@0: I[1] = String.fromCharCode((i >> 16) & 0xff); michael@0: I[2] = String.fromCharCode((i >> 8) & 0xff); michael@0: I[3] = String.fromCharCode(i & 0xff); michael@0: michael@0: U[0] = CryptoUtils.digestBytes(S + I.join(''), h); michael@0: for (let j = 1; j < c; j++) { michael@0: U[j] = CryptoUtils.digestBytes(U[j - 1], h); michael@0: } michael@0: michael@0: ret = U[0]; michael@0: for (let j = 1; j < c; j++) { michael@0: ret = CommonUtils.byteArrayToString(XOR(ret, U[j])); michael@0: } michael@0: michael@0: return ret; michael@0: } michael@0: michael@0: let l = Math.ceil(dkLen / hmacLen); michael@0: let r = dkLen - ((l - 1) * hmacLen); michael@0: michael@0: // Reuse the key and the hasher. Remaking them 4096 times is 'spensive. michael@0: let h = CryptoUtils.makeHMACHasher(hmacAlg, michael@0: CryptoUtils.makeHMACKey(P)); michael@0: michael@0: let T = []; michael@0: for (let i = 0; i < l;) { michael@0: T[i] = F(S, c, ++i, h); michael@0: } michael@0: michael@0: let ret = ""; michael@0: for (let i = 0; i < l-1;) { michael@0: ret += T[i++]; michael@0: } michael@0: ret += T[l - 1].substr(0, r); michael@0: michael@0: return ret; michael@0: }, michael@0: michael@0: deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase, michael@0: salt, michael@0: keyLength, michael@0: forceJS) { michael@0: if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) { michael@0: return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength); michael@0: } michael@0: else { michael@0: // Fall back to JS implementation. michael@0: // 4096 is hardcoded in WeaveCrypto, so do so here. michael@0: return CryptoUtils.pbkdf2Generate(passphrase, atob(salt), 4096, michael@0: keyLength); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Compute the HTTP MAC SHA-1 for an HTTP request. michael@0: * michael@0: * @param identifier michael@0: * (string) MAC Key Identifier. michael@0: * @param key michael@0: * (string) MAC Key. michael@0: * @param method michael@0: * (string) HTTP request method. michael@0: * @param URI michael@0: * (nsIURI) HTTP request URI. michael@0: * @param extra michael@0: * (object) Optional extra parameters. Valid keys are: michael@0: * nonce_bytes - How many bytes the nonce should be. This defaults michael@0: * to 8. Note that this many bytes are Base64 encoded, so the michael@0: * string length of the nonce will be longer than this value. michael@0: * ts - Timestamp to use. Should only be defined for testing. michael@0: * nonce - String nonce. Should only be defined for testing as this michael@0: * function will generate a cryptographically secure random one michael@0: * if not defined. michael@0: * ext - Extra string to be included in MAC. Per the HTTP MAC spec, michael@0: * the format is undefined and thus application specific. michael@0: * @returns michael@0: * (object) Contains results of operation and input arguments (for michael@0: * symmetry). The object has the following keys: michael@0: * michael@0: * identifier - (string) MAC Key Identifier (from arguments). michael@0: * key - (string) MAC Key (from arguments). michael@0: * method - (string) HTTP request method (from arguments). michael@0: * hostname - (string) HTTP hostname used (derived from arguments). michael@0: * port - (string) HTTP port number used (derived from arguments). michael@0: * mac - (string) Raw HMAC digest bytes. michael@0: * getHeader - (function) Call to obtain the string Authorization michael@0: * header value for this invocation. michael@0: * nonce - (string) Nonce value used. michael@0: * ts - (number) Integer seconds since Unix epoch that was used. michael@0: */ michael@0: computeHTTPMACSHA1: function computeHTTPMACSHA1(identifier, key, method, michael@0: uri, extra) { michael@0: let ts = (extra && extra.ts) ? extra.ts : Math.floor(Date.now() / 1000); michael@0: let nonce_bytes = (extra && extra.nonce_bytes > 0) ? extra.nonce_bytes : 8; michael@0: michael@0: // We are allowed to use more than the Base64 alphabet if we want. michael@0: let nonce = (extra && extra.nonce) michael@0: ? extra.nonce michael@0: : btoa(CryptoUtils.generateRandomBytes(nonce_bytes)); michael@0: michael@0: let host = uri.asciiHost; michael@0: let port; michael@0: let usedMethod = method.toUpperCase(); michael@0: michael@0: if (uri.port != -1) { michael@0: port = uri.port; michael@0: } else if (uri.scheme == "http") { michael@0: port = "80"; michael@0: } else if (uri.scheme == "https") { michael@0: port = "443"; michael@0: } else { michael@0: throw new Error("Unsupported URI scheme: " + uri.scheme); michael@0: } michael@0: michael@0: let ext = (extra && extra.ext) ? extra.ext : ""; michael@0: michael@0: let requestString = ts.toString(10) + "\n" + michael@0: nonce + "\n" + michael@0: usedMethod + "\n" + michael@0: uri.path + "\n" + michael@0: host + "\n" + michael@0: port + "\n" + michael@0: ext + "\n"; michael@0: michael@0: let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1, michael@0: CryptoUtils.makeHMACKey(key)); michael@0: let mac = CryptoUtils.digestBytes(requestString, hasher); michael@0: michael@0: function getHeader() { michael@0: return CryptoUtils.getHTTPMACSHA1Header(this.identifier, this.ts, michael@0: this.nonce, this.mac, this.ext); michael@0: } michael@0: michael@0: return { michael@0: identifier: identifier, michael@0: key: key, michael@0: method: usedMethod, michael@0: hostname: host, michael@0: port: port, michael@0: mac: mac, michael@0: nonce: nonce, michael@0: ts: ts, michael@0: ext: ext, michael@0: getHeader: getHeader michael@0: }; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Obtain the HTTP MAC Authorization header value from fields. michael@0: * michael@0: * @param identifier michael@0: * (string) MAC key identifier. michael@0: * @param ts michael@0: * (number) Integer seconds since Unix epoch. michael@0: * @param nonce michael@0: * (string) Nonce value. michael@0: * @param mac michael@0: * (string) Computed HMAC digest (raw bytes). michael@0: * @param ext michael@0: * (optional) (string) Extra string content. michael@0: * @returns michael@0: * (string) Value to put in Authorization header. michael@0: */ michael@0: getHTTPMACSHA1Header: function getHTTPMACSHA1Header(identifier, ts, nonce, michael@0: mac, ext) { michael@0: let header ='MAC id="' + identifier + '", ' + michael@0: 'ts="' + ts + '", ' + michael@0: 'nonce="' + nonce + '", ' + michael@0: 'mac="' + btoa(mac) + '"'; michael@0: michael@0: if (!ext) { michael@0: return header; michael@0: } michael@0: michael@0: return header += ', ext="' + ext +'"'; michael@0: }, michael@0: michael@0: /** michael@0: * Given an HTTP header value, strip out any attributes. michael@0: */ michael@0: michael@0: stripHeaderAttributes: function(value) { michael@0: let value = value || ""; michael@0: let i = value.indexOf(";"); michael@0: return value.substring(0, (i >= 0) ? i : undefined).trim().toLowerCase(); michael@0: }, michael@0: michael@0: /** michael@0: * Compute the HAWK client values (mostly the header) for an HTTP request. michael@0: * michael@0: * @param URI michael@0: * (nsIURI) HTTP request URI. michael@0: * @param method michael@0: * (string) HTTP request method. michael@0: * @param options michael@0: * (object) extra parameters (all but "credentials" are optional): michael@0: * credentials - (object, mandatory) HAWK credentials object. michael@0: * All three keys are required: michael@0: * id - (string) key identifier michael@0: * key - (string) raw key bytes michael@0: * algorithm - (string) which hash to use: "sha1" or "sha256" michael@0: * ext - (string) application-specific data, included in MAC michael@0: * localtimeOffsetMsec - (number) local clock offset (vs server) michael@0: * payload - (string) payload to include in hash, containing the michael@0: * HTTP request body. If not provided, the HAWK hash michael@0: * will not cover the request body, and the server michael@0: * should not check it either. This will be UTF-8 michael@0: * encoded into bytes before hashing. This function michael@0: * cannot handle arbitrary binary data, sorry (the michael@0: * UTF-8 encoding process will corrupt any codepoints michael@0: * between U+0080 and U+00FF). Callers must be careful michael@0: * to use an HTTP client function which encodes the michael@0: * payload exactly the same way, otherwise the hash michael@0: * will not match. michael@0: * contentType - (string) payload Content-Type. This is included michael@0: * (without any attributes like "charset=") in the michael@0: * HAWK hash. It does *not* affect interpretation michael@0: * of the "payload" property. michael@0: * hash - (base64 string) pre-calculated payload hash. If michael@0: * provided, "payload" is ignored. michael@0: * ts - (number) pre-calculated timestamp, secs since epoch michael@0: * now - (number) current time, ms-since-epoch, for tests michael@0: * nonce - (string) pre-calculated nonce. Should only be defined michael@0: * for testing as this function will generate a michael@0: * cryptographically secure random one if not defined. michael@0: * @returns michael@0: * (object) Contains results of operation. The object has the michael@0: * following keys: michael@0: * field - (string) HAWK header, to use in Authorization: header michael@0: * artifacts - (object) other generated values: michael@0: * ts - (number) timestamp, in seconds since epoch michael@0: * nonce - (string) michael@0: * method - (string) michael@0: * resource - (string) path plus querystring michael@0: * host - (string) michael@0: * port - (number) michael@0: * hash - (string) payload hash (base64) michael@0: * ext - (string) app-specific data michael@0: * MAC - (string) request MAC (base64) michael@0: */ michael@0: computeHAWK: function(uri, method, options) { michael@0: let credentials = options.credentials; michael@0: let ts = options.ts || Math.floor(((options.now || Date.now()) + michael@0: (options.localtimeOffsetMsec || 0)) michael@0: / 1000); michael@0: michael@0: let hash_algo, hmac_algo; michael@0: if (credentials.algorithm == "sha1") { michael@0: hash_algo = Ci.nsICryptoHash.SHA1; michael@0: hmac_algo = Ci.nsICryptoHMAC.SHA1; michael@0: } else if (credentials.algorithm == "sha256") { michael@0: hash_algo = Ci.nsICryptoHash.SHA256; michael@0: hmac_algo = Ci.nsICryptoHMAC.SHA256; michael@0: } else { michael@0: throw new Error("Unsupported algorithm: " + credentials.algorithm); michael@0: } michael@0: michael@0: let port; michael@0: if (uri.port != -1) { michael@0: port = uri.port; michael@0: } else if (uri.scheme == "http") { michael@0: port = 80; michael@0: } else if (uri.scheme == "https") { michael@0: port = 443; michael@0: } else { michael@0: throw new Error("Unsupported URI scheme: " + uri.scheme); michael@0: } michael@0: michael@0: let artifacts = { michael@0: ts: ts, michael@0: nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)), michael@0: method: method.toUpperCase(), michael@0: resource: uri.path, // This includes both path and search/queryarg. michael@0: host: uri.asciiHost.toLowerCase(), // This includes punycoding. michael@0: port: port.toString(10), michael@0: hash: options.hash, michael@0: ext: options.ext, michael@0: }; michael@0: michael@0: let contentType = CryptoUtils.stripHeaderAttributes(options.contentType); michael@0: michael@0: if (!artifacts.hash && options.hasOwnProperty("payload") michael@0: && options.payload) { michael@0: let hasher = Cc["@mozilla.org/security/hash;1"] michael@0: .createInstance(Ci.nsICryptoHash); michael@0: hasher.init(hash_algo); michael@0: CryptoUtils.updateUTF8("hawk.1.payload\n", hasher); michael@0: CryptoUtils.updateUTF8(contentType+"\n", hasher); michael@0: CryptoUtils.updateUTF8(options.payload, hasher); michael@0: CryptoUtils.updateUTF8("\n", hasher); michael@0: let hash = hasher.finish(false); michael@0: // HAWK specifies this .hash to use +/ (not _-) and include the michael@0: // trailing "==" padding. michael@0: let hash_b64 = btoa(hash); michael@0: artifacts.hash = hash_b64; michael@0: } michael@0: michael@0: let requestString = ("hawk.1.header" + "\n" + michael@0: artifacts.ts.toString(10) + "\n" + michael@0: artifacts.nonce + "\n" + michael@0: artifacts.method + "\n" + michael@0: artifacts.resource + "\n" + michael@0: artifacts.host + "\n" + michael@0: artifacts.port + "\n" + michael@0: (artifacts.hash || "") + "\n"); michael@0: if (artifacts.ext) { michael@0: requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n"); michael@0: } michael@0: requestString += "\n"; michael@0: michael@0: let hasher = CryptoUtils.makeHMACHasher(hmac_algo, michael@0: CryptoUtils.makeHMACKey(credentials.key)); michael@0: artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher)); michael@0: // The output MAC uses "+" and "/", and padded== . michael@0: michael@0: function escape(attribute) { michael@0: // This is used for "x=y" attributes inside HTTP headers. michael@0: return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"'); michael@0: } michael@0: let header = ('Hawk id="' + credentials.id + '", ' + michael@0: 'ts="' + artifacts.ts + '", ' + michael@0: 'nonce="' + artifacts.nonce + '", ' + michael@0: (artifacts.hash ? ('hash="' + artifacts.hash + '", ') : "") + michael@0: (artifacts.ext ? ('ext="' + escape(artifacts.ext) + '", ') : "") + michael@0: 'mac="' + artifacts.mac + '"'); michael@0: return { michael@0: artifacts: artifacts, michael@0: field: header, michael@0: }; michael@0: }, michael@0: michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() { michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: michael@0: return converter; michael@0: }); michael@0: michael@0: let Svc = {}; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(Svc, michael@0: "KeyFactory", michael@0: "@mozilla.org/security/keyobjectfactory;1", michael@0: "nsIKeyObjectFactory"); michael@0: michael@0: Svc.__defineGetter__("Crypto", function() { michael@0: let ns = {}; michael@0: Cu.import("resource://services-crypto/WeaveCrypto.js", ns); michael@0: michael@0: let wc = new ns.WeaveCrypto(); michael@0: delete Svc.Crypto; michael@0: return Svc.Crypto = wc; michael@0: }); michael@0: michael@0: Observers.add("xpcom-shutdown", function unloadServices() { michael@0: Observers.remove("xpcom-shutdown", unloadServices); michael@0: michael@0: for (let k in Svc) { michael@0: delete Svc[k]; michael@0: } michael@0: });