summaryrefslogtreecommitdiffstats
path: root/services/crypto/modules
diff options
context:
space:
mode:
Diffstat (limited to 'services/crypto/modules')
-rw-r--r--services/crypto/modules/WeaveCrypto.sys.mjs232
-rw-r--r--services/crypto/modules/jwcrypto.sys.mjs214
-rw-r--r--services/crypto/modules/utils.sys.mjs539
3 files changed, 985 insertions, 0 deletions
diff --git a/services/crypto/modules/WeaveCrypto.sys.mjs b/services/crypto/modules/WeaveCrypto.sys.mjs
new file mode 100644
index 0000000000..63ee51a1a2
--- /dev/null
+++ b/services/crypto/modules/WeaveCrypto.sys.mjs
@@ -0,0 +1,232 @@
+/* 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/. */
+
+const CRYPT_ALGO = "AES-CBC";
+const CRYPT_ALGO_LENGTH = 256;
+const CRYPT_ALGO_USAGES = ["encrypt", "decrypt"];
+const AES_CBC_IV_SIZE = 16;
+const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 };
+const UTF_LABEL = "utf-8";
+
+export function WeaveCrypto() {
+ this.init();
+}
+
+WeaveCrypto.prototype = {
+ prefBranch: null,
+ debug: true, // services.sync.log.cryptoDebug
+
+ observer: {
+ _self: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ observe(subject, topic, data) {
+ let self = this._self;
+ self.log("Observed " + topic + " topic.");
+ if (topic == "nsPref:changed") {
+ self.debug = self.prefBranch.getBoolPref("cryptoDebug");
+ }
+ },
+ },
+
+ init() {
+ // Preferences. Add observer so we get notified of changes.
+ this.prefBranch = Services.prefs.getBranch("services.sync.log.");
+ this.prefBranch.addObserver("cryptoDebug", this.observer);
+ this.observer._self = this;
+ this.debug = this.prefBranch.getBoolPref("cryptoDebug", false);
+ ChromeUtils.defineLazyGetter(
+ this,
+ "encoder",
+ () => new TextEncoder(UTF_LABEL)
+ );
+ ChromeUtils.defineLazyGetter(
+ this,
+ "decoder",
+ () => new TextDecoder(UTF_LABEL, { fatal: true })
+ );
+ },
+
+ log(message) {
+ if (!this.debug) {
+ return;
+ }
+ dump("WeaveCrypto: " + message + "\n");
+ Services.console.logStringMessage("WeaveCrypto: " + message);
+ },
+
+ // /!\ Only use this for tests! /!\
+ _getCrypto() {
+ return crypto;
+ },
+
+ async encrypt(clearTextUCS2, symmetricKey, iv) {
+ this.log("encrypt() called");
+ let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
+ let encrypted = await this._commonCrypt(
+ clearTextBuffer,
+ symmetricKey,
+ iv,
+ OPERATIONS.ENCRYPT
+ );
+ return this.encodeBase64(encrypted);
+ },
+
+ async decrypt(cipherText, symmetricKey, iv) {
+ this.log("decrypt() called");
+ if (cipherText.length) {
+ cipherText = atob(cipherText);
+ }
+ let cipherTextBuffer = this.byteCompressInts(cipherText);
+ let decrypted = await this._commonCrypt(
+ cipherTextBuffer,
+ symmetricKey,
+ iv,
+ OPERATIONS.DECRYPT
+ );
+ return this.decoder.decode(decrypted);
+ },
+
+ /**
+ * _commonCrypt
+ *
+ * @args
+ * data: data to encrypt/decrypt (ArrayBuffer)
+ * symKeyStr: symmetric key (Base64 String)
+ * ivStr: initialization vector (Base64 String)
+ * operation: operation to apply (either OPERATIONS.ENCRYPT or OPERATIONS.DECRYPT)
+ * @returns
+ * the encrypted/decrypted data (ArrayBuffer)
+ */
+ async _commonCrypt(data, symKeyStr, ivStr, operation) {
+ this.log("_commonCrypt() called");
+ ivStr = atob(ivStr);
+
+ if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) {
+ throw new Error("Unsupported operation in _commonCrypt.");
+ }
+ // We never want an IV longer than the block size, which is 16 bytes
+ // for AES, neither do we want one smaller; throw in both cases.
+ if (ivStr.length !== AES_CBC_IV_SIZE) {
+ throw new Error(`Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`);
+ }
+
+ let iv = this.byteCompressInts(ivStr);
+ let symKey = await this.importSymKey(symKeyStr, operation);
+ let cryptMethod = (
+ operation === OPERATIONS.ENCRYPT
+ ? crypto.subtle.encrypt
+ : crypto.subtle.decrypt
+ ).bind(crypto.subtle);
+ let algo = { name: CRYPT_ALGO, iv };
+
+ let keyBytes = await cryptMethod.call(crypto.subtle, algo, symKey, data);
+ return new Uint8Array(keyBytes);
+ },
+
+ async generateRandomKey() {
+ this.log("generateRandomKey() called");
+ let algo = {
+ name: CRYPT_ALGO,
+ length: CRYPT_ALGO_LENGTH,
+ };
+ let key = await crypto.subtle.generateKey(algo, true, CRYPT_ALGO_USAGES);
+ let keyBytes = await crypto.subtle.exportKey("raw", key);
+ return this.encodeBase64(new Uint8Array(keyBytes));
+ },
+
+ generateRandomIV() {
+ return this.generateRandomBytes(AES_CBC_IV_SIZE);
+ },
+
+ generateRandomBytes(byteCount) {
+ this.log("generateRandomBytes() called");
+
+ let randBytes = new Uint8Array(byteCount);
+ crypto.getRandomValues(randBytes);
+
+ return this.encodeBase64(randBytes);
+ },
+
+ //
+ // SymKey CryptoKey memoization.
+ //
+
+ // Memoize the import of symmetric keys. We do this by using the base64
+ // string itself as a key.
+ _encryptionSymKeyMemo: {},
+ _decryptionSymKeyMemo: {},
+ async importSymKey(encodedKeyString, operation) {
+ let memo;
+
+ // We use two separate memos for thoroughness: operation is an input to
+ // key import.
+ switch (operation) {
+ case OPERATIONS.ENCRYPT:
+ memo = this._encryptionSymKeyMemo;
+ break;
+ case OPERATIONS.DECRYPT:
+ memo = this._decryptionSymKeyMemo;
+ break;
+ default:
+ throw new Error("Unsupported operation in importSymKey.");
+ }
+
+ if (encodedKeyString in memo) {
+ return memo[encodedKeyString];
+ }
+
+ let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true);
+ let algo = { name: CRYPT_ALGO };
+ let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"];
+ let symKey = await crypto.subtle.importKey(
+ "raw",
+ symmetricKeyBuffer,
+ algo,
+ false,
+ usages
+ );
+ memo[encodedKeyString] = symKey;
+ return symKey;
+ },
+
+ //
+ // Utility functions
+ //
+
+ /**
+ * Returns an Uint8Array filled with a JS string,
+ * which means we only keep utf-16 characters from 0x00 to 0xFF.
+ */
+ byteCompressInts(str) {
+ let arrayBuffer = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ arrayBuffer[i] = str.charCodeAt(i) & 0xff;
+ }
+ return arrayBuffer;
+ },
+
+ expandData(data) {
+ let expanded = "";
+ for (let i = 0; i < data.length; i++) {
+ expanded += String.fromCharCode(data[i]);
+ }
+ return expanded;
+ },
+
+ encodeBase64(data) {
+ return btoa(this.expandData(data));
+ },
+
+ makeUint8Array(input, isEncoded) {
+ if (isEncoded) {
+ input = atob(input);
+ }
+ return this.byteCompressInts(input);
+ },
+};
diff --git a/services/crypto/modules/jwcrypto.sys.mjs b/services/crypto/modules/jwcrypto.sys.mjs
new file mode 100644
index 0000000000..d1f37eaa58
--- /dev/null
+++ b/services/crypto/modules/jwcrypto.sys.mjs
@@ -0,0 +1,214 @@
+/* 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/. */
+
+const ECDH_PARAMS = {
+ name: "ECDH",
+ namedCurve: "P-256",
+};
+const AES_PARAMS = {
+ name: "AES-GCM",
+ length: 256,
+};
+const AES_TAG_LEN = 128;
+const AES_GCM_IV_SIZE = 12;
+const UTF8_ENCODER = new TextEncoder();
+const UTF8_DECODER = new TextDecoder();
+
+class JWCrypto {
+ /**
+ * Encrypts the given data into a JWE using AES-256-GCM content encryption.
+ *
+ * This function implements a very small subset of the JWE encryption standard
+ * from https://tools.ietf.org/html/rfc7516. The only supported content encryption
+ * algorithm is enc="A256GCM" [1] and the only supported key encryption algorithm
+ * is alg="ECDH-ES" [2].
+ *
+ * @param {Object} key Peer Public JWK.
+ * @param {ArrayBuffer} data
+ *
+ * [1] https://tools.ietf.org/html/rfc7518#section-5.3
+ * [2] https://tools.ietf.org/html/rfc7518#section-4.6
+ *
+ * @returns {Promise<String>}
+ */
+ async generateJWE(key, data) {
+ // Generate an ephemeral key to use just for this encryption.
+ // The public component gets embedded in the JWE header.
+ const epk = await crypto.subtle.generateKey(ECDH_PARAMS, true, [
+ "deriveKey",
+ ]);
+ const ownPublicJWK = await crypto.subtle.exportKey("jwk", epk.publicKey);
+ // Remove properties added by our WebCrypto implementation but that aren't typically
+ // used with JWE in the wild. This saves space in the resulting JWE, and makes it easier
+ // to re-import the resulting JWK.
+ delete ownPublicJWK.key_ops;
+ delete ownPublicJWK.ext;
+ let header = { alg: "ECDH-ES", enc: "A256GCM", epk: ownPublicJWK };
+ // Import the peer's public key.
+ const peerPublicKey = await crypto.subtle.importKey(
+ "jwk",
+ key,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+ if (key.hasOwnProperty("kid")) {
+ header.kid = key.kid;
+ }
+ // Do ECDH agreement to get the content encryption key.
+ const contentKey = await deriveECDHSharedAESKey(
+ epk.privateKey,
+ peerPublicKey,
+ ["encrypt"]
+ );
+ // Encrypt with AES-GCM using the generated key.
+ // Note that the IV is generated randomly, which *in general* is not safe to do with AES-GCM because
+ // it's too short to guarantee uniqueness. But we know that the AES-GCM key itself is unique and will
+ // only be used for this single encryption, making a random IV safe to use for this particular use-case.
+ let iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_SIZE));
+ // Yes, additionalData is the byte representation of the base64 representation of the stringified header.
+ const additionalData = UTF8_ENCODER.encode(
+ ChromeUtils.base64URLEncode(UTF8_ENCODER.encode(JSON.stringify(header)), {
+ pad: false,
+ })
+ );
+ const encrypted = await crypto.subtle.encrypt(
+ {
+ name: "AES-GCM",
+ iv,
+ additionalData,
+ tagLength: AES_TAG_LEN,
+ },
+ contentKey,
+ data
+ );
+ // JWE needs the authentication tag as a separate string.
+ const tagIdx = encrypted.byteLength - ((AES_TAG_LEN + 7) >> 3);
+ let ciphertext = encrypted.slice(0, tagIdx);
+ let tag = encrypted.slice(tagIdx);
+ // JWE serialization in compact format.
+ header = UTF8_ENCODER.encode(JSON.stringify(header));
+ header = ChromeUtils.base64URLEncode(header, { pad: false });
+ tag = ChromeUtils.base64URLEncode(tag, { pad: false });
+ ciphertext = ChromeUtils.base64URLEncode(ciphertext, { pad: false });
+ iv = ChromeUtils.base64URLEncode(iv, { pad: false });
+ return `${header}..${iv}.${ciphertext}.${tag}`; // No CEK
+ }
+
+ /**
+ * Decrypts the given JWE using AES-256-GCM content encryption into a byte array.
+ * This function does the opposite of `JWCrypto.generateJWE`.
+ * The only supported content encryption algorithm is enc="A256GCM" [1]
+ * and the only supported key encryption algorithm is alg="ECDH-ES" [2].
+ *
+ * @param {"ECDH-ES"} algorithm
+ * @param {CryptoKey} key Local private key
+ *
+ * [1] https://tools.ietf.org/html/rfc7518#section-5.3
+ * [2] https://tools.ietf.org/html/rfc7518#section-4.6
+ *
+ * @returns {Promise<Uint8Array>}
+ */
+ async decryptJWE(jwe, key) {
+ let [header, cek, iv, ciphertext, authTag] = jwe.split(".");
+ const additionalData = UTF8_ENCODER.encode(header);
+ header = JSON.parse(
+ UTF8_DECODER.decode(
+ ChromeUtils.base64URLDecode(header, { padding: "reject" })
+ )
+ );
+ if (!!cek.length || header.enc !== "A256GCM" || header.alg !== "ECDH-ES") {
+ throw new Error("Unknown algorithm.");
+ }
+ if ("apu" in header || "apv" in header) {
+ throw new Error("apu and apv header values are not supported.");
+ }
+ const peerPublicKey = await crypto.subtle.importKey(
+ "jwk",
+ header.epk,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+ // Do ECDH agreement to get the content encryption key.
+ const contentKey = await deriveECDHSharedAESKey(key, peerPublicKey, [
+ "decrypt",
+ ]);
+ iv = ChromeUtils.base64URLDecode(iv, { padding: "reject" });
+ ciphertext = new Uint8Array(
+ ChromeUtils.base64URLDecode(ciphertext, { padding: "reject" })
+ );
+ authTag = new Uint8Array(
+ ChromeUtils.base64URLDecode(authTag, { padding: "reject" })
+ );
+ const bundle = new Uint8Array([...ciphertext, ...authTag]);
+
+ const decrypted = await crypto.subtle.decrypt(
+ {
+ name: "AES-GCM",
+ iv,
+ tagLength: AES_TAG_LEN,
+ additionalData,
+ },
+ contentKey,
+ bundle
+ );
+ return new Uint8Array(decrypted);
+ }
+}
+
+/**
+ * Do an ECDH agreement between a public and private key,
+ * returning the derived encryption key as specced by
+ * JWA RFC.
+ * The raw ECDH secret is derived into a key using
+ * Concat KDF, as defined in Section 5.8.1 of [NIST.800-56A].
+ * @param {CryptoKey} privateKey
+ * @param {CryptoKey} publicKey
+ * @param {String[]} keyUsages See `SubtleCrypto.deriveKey` 5th paramater documentation.
+ * @returns {Promise<CryptoKey>}
+ */
+async function deriveECDHSharedAESKey(privateKey, publicKey, keyUsages) {
+ const params = { ...ECDH_PARAMS, ...{ public: publicKey } };
+ const sharedKey = await crypto.subtle.deriveKey(
+ params,
+ privateKey,
+ AES_PARAMS,
+ true,
+ keyUsages
+ );
+ // This is the NIST Concat KDF specialized to a specific set of parameters,
+ // which basically turn it into a single application of SHA256.
+ // The details are from the JWA RFC.
+ let sharedKeyBytes = await crypto.subtle.exportKey("raw", sharedKey);
+ sharedKeyBytes = new Uint8Array(sharedKeyBytes);
+ const info = [
+ "\x00\x00\x00\x07A256GCM", // 7-byte algorithm identifier
+ "\x00\x00\x00\x00", // empty PartyUInfo
+ "\x00\x00\x00\x00", // empty PartyVInfo
+ "\x00\x00\x01\x00", // keylen == 256
+ ].join("");
+ const pkcs = `\x00\x00\x00\x01${String.fromCharCode.apply(
+ null,
+ sharedKeyBytes
+ )}${info}`;
+ const pkcsBuf = Uint8Array.from(
+ Array.prototype.map.call(pkcs, c => c.charCodeAt(0))
+ );
+ const derivedKeyBytes = await crypto.subtle.digest(
+ {
+ name: "SHA-256",
+ },
+ pkcsBuf
+ );
+ return crypto.subtle.importKey(
+ "raw",
+ derivedKeyBytes,
+ AES_PARAMS,
+ false,
+ keyUsages
+ );
+}
+
+export const jwcrypto = new JWCrypto();
diff --git a/services/crypto/modules/utils.sys.mjs b/services/crypto/modules/utils.sys.mjs
new file mode 100644
index 0000000000..3aa3db32d1
--- /dev/null
+++ b/services/crypto/modules/utils.sys.mjs
@@ -0,0 +1,539 @@
+/* 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";
+
+const lazy = {};
+
+ChromeUtils.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 = lazy.textEncoder.encode(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 = lazy.textEncoder.encode(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 = lazy.textEncoder.encode(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<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 = 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,
+ };
+ },
+};
+
+var Svc = {};
+
+Observers.add("xpcom-shutdown", function unloadServices() {
+ Observers.remove("xpcom-shutdown", unloadServices);
+
+ for (let k in Svc) {
+ delete Svc[k];
+ }
+});