/* 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/. */ import { Observers } from "resource://services-common/observers.sys.mjs"; import { CommonUtils } from "resource://services-common/utils.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; XPCOMUtils.defineLazyGetter(lazy, "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. */ export 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. */ digestUTF8(message, hasher) { let data = this._utf8Converter.convertToByteArray(message, {}); hasher.update(data, data.length); let result = hasher.finish(false); return result; }, /** * Treat the given message as a bytes string (if necessary) and hash it with * the given hasher. Returns a string containing bytes. */ 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); 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); }, /** * @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 = lazy.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 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 = lazy.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 = {}; Observers.add("xpcom-shutdown", function unloadServices() { Observers.remove("xpcom-shutdown", unloadServices); for (let k in Svc) { delete Svc[k]; } });