summaryrefslogtreecommitdiffstats
path: root/services/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'services/crypto')
-rw-r--r--services/crypto/cryptoComponents.manifest1
-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
-rw-r--r--services/crypto/moz.build20
-rw-r--r--services/crypto/tests/unit/head_helpers.js78
-rw-r--r--services/crypto/tests/unit/test_crypto_crypt.js226
-rw-r--r--services/crypto/tests/unit/test_crypto_random.js52
-rw-r--r--services/crypto/tests/unit/test_jwcrypto.js51
-rw-r--r--services/crypto/tests/unit/test_load_modules.js12
-rw-r--r--services/crypto/tests/unit/test_utils_hawk.js346
-rw-r--r--services/crypto/tests/unit/test_utils_httpmac.js73
-rw-r--r--services/crypto/tests/unit/xpcshell.toml18
13 files changed, 1862 insertions, 0 deletions
diff --git a/services/crypto/cryptoComponents.manifest b/services/crypto/cryptoComponents.manifest
new file mode 100644
index 0000000000..f9f47bb42a
--- /dev/null
+++ b/services/crypto/cryptoComponents.manifest
@@ -0,0 +1 @@
+resource services-crypto resource://gre/modules/services-crypto/
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];
+ }
+});
diff --git a/services/crypto/moz.build b/services/crypto/moz.build
new file mode 100644
index 0000000000..4a1650cb6d
--- /dev/null
+++ b/services/crypto/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Sync")
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.toml"]
+
+EXTRA_JS_MODULES["services-crypto"] += [
+ "modules/jwcrypto.sys.mjs",
+ "modules/utils.sys.mjs",
+ "modules/WeaveCrypto.sys.mjs",
+]
+
+EXTRA_COMPONENTS += [
+ "cryptoComponents.manifest",
+]
diff --git a/services/crypto/tests/unit/head_helpers.js b/services/crypto/tests/unit/head_helpers.js
new file mode 100644
index 0000000000..322f6761a9
--- /dev/null
+++ b/services/crypto/tests/unit/head_helpers.js
@@ -0,0 +1,78 @@
+/* import-globals-from ../../../common/tests/unit/head_helpers.js */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+try {
+ // In the context of xpcshell tests, there won't be a default AppInfo
+ // eslint-disable-next-line mozilla/use-services
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+} catch (ex) {
+ // Make sure to provide the right OS so crypto loads the right binaries
+ var OS = "XPCShell";
+ if (mozinfo.os == "win") {
+ OS = "WINNT";
+ } else if (mozinfo.os == "mac") {
+ OS = "Darwin";
+ } else {
+ OS = "Linux";
+ }
+
+ const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+ );
+ updateAppInfo({
+ name: "XPCShell",
+ ID: "{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}",
+ version: "1",
+ platformVersion: "",
+ OS,
+ });
+}
+
+function base64UrlDecode(s) {
+ s = s.replace(/-/g, "+");
+ s = s.replace(/_/g, "/");
+
+ // Replace padding if it was stripped by the sender.
+ // See http://tools.ietf.org/html/rfc4648#section-4
+ switch (s.length % 4) {
+ case 0:
+ break; // No pad chars in this case
+ case 2:
+ s += "==";
+ break; // Two pad chars
+ case 3:
+ s += "=";
+ break; // One pad char
+ default:
+ throw new Error("Illegal base64url string!");
+ }
+
+ // With correct padding restored, apply the standard base64 decoder
+ return atob(s);
+}
+
+// Register resource alias. Normally done in SyncComponents.manifest.
+function addResourceAlias() {
+ const resProt = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ let uri = Services.io.newURI("resource://gre/modules/services-crypto/");
+ resProt.setSubstitution("services-crypto", uri);
+}
+addResourceAlias();
+
+/**
+ * Print some debug message to the console. All arguments will be printed,
+ * separated by spaces.
+ *
+ * @param [arg0, arg1, arg2, ...]
+ * Any number of arguments to print out
+ * @usage _("Hello World") -> prints "Hello World"
+ * @usage _(1, 2, 3) -> prints "1 2 3"
+ */
+var _ = function (some, debug, text, to) {
+ print(Array.from(arguments).join(" "));
+};
diff --git a/services/crypto/tests/unit/test_crypto_crypt.js b/services/crypto/tests/unit/test_crypto_crypt.js
new file mode 100644
index 0000000000..8fadd307c7
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_crypt.js
@@ -0,0 +1,226 @@
+const { WeaveCrypto } = ChromeUtils.importESModule(
+ "resource://services-crypto/WeaveCrypto.sys.mjs"
+);
+
+var cryptoSvc = new WeaveCrypto();
+
+add_task(async function test_key_memoization() {
+ let cryptoGlobal = cryptoSvc._getCrypto();
+ let oldImport = cryptoGlobal.subtle.importKey;
+ if (!oldImport) {
+ _("Couldn't swizzle crypto.subtle.importKey; returning.");
+ return;
+ }
+
+ let iv = cryptoSvc.generateRandomIV();
+ let key = await cryptoSvc.generateRandomKey();
+ let c = 0;
+ cryptoGlobal.subtle.importKey = function (
+ format,
+ keyData,
+ algo,
+ extractable,
+ usages
+ ) {
+ c++;
+ return oldImport.call(
+ cryptoGlobal.subtle,
+ format,
+ keyData,
+ algo,
+ extractable,
+ usages
+ );
+ };
+
+ // Encryption should cause a single counter increment.
+ Assert.equal(c, 0);
+ let cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
+ Assert.equal(c, 1);
+ cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
+ Assert.equal(c, 1);
+
+ // ... as should decryption.
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(c, 2);
+
+ // Un-swizzle.
+ cryptoGlobal.subtle.importKey = oldImport;
+});
+
+// Just verify that it gets populated with the correct bytes.
+add_task(async function test_makeUint8Array() {
+ ChromeUtils.importESModule("resource://gre/modules/ctypes.sys.mjs");
+
+ let item1 = cryptoSvc.makeUint8Array("abcdefghi", false);
+ Assert.ok(item1);
+ for (let i = 0; i < 8; ++i) {
+ Assert.equal(item1[i], "abcdefghi".charCodeAt(i));
+ }
+});
+
+add_task(async function test_encrypt_decrypt() {
+ // First, do a normal run with expected usage... Generate a random key and
+ // iv, encrypt and decrypt a string.
+ var iv = cryptoSvc.generateRandomIV();
+ Assert.equal(iv.length, 24);
+
+ var key = await cryptoSvc.generateRandomKey();
+ Assert.equal(key.length, 44);
+
+ var mySecret = "bacon is a vegetable";
+ var cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ Assert.equal(cipherText.length, 44);
+
+ var clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(clearText.length, 20);
+
+ // Did the text survive the encryption round-trip?
+ Assert.equal(clearText, mySecret);
+ Assert.notEqual(cipherText, mySecret); // just to be explicit
+
+ // Do some more tests with a fixed key/iv, to check for reproducable results.
+ key = "St1tFCor7vQEJNug/465dQ==";
+ iv = "oLjkfrLIOnK2bDRvW4kXYA==";
+
+ _("Testing small IV.");
+ mySecret = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=";
+ let shortiv = "YWJj";
+ let err;
+ try {
+ await cryptoSvc.encrypt(mySecret, key, shortiv);
+ } catch (ex) {
+ err = ex;
+ }
+ Assert.ok(!!err);
+
+ _("Testing long IV.");
+ let longiv = "gsgLRDaxWvIfKt75RjuvFWERt83FFsY2A0TW+0b2iVk=";
+ try {
+ await cryptoSvc.encrypt(mySecret, key, longiv);
+ } catch (ex) {
+ err = ex;
+ }
+ Assert.ok(!!err);
+
+ // Test small input sizes
+ mySecret = "";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "OGQjp6mK1a3fs9k9Ml4L3w==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "x";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "96iMl4vhOxFUW/lVHHzVqg==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "olpPbETRYROCSqFWcH2SWg==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xxx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "rRbpHGyVSZizLX/x43Wm+Q==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xxxx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "HeC7miVGDcpxae9RmiIKAw==");
+ Assert.equal(clearText, mySecret);
+
+ // Test non-ascii input
+ // ("testuser1" using similar-looking glyphs)
+ mySecret = String.fromCharCode(355, 277, 349, 357, 533, 537, 101, 345, 185);
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "Pj4ixByXoH3SU3JkOXaEKPgwRAWplAWFLQZkpJd5Kr4=");
+ Assert.equal(clearText, mySecret);
+
+ // Tests input spanning a block boundary (AES block size is 16 bytes)
+ mySecret = "123456789012345";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "e6c5hwphe45/3VN/M0bMUA==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "1234567890123456";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP1JOIQF87E2vTUvBUQnyV04=");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "12345678901234567";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP5GvxWJ9+GIAS6lXw+5fHTI=");
+ Assert.equal(clearText, mySecret);
+
+ key = "iz35tuIMq4/H+IYw2KTgow==";
+ iv = "TJYrvva2KxvkM8hvOIvWp3==";
+ mySecret = "i like pie";
+
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "DLGx8BWqSCLGG7i/xwvvxg==");
+ Assert.equal(clearText, mySecret);
+
+ key = "c5hG3YG+NC61FFy8NOHQak1ZhMEWO79bwiAfar2euzI=";
+ iv = "gsgLRDaxWvIfKt75RjuvFW==";
+ mySecret = "i like pie";
+
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "o+ADtdMd8ubzNWurS6jt0Q==");
+ Assert.equal(clearText, mySecret);
+
+ key = "St1tFCor7vQEJNug/465dQ==";
+ iv = "oLjkfrLIOnK2bDRvW4kXYA==";
+ mySecret = "does thunder read testcases?";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ Assert.equal(cipherText, "T6fik9Ros+DB2ablH9zZ8FWZ0xm/szSwJjIHZu7sjPs=");
+
+ var badkey = "badkeybadkeybadkeybadk==";
+ var badiv = "badivbadivbadivbadivbad=";
+ var badcipher = "crapinputcrapinputcrapinputcrapinputcrapinp=";
+ var failure;
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, badkey, iv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, key, badiv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, badkey, badiv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(badcipher, key, iv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+});
diff --git a/services/crypto/tests/unit/test_crypto_random.js b/services/crypto/tests/unit/test_crypto_random.js
new file mode 100644
index 0000000000..bd913c81e8
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_random.js
@@ -0,0 +1,52 @@
+const { WeaveCrypto } = ChromeUtils.importESModule(
+ "resource://services-crypto/WeaveCrypto.sys.mjs"
+);
+
+var cryptoSvc = new WeaveCrypto();
+
+add_task(async function test_crypto_random() {
+ if (this.gczeal) {
+ _("Running crypto random tests with gczeal(2).");
+ gczeal(2);
+ }
+
+ // Test salt generation.
+ var salt;
+
+ salt = cryptoSvc.generateRandomBytes(0);
+ Assert.equal(salt.length, 0);
+ salt = cryptoSvc.generateRandomBytes(1);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(2);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(3);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(4);
+ Assert.equal(salt.length, 8);
+ salt = cryptoSvc.generateRandomBytes(8);
+ Assert.equal(salt.length, 12);
+
+ // sanity check to make sure salts seem random
+ var salt2 = cryptoSvc.generateRandomBytes(8);
+ Assert.equal(salt2.length, 12);
+ Assert.notEqual(salt, salt2);
+
+ salt = cryptoSvc.generateRandomBytes(1024);
+ Assert.equal(salt.length, 1368);
+ salt = cryptoSvc.generateRandomBytes(16);
+ Assert.equal(salt.length, 24);
+
+ // Test random key generation
+ var keydata, keydata2, iv;
+
+ keydata = await cryptoSvc.generateRandomKey();
+ Assert.equal(keydata.length, 44);
+ keydata2 = await cryptoSvc.generateRandomKey();
+ Assert.notEqual(keydata, keydata2); // sanity check for randomness
+ iv = cryptoSvc.generateRandomIV();
+ Assert.equal(iv.length, 24);
+
+ if (this.gczeal) {
+ gczeal(0);
+ }
+});
diff --git a/services/crypto/tests/unit/test_jwcrypto.js b/services/crypto/tests/unit/test_jwcrypto.js
new file mode 100644
index 0000000000..02f064d431
--- /dev/null
+++ b/services/crypto/tests/unit/test_jwcrypto.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+});
+
+// Enable logging from jwcrypto.jsm.
+Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug");
+
+add_task(async function test_jwe_roundtrip_ecdh_es_encryption() {
+ const plaintext = crypto.getRandomValues(new Uint8Array(123));
+ const remoteKey = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-256",
+ },
+ true,
+ ["deriveKey"]
+ );
+ const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey);
+ delete remoteJWK.key_ops;
+ const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext);
+ const decrypted = await jwcrypto.decryptJWE(jwe, remoteKey.privateKey);
+ Assert.deepEqual(plaintext, decrypted);
+});
+
+add_task(async function test_jwe_header_includes_key_id() {
+ const plaintext = crypto.getRandomValues(new Uint8Array(123));
+ const remoteKey = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-256",
+ },
+ true,
+ ["deriveKey"]
+ );
+ const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey);
+ delete remoteJWK.key_ops;
+ remoteJWK.kid = "key identifier";
+ const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext);
+ let [header /* other items deliberately ignored */] = jwe.split(".");
+ header = JSON.parse(
+ new TextDecoder().decode(
+ ChromeUtils.base64URLDecode(header, { padding: "reject" })
+ )
+ );
+ Assert.equal(header.kid, "key identifier");
+});
diff --git a/services/crypto/tests/unit/test_load_modules.js b/services/crypto/tests/unit/test_load_modules.js
new file mode 100644
index 0000000000..2c850d3dab
--- /dev/null
+++ b/services/crypto/tests/unit/test_load_modules.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const modules = ["utils.sys.mjs", "WeaveCrypto.sys.mjs"];
+
+function run_test() {
+ for (let m of modules) {
+ let resource = "resource://services-crypto/" + m;
+ _("Attempting to import: " + resource);
+ ChromeUtils.importESModule(resource);
+ }
+}
diff --git a/services/crypto/tests/unit/test_utils_hawk.js b/services/crypto/tests/unit/test_utils_hawk.js
new file mode 100644
index 0000000000..71702b7349
--- /dev/null
+++ b/services/crypto/tests/unit/test_utils_hawk.js
@@ -0,0 +1,346 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+
+function run_test() {
+ initTestLogging();
+
+ run_next_test();
+}
+
+add_task(async function test_hawk() {
+ let compute = CryptoUtils.computeHAWK;
+
+ let method = "POST";
+ let ts = 1353809207;
+ let nonce = "Ygvqdz";
+
+ let credentials = {
+ id: "123456",
+ key: "2983d45yun89q",
+ };
+
+ let uri_https = CommonUtils.makeURI(
+ "https://example.net/somewhere/over/the/rainbow"
+ );
+ let opts = {
+ credentials,
+ ext: "Bazinga!",
+ ts,
+ nonce,
+ payload: "something to write about",
+ contentType: "text/plain",
+ };
+
+ let result = await compute(uri_https, method, opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+ Assert.equal(result.artifacts.ts, ts);
+ Assert.equal(result.artifacts.nonce, nonce);
+ Assert.equal(result.artifacts.method, method);
+ Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
+ Assert.equal(result.artifacts.host, "example.net");
+ Assert.equal(result.artifacts.port, 443);
+ Assert.equal(
+ result.artifacts.hash,
+ "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="
+ );
+ Assert.equal(result.artifacts.ext, "Bazinga!");
+
+ let opts_noext = {
+ credentials,
+ ts,
+ nonce,
+ payload: "something to write about",
+ contentType: "text/plain",
+ };
+ result = await compute(uri_https, method, opts_noext);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'
+ );
+ Assert.equal(result.artifacts.ts, ts);
+ Assert.equal(result.artifacts.nonce, nonce);
+ Assert.equal(result.artifacts.method, method);
+ Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
+ Assert.equal(result.artifacts.host, "example.net");
+ Assert.equal(result.artifacts.port, 443);
+ Assert.equal(
+ result.artifacts.hash,
+ "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="
+ );
+
+ /* Leaving optional fields out should work, although of course then we can't
+ * assert much about the resulting hashes. The resulting header should look
+ * roughly like:
+ * Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U="
+ */
+
+ result = await compute(uri_https, method, { credentials });
+ let fields = result.field.split(" ");
+ Assert.equal(fields[0], "Hawk");
+ Assert.equal(fields[1], 'id="123456",'); // from creds.id
+ Assert.ok(fields[2].startsWith('ts="'));
+ /* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch.
+ * Warning: this test will fail in the year 33658, and for time travellers
+ * who journey earlier than 2001. Please plan accordingly. */
+ Assert.ok(result.artifacts.ts > 1000 * 1000 * 1000);
+ Assert.ok(result.artifacts.ts < 1000 * 1000 * 1000 * 1000);
+ Assert.ok(fields[3].startsWith('nonce="'));
+ Assert.equal(fields[3].length, 'nonce="12345678901=",'.length);
+ Assert.equal(result.artifacts.nonce.length, "12345678901=".length);
+
+ let result2 = await compute(uri_https, method, { credentials });
+ Assert.notEqual(result.artifacts.nonce, result2.artifacts.nonce);
+
+ /* Using an upper-case URI hostname shouldn't affect the hash. */
+
+ let uri_https_upper = CommonUtils.makeURI(
+ "https://EXAMPLE.NET/somewhere/over/the/rainbow"
+ );
+ result = await compute(uri_https_upper, method, opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+
+ /* Using a lower-case method name shouldn't affect the hash. */
+ result = await compute(uri_https_upper, method.toLowerCase(), opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+
+ /* The localtimeOffsetMsec field should be honored. HAWK uses this to
+ * compensate for clock skew between client and server: if the request is
+ * rejected with a timestamp out-of-range error, the error includes the
+ * server's time, and the client computes its clock offset and tries again.
+ * Clients can remember this offset for a while.
+ */
+
+ result = await compute(uri_https, method, {
+ credentials,
+ now: 1378848968650,
+ });
+ Assert.equal(result.artifacts.ts, 1378848968);
+
+ result = await compute(uri_https, method, {
+ credentials,
+ now: 1378848968650,
+ localtimeOffsetMsec: 1000 * 1000,
+ });
+ Assert.equal(result.artifacts.ts, 1378848968 + 1000);
+
+ /* Search/query-args in URIs should be included in the hash. */
+ let makeURI = CommonUtils.makeURI;
+ result = await compute(makeURI("http://example.net/path"), method, opts);
+ Assert.equal(result.artifacts.resource, "/path");
+ Assert.equal(
+ result.artifacts.mac,
+ "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI="
+ );
+
+ result = await compute(makeURI("http://example.net/path/"), method, opts);
+ Assert.equal(result.artifacts.resource, "/path/");
+ Assert.equal(
+ result.artifacts.mac,
+ "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw="
+ );
+
+ result = await compute(
+ makeURI("http://example.net/path?query=search"),
+ method,
+ opts
+ );
+ Assert.equal(result.artifacts.resource, "/path?query=search");
+ Assert.equal(
+ result.artifacts.mac,
+ "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho="
+ );
+
+ /* Test handling of the payload, which is supposed to be a bytestring
+ (String with codepoints from U+0000 to U+00FF, pre-encoded). */
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(result.artifacts.hash, undefined);
+ Assert.equal(
+ result.artifacts.mac,
+ "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="
+ );
+
+ // Empty payload changes nothing.
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: null,
+ });
+ Assert.equal(result.artifacts.hash, undefined);
+ Assert.equal(
+ result.artifacts.mac,
+ "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="
+ );
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "hello",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM="
+ );
+
+ // update, utf-8 payload
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "andré@example.org", // non-ASCII
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="
+ );
+
+ /* If "hash" is provided, "payload" is ignored. */
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=",
+ payload: "something else",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="
+ );
+
+ // the payload "hash" is also non-urlsafe base64 (+/)
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "something else",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "jiZuhsac35oD7IdcblhFncBr8tJFHcwWLr8NIYWr9PQ="
+ );
+
+ /* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes
+ * "ëxample.net" into "xn--xample-ova.net" before hashing. I still think
+ * punycode was a bad joke that got out of the lab and into a spec.
+ */
+
+ result = await compute(makeURI("http://ëxample.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0="
+ );
+ Assert.equal(result.artifacts.host, "xn--xample-ova.net");
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ ext: 'backslash=\\ quote=" EOF',
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="'
+ );
+
+ result = await compute(makeURI("http://example.net:1234/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'
+ );
+
+ /* HAWK (the node.js library) uses a URL parser which stores the "port"
+ * field as a string, but makeURI() gives us an integer. So we'll diverge
+ * on ports with a leading zero. This test vector would fail on the node.js
+ * library (HAWK-1.1.1), where they get a MAC of
+ * "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be
+ * updated to do what we do here, so port="01234" should get the same hash
+ * as port="1234".
+ */
+ result = await compute(makeURI("http://example.net:01234/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'
+ );
+});
+
+add_test(function test_strip_header_attributes() {
+ let strip = CryptoUtils.stripHeaderAttributes;
+
+ Assert.equal(strip(undefined), "");
+ Assert.equal(strip("text/plain"), "text/plain");
+ Assert.equal(strip("TEXT/PLAIN"), "text/plain");
+ Assert.equal(strip(" text/plain "), "text/plain");
+ Assert.equal(strip("text/plain ; charset=utf-8 "), "text/plain");
+
+ run_next_test();
+});
diff --git a/services/crypto/tests/unit/test_utils_httpmac.js b/services/crypto/tests/unit/test_utils_httpmac.js
new file mode 100644
index 0000000000..4831683e70
--- /dev/null
+++ b/services/crypto/tests/unit/test_utils_httpmac.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+
+add_test(function setup() {
+ initTestLogging();
+ run_next_test();
+});
+
+add_task(async 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 = CommonUtils.makeURI("http://10.250.2.176/alias/");
+
+ let result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, {
+ ts,
+ nonce,
+ });
+
+ Assert.equal(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck=");
+
+ Assert.equal(
+ result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="'
+ );
+
+ let ext = "EXTRA DATA; foo,bar=1";
+
+ result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, {
+ ts,
+ nonce,
+ ext,
+ });
+ Assert.equal(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68=");
+ Assert.equal(
+ result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' +
+ 'ext="EXTRA DATA; foo,bar=1"'
+ );
+});
+
+add_task(async function test_nonce_length() {
+ _("Ensure custom nonce lengths are honoured.");
+
+ function get_mac(length) {
+ let uri = CommonUtils.makeURI("http://example.com/");
+ return CryptoUtils.computeHTTPMACSHA1("foo", "bar", "GET", uri, {
+ nonce_bytes: length,
+ });
+ }
+
+ let result = await get_mac(12);
+ Assert.equal(12, atob(result.nonce).length);
+
+ result = await get_mac(2);
+ Assert.equal(2, atob(result.nonce).length);
+
+ result = await get_mac(0);
+ Assert.equal(8, atob(result.nonce).length);
+
+ result = await get_mac(-1);
+ Assert.equal(8, atob(result.nonce).length);
+});
diff --git a/services/crypto/tests/unit/xpcshell.toml b/services/crypto/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..9ce7748357
--- /dev/null
+++ b/services/crypto/tests/unit/xpcshell.toml
@@ -0,0 +1,18 @@
+[DEFAULT]
+head = "head_helpers.js ../../../common/tests/unit/head_helpers.js"
+firefox-appdir = "browser"
+support-files = ["!/services/common/tests/unit/head_helpers.js"]
+
+["test_crypto_crypt.js"]
+
+["test_crypto_random.js"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_jwcrypto.js"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_load_modules.js"]
+
+["test_utils_hawk.js"]
+
+["test_utils_httpmac.js"]