summaryrefslogtreecommitdiffstats
path: root/services/crypto/modules/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/crypto/modules/utils.js')
-rw-r--r--services/crypto/modules/utils.js587
1 files changed, 587 insertions, 0 deletions
diff --git a/services/crypto/modules/utils.js b/services/crypto/modules/utils.js
new file mode 100644
index 0000000000..3f37260f13
--- /dev/null
+++ b/services/crypto/modules/utils.js
@@ -0,0 +1,587 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["CryptoUtils"];
+
+const { Observers } = ChromeUtils.import(
+ "resource://services-common/observers.js"
+);
+const { CommonUtils } = ChromeUtils.import(
+ "resource://services-common/utils.js"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyGlobalGetters(this, ["crypto"]);
+
+XPCOMUtils.defineLazyGetter(this, "textEncoder", function() {
+ return new TextEncoder();
+});
+
+/**
+ * A number of `Legacy` suffixed functions are exposed by CryptoUtils.
+ * They work with octet strings, which were used before Javascript
+ * got ArrayBuffer and friends.
+ */
+var CryptoUtils = {
+ xor(a, b) {
+ let bytes = [];
+
+ if (a.length != b.length) {
+ throw new Error(
+ "can't xor unequal length strings: " + a.length + " vs " + b.length
+ );
+ }
+
+ for (let i = 0; i < a.length; i++) {
+ bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
+ }
+
+ return String.fromCharCode.apply(String, bytes);
+ },
+
+ /**
+ * Generate a string of random bytes.
+ * @returns {String} Octet string
+ */
+ generateRandomBytesLegacy(length) {
+ let bytes = CryptoUtils.generateRandomBytes(length);
+ return CommonUtils.arrayBufferToByteString(bytes);
+ },
+
+ generateRandomBytes(length) {
+ return crypto.getRandomValues(new Uint8Array(length));
+ },
+
+ /**
+ * UTF8-encode a message and hash it with the given hasher. Returns a
+ * string containing bytes. The hasher is reset if it's an HMAC hasher.
+ */
+ digestUTF8(message, hasher) {
+ let data = this._utf8Converter.convertToByteArray(message, {});
+ hasher.update(data, data.length);
+ let result = hasher.finish(false);
+ if (hasher instanceof Ci.nsICryptoHMAC) {
+ hasher.reset();
+ }
+ return result;
+ },
+
+ /**
+ * Treat the given message as a bytes string (if necessary) and hash it with
+ * the given hasher. Returns a string containing bytes.
+ * The hasher is reset if it's an HMAC hasher.
+ */
+ digestBytes(bytes, hasher) {
+ if (typeof bytes == "string" || bytes instanceof String) {
+ bytes = CommonUtils.byteStringToArrayBuffer(bytes);
+ }
+ return CryptoUtils.digestBytesArray(bytes, hasher);
+ },
+
+ digestBytesArray(bytes, hasher) {
+ hasher.update(bytes, bytes.length);
+ let result = hasher.finish(false);
+ if (hasher instanceof Ci.nsICryptoHMAC) {
+ hasher.reset();
+ }
+ return result;
+ },
+
+ /**
+ * Encode the message into UTF-8 and feed the resulting bytes into the
+ * given hasher. Does not return a hash. This can be called multiple times
+ * with a single hasher, but eventually you must extract the result
+ * yourself.
+ */
+ updateUTF8(message, hasher) {
+ let bytes = this._utf8Converter.convertToByteArray(message, {});
+ hasher.update(bytes, bytes.length);
+ },
+
+ sha256(message) {
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA256);
+ return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
+ },
+
+ sha256Base64(message) {
+ let data = this._utf8Converter.convertToByteArray(message, {});
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+ return hasher.finish(true);
+ },
+
+ /**
+ * Produce an HMAC key object from a key string.
+ */
+ makeHMACKey: function makeHMACKey(str) {
+ return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str);
+ },
+
+ /**
+ * Produce an HMAC hasher and initialize it with the given HMAC key.
+ */
+ makeHMACHasher: function makeHMACHasher(type, key) {
+ let hasher = Cc["@mozilla.org/security/hmac;1"].createInstance(
+ Ci.nsICryptoHMAC
+ );
+ hasher.init(type, key);
+ return hasher;
+ },
+
+ /**
+ * @param {string} alg Hash algorithm (common values are SHA-1 or SHA-256)
+ * @param {string} key Key as an octet string.
+ * @param {string} data Data as an octet string.
+ */
+ async hmacLegacy(alg, key, data) {
+ if (!key || !key.length) {
+ key = "\0";
+ }
+ data = CommonUtils.byteStringToArrayBuffer(data);
+ key = CommonUtils.byteStringToArrayBuffer(key);
+ const result = await CryptoUtils.hmac(alg, key, data);
+ return CommonUtils.arrayBufferToByteString(result);
+ },
+
+ /**
+ * @param {string} ikm IKM as an octet string.
+ * @param {string} salt Salt as an Hex string.
+ * @param {string} info Info as a regular string.
+ * @param {Number} len Desired output length in bytes.
+ */
+ async hkdfLegacy(ikm, xts, info, len) {
+ ikm = CommonUtils.byteStringToArrayBuffer(ikm);
+ xts = CommonUtils.byteStringToArrayBuffer(xts);
+ info = textEncoder.encode(info);
+ const okm = await CryptoUtils.hkdf(ikm, xts, info, len);
+ return CommonUtils.arrayBufferToByteString(okm);
+ },
+
+ /**
+ * @param {String} alg Hash algorithm (common values are SHA-1 or SHA-256)
+ * @param {ArrayBuffer} key
+ * @param {ArrayBuffer} data
+ * @param {Number} len Desired output length in bytes.
+ * @returns {Uint8Array}
+ */
+ async hmac(alg, key, data) {
+ const hmacKey = await crypto.subtle.importKey(
+ "raw",
+ key,
+ { name: "HMAC", hash: alg },
+ false,
+ ["sign"]
+ );
+ const result = await crypto.subtle.sign("HMAC", hmacKey, data);
+ return new Uint8Array(result);
+ },
+
+ /**
+ * @param {ArrayBuffer} ikm
+ * @param {ArrayBuffer} salt
+ * @param {ArrayBuffer} info
+ * @param {Number} len Desired output length in bytes.
+ * @returns {Uint8Array}
+ */
+ async hkdf(ikm, salt, info, len) {
+ const key = await crypto.subtle.importKey(
+ "raw",
+ ikm,
+ { name: "HKDF" },
+ false,
+ ["deriveBits"]
+ );
+ const okm = await crypto.subtle.deriveBits(
+ {
+ name: "HKDF",
+ hash: "SHA-256",
+ salt,
+ info,
+ },
+ key,
+ len * 8
+ );
+ return new Uint8Array(okm);
+ },
+
+ /**
+ * PBKDF2 password stretching with SHA-256 hmac.
+ *
+ * @param {string} passphrase Passphrase as an octet string.
+ * @param {string} salt Salt as an octet string.
+ * @param {string} iterations Number of iterations, a positive integer.
+ * @param {string} len Desired output length in bytes.
+ */
+ async pbkdf2Generate(passphrase, salt, iterations, len) {
+ passphrase = CommonUtils.byteStringToArrayBuffer(passphrase);
+ salt = CommonUtils.byteStringToArrayBuffer(salt);
+ const key = await crypto.subtle.importKey(
+ "raw",
+ passphrase,
+ { name: "PBKDF2" },
+ false,
+ ["deriveBits"]
+ );
+ const output = await crypto.subtle.deriveBits(
+ {
+ name: "PBKDF2",
+ hash: "SHA-256",
+ salt,
+ iterations,
+ },
+ key,
+ len * 8
+ );
+ return CommonUtils.arrayBufferToByteString(new Uint8Array(output));
+ },
+
+ /**
+ * 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.
+ */
+ async 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(CryptoUtils.generateRandomBytesLegacy(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.pathQueryRef +
+ "\n" +
+ host +
+ "\n" +
+ port +
+ "\n" +
+ ext +
+ "\n";
+
+ const mac = await CryptoUtils.hmacLegacy("SHA-1", key, requestString);
+
+ function getHeader() {
+ return CryptoUtils.getHTTPMACSHA1Header(
+ this.identifier,
+ this.ts,
+ this.nonce,
+ this.mac,
+ this.ext
+ );
+ }
+
+ return {
+ identifier,
+ key,
+ method: usedMethod,
+ hostname: host,
+ port,
+ mac,
+ nonce,
+ ts,
+ ext,
+ 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 + '"');
+ },
+
+ /**
+ * Given an HTTP header value, strip out any attributes.
+ */
+
+ stripHeaderAttributes(value) {
+ value = value || "";
+ let i = value.indexOf(";");
+ return value
+ .substring(0, i >= 0 ? i : undefined)
+ .trim()
+ .toLowerCase();
+ },
+
+ /**
+ * Compute the HAWK client values (mostly the header) for an HTTP request.
+ *
+ * @param URI
+ * (nsIURI) HTTP request URI.
+ * @param method
+ * (string) HTTP request method.
+ * @param options
+ * (object) extra parameters (all but "credentials" are optional):
+ * credentials - (object, mandatory) HAWK credentials object.
+ * All three keys are required:
+ * id - (string) key identifier
+ * key - (string) raw key bytes
+ * ext - (string) application-specific data, included in MAC
+ * localtimeOffsetMsec - (number) local clock offset (vs server)
+ * payload - (string) payload to include in hash, containing the
+ * HTTP request body. If not provided, the HAWK hash
+ * will not cover the request body, and the server
+ * should not check it either. This will be UTF-8
+ * encoded into bytes before hashing. This function
+ * cannot handle arbitrary binary data, sorry (the
+ * UTF-8 encoding process will corrupt any codepoints
+ * between U+0080 and U+00FF). Callers must be careful
+ * to use an HTTP client function which encodes the
+ * payload exactly the same way, otherwise the hash
+ * will not match.
+ * contentType - (string) payload Content-Type. This is included
+ * (without any attributes like "charset=") in the
+ * HAWK hash. It does *not* affect interpretation
+ * of the "payload" property.
+ * hash - (base64 string) pre-calculated payload hash. If
+ * provided, "payload" is ignored.
+ * ts - (number) pre-calculated timestamp, secs since epoch
+ * now - (number) current time, ms-since-epoch, for tests
+ * nonce - (string) pre-calculated nonce. Should only be defined
+ * for testing as this function will generate a
+ * cryptographically secure random one if not defined.
+ * @returns
+ * Promise<Object> Contains results of operation. The object has the
+ * following keys:
+ * field - (string) HAWK header, to use in Authorization: header
+ * artifacts - (object) other generated values:
+ * ts - (number) timestamp, in seconds since epoch
+ * nonce - (string)
+ * method - (string)
+ * resource - (string) path plus querystring
+ * host - (string)
+ * port - (number)
+ * hash - (string) payload hash (base64)
+ * ext - (string) app-specific data
+ * MAC - (string) request MAC (base64)
+ */
+ async computeHAWK(uri, method, options) {
+ let credentials = options.credentials;
+ let ts =
+ options.ts ||
+ Math.floor(
+ ((options.now || Date.now()) + (options.localtimeOffsetMsec || 0)) /
+ 1000
+ );
+ let port;
+ 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 artifacts = {
+ ts,
+ nonce: options.nonce || btoa(CryptoUtils.generateRandomBytesLegacy(8)),
+ method: method.toUpperCase(),
+ resource: uri.pathQueryRef, // This includes both path and search/queryarg.
+ host: uri.asciiHost.toLowerCase(), // This includes punycoding.
+ port: port.toString(10),
+ hash: options.hash,
+ ext: options.ext,
+ };
+
+ let contentType = CryptoUtils.stripHeaderAttributes(options.contentType);
+
+ if (
+ !artifacts.hash &&
+ options.hasOwnProperty("payload") &&
+ options.payload
+ ) {
+ const buffer = textEncoder.encode(
+ `hawk.1.payload\n${contentType}\n${options.payload}\n`
+ );
+ const hash = await crypto.subtle.digest("SHA-256", buffer);
+ // HAWK specifies this .hash to use +/ (not _-) and include the
+ // trailing "==" padding.
+ artifacts.hash = ChromeUtils.base64URLEncode(hash, { pad: true })
+ .replace(/-/g, "+")
+ .replace(/_/g, "/");
+ }
+
+ let requestString =
+ "hawk.1.header\n" +
+ artifacts.ts.toString(10) +
+ "\n" +
+ artifacts.nonce +
+ "\n" +
+ artifacts.method +
+ "\n" +
+ artifacts.resource +
+ "\n" +
+ artifacts.host +
+ "\n" +
+ artifacts.port +
+ "\n" +
+ (artifacts.hash || "") +
+ "\n";
+ if (artifacts.ext) {
+ requestString += artifacts.ext.replace("\\", "\\\\").replace("\n", "\\n");
+ }
+ requestString += "\n";
+
+ const hash = await CryptoUtils.hmacLegacy(
+ "SHA-256",
+ credentials.key,
+ requestString
+ );
+ artifacts.mac = btoa(hash);
+ // The output MAC uses "+" and "/", and padded== .
+
+ function escape(attribute) {
+ // This is used for "x=y" attributes inside HTTP headers.
+ return attribute.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
+ }
+ let header =
+ 'Hawk id="' +
+ credentials.id +
+ '", ' +
+ 'ts="' +
+ artifacts.ts +
+ '", ' +
+ 'nonce="' +
+ artifacts.nonce +
+ '", ' +
+ (artifacts.hash ? 'hash="' + artifacts.hash + '", ' : "") +
+ (artifacts.ext ? 'ext="' + escape(artifacts.ext) + '", ' : "") +
+ 'mac="' +
+ artifacts.mac +
+ '"';
+ return {
+ artifacts,
+ field: header,
+ };
+ },
+};
+
+XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() {
+ let converter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ return converter;
+});
+
+var Svc = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ Svc,
+ "KeyFactory",
+ "@mozilla.org/security/keyobjectfactory;1",
+ "nsIKeyObjectFactory"
+);
+
+Observers.add("xpcom-shutdown", function unloadServices() {
+ Observers.remove("xpcom-shutdown", unloadServices);
+
+ for (let k in Svc) {
+ delete Svc[k];
+ }
+});