summaryrefslogtreecommitdiffstats
path: root/services/crypto/modules
diff options
context:
space:
mode:
Diffstat (limited to 'services/crypto/modules')
-rw-r--r--services/crypto/modules/WeaveCrypto.js266
-rw-r--r--services/crypto/modules/utils.js584
2 files changed, 850 insertions, 0 deletions
diff --git a/services/crypto/modules/WeaveCrypto.js b/services/crypto/modules/WeaveCrypto.js
new file mode 100644
index 000000000..c040c4f6f
--- /dev/null
+++ b/services/crypto/modules/WeaveCrypto.js
@@ -0,0 +1,266 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["WeaveCrypto"];
+
+var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/async.js");
+
+Cu.importGlobalProperties(['crypto']);
+
+const CRYPT_ALGO = "AES-CBC";
+const CRYPT_ALGO_LENGTH = 256;
+const AES_CBC_IV_SIZE = 16;
+const OPERATIONS = { ENCRYPT: 0, DECRYPT: 1 };
+const UTF_LABEL = "utf-8";
+
+const KEY_DERIVATION_ALGO = "PBKDF2";
+const KEY_DERIVATION_HASHING_ALGO = "SHA-1";
+const KEY_DERIVATION_ITERATIONS = 4096; // PKCS#5 recommends at least 1000.
+const DERIVED_KEY_ALGO = CRYPT_ALGO;
+
+this.WeaveCrypto = function WeaveCrypto() {
+ this.init();
+};
+
+WeaveCrypto.prototype = {
+ prefBranch : null,
+ debug : true, // services.sync.log.cryptoDebug
+
+ observer : {
+ _self : null,
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.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, false);
+ this.observer._self = this;
+ try {
+ this.debug = this.prefBranch.getBoolPref("cryptoDebug");
+ } catch (x) {
+ this.debug = false;
+ }
+ XPCOMUtils.defineLazyGetter(this, 'encoder', () => new TextEncoder(UTF_LABEL));
+ XPCOMUtils.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;
+ },
+
+ encrypt(clearTextUCS2, symmetricKey, iv) {
+ this.log("encrypt() called");
+ let clearTextBuffer = this.encoder.encode(clearTextUCS2).buffer;
+ let encrypted = this._commonCrypt(clearTextBuffer, symmetricKey, iv, OPERATIONS.ENCRYPT);
+ return this.encodeBase64(encrypted);
+ },
+
+ decrypt(cipherText, symmetricKey, iv) {
+ this.log("decrypt() called");
+ if (cipherText.length) {
+ cipherText = atob(cipherText);
+ }
+ let cipherTextBuffer = this.byteCompressInts(cipherText);
+ let decrypted = 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)
+ */
+ _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 "Invalid IV size; must be " + AES_CBC_IV_SIZE + " bytes.";
+ }
+
+ let iv = this.byteCompressInts(ivStr);
+ let symKey = this.importSymKey(symKeyStr, operation);
+ let cryptMethod = (operation === OPERATIONS.ENCRYPT
+ ? crypto.subtle.encrypt
+ : crypto.subtle.decrypt)
+ .bind(crypto.subtle);
+ let algo = { name: CRYPT_ALGO, iv: iv };
+
+
+ return Async.promiseSpinningly(
+ cryptMethod(algo, symKey, data)
+ .then(keyBytes => new Uint8Array(keyBytes))
+ );
+ },
+
+
+ generateRandomKey() {
+ this.log("generateRandomKey() called");
+ let algo = {
+ name: CRYPT_ALGO,
+ length: CRYPT_ALGO_LENGTH
+ };
+ return Async.promiseSpinningly(
+ crypto.subtle.generateKey(algo, true, [])
+ .then(key => crypto.subtle.exportKey("raw", key))
+ .then(keyBytes => {
+ keyBytes = new Uint8Array(keyBytes);
+ return this.encodeBase64(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: {},
+ 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 "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"];
+
+ return Async.promiseSpinningly(
+ crypto.subtle.importKey("raw", symmetricKeyBuffer, algo, false, usages)
+ .then(symKey => {
+ 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);
+ },
+
+ /**
+ * Returns the expanded data string for the derived key.
+ */
+ deriveKeyFromPassphrase(passphrase, saltStr, keyLength = 32) {
+ this.log("deriveKeyFromPassphrase() called.");
+ let keyData = this.makeUint8Array(passphrase, false);
+ let salt = this.makeUint8Array(saltStr, true);
+ let importAlgo = { name: KEY_DERIVATION_ALGO };
+ let deriveAlgo = {
+ name: KEY_DERIVATION_ALGO,
+ salt: salt,
+ iterations: KEY_DERIVATION_ITERATIONS,
+ hash: { name: KEY_DERIVATION_HASHING_ALGO },
+ };
+ let derivedKeyType = {
+ name: DERIVED_KEY_ALGO,
+ length: keyLength * 8,
+ };
+ return Async.promiseSpinningly(
+ crypto.subtle.importKey("raw", keyData, importAlgo, false, ["deriveKey"])
+ .then(key => crypto.subtle.deriveKey(deriveAlgo, key, derivedKeyType, true, []))
+ .then(derivedKey => crypto.subtle.exportKey("raw", derivedKey))
+ .then(keyBytes => {
+ keyBytes = new Uint8Array(keyBytes);
+ return this.expandData(keyBytes);
+ })
+ );
+ },
+};
diff --git a/services/crypto/modules/utils.js b/services/crypto/modules/utils.js
new file mode 100644
index 000000000..c17f5dfa1
--- /dev/null
+++ b/services/crypto/modules/utils.js
@@ -0,0 +1,584 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+this.EXPORTED_SYMBOLS = ["CryptoUtils"];
+
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.CryptoUtils = {
+ xor: function 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.
+ */
+ generateRandomBytes: function generateRandomBytes(length) {
+ let rng = Cc["@mozilla.org/security/random-generator;1"]
+ .createInstance(Ci.nsIRandomGenerator);
+ let bytes = rng.generateRandomBytes(length);
+ return CommonUtils.byteArrayToString(bytes);
+ },
+
+ /**
+ * UTF8-encode a message and hash it with the given hasher. Returns a
+ * string containing bytes. The hasher is reset if it's an HMAC hasher.
+ */
+ digestUTF8: function digestUTF8(message, hasher) {
+ let data = this._utf8Converter.convertToByteArray(message, {});
+ hasher.update(data, data.length);
+ let result = hasher.finish(false);
+ if (hasher instanceof Ci.nsICryptoHMAC) {
+ hasher.reset();
+ }
+ return result;
+ },
+
+ /**
+ * Treat the given message as a bytes string and hash it with the given
+ * hasher. Returns a string containing bytes. The hasher is reset if it's
+ * an HMAC hasher.
+ */
+ digestBytes: function digestBytes(message, hasher) {
+ // No UTF-8 encoding for you, sunshine.
+ let bytes = Array.prototype.slice.call(message).map(b => b.charCodeAt(0));
+ hasher.update(bytes, bytes.length);
+ let result = hasher.finish(false);
+ if (hasher instanceof Ci.nsICryptoHMAC) {
+ hasher.reset();
+ }
+ return result;
+ },
+
+ /**
+ * Encode the message into UTF-8 and feed the resulting bytes into the
+ * given hasher. Does not return a hash. This can be called multiple times
+ * with a single hasher, but eventually you must extract the result
+ * yourself.
+ */
+ updateUTF8: function(message, hasher) {
+ let bytes = this._utf8Converter.convertToByteArray(message, {});
+ hasher.update(bytes, bytes.length);
+ },
+
+ /**
+ * UTF-8 encode a message and perform a SHA-1 over it.
+ *
+ * @param message
+ * (string) Buffer to perform operation on. Should be a JS string.
+ * It is possible to pass in a string representing an array
+ * of bytes. But, you probably don't want to UTF-8 encode
+ * such data and thus should not be using this function.
+ *
+ * @return string
+ * Raw bytes constituting SHA-1 hash. Value is a JS string. Each
+ * character is the byte value for that offset. Returned string
+ * always has .length == 20.
+ */
+ UTF8AndSHA1: function UTF8AndSHA1(message) {
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA1);
+
+ return CryptoUtils.digestUTF8(message, hasher);
+ },
+
+ sha1: function sha1(message) {
+ return CommonUtils.bytesAsHex(CryptoUtils.UTF8AndSHA1(message));
+ },
+
+ sha1Base32: function sha1Base32(message) {
+ return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message));
+ },
+
+ sha256(message) {
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
+ },
+
+ /**
+ * Produce an HMAC key object from a key string.
+ */
+ makeHMACKey: function makeHMACKey(str) {
+ return Svc.KeyFactory.keyFromString(Ci.nsIKeyObject.HMAC, str);
+ },
+
+ /**
+ * Produce an HMAC hasher and initialize it with the given HMAC key.
+ */
+ makeHMACHasher: function makeHMACHasher(type, key) {
+ let hasher = Cc["@mozilla.org/security/hmac;1"]
+ .createInstance(Ci.nsICryptoHMAC);
+ hasher.init(type, key);
+ return hasher;
+ },
+
+ /**
+ * HMAC-based Key Derivation (RFC 5869).
+ */
+ hkdf: function hkdf(ikm, xts, info, len) {
+ const BLOCKSIZE = 256 / 8;
+ if (typeof xts === undefined)
+ xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0);
+ let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
+ CryptoUtils.makeHMACKey(xts));
+ let prk = CryptoUtils.digestBytes(ikm, h);
+ return CryptoUtils.hkdfExpand(prk, info, len);
+ },
+
+ /**
+ * HMAC-based Key Derivation Step 2 according to RFC 5869.
+ */
+ hkdfExpand: function hkdfExpand(prk, info, len) {
+ const BLOCKSIZE = 256 / 8;
+ let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
+ CryptoUtils.makeHMACKey(prk));
+ let T = "";
+ let Tn = "";
+ let iterations = Math.ceil(len/BLOCKSIZE);
+ for (let i = 0; i < iterations; i++) {
+ Tn = CryptoUtils.digestBytes(Tn + info + String.fromCharCode(i + 1), h);
+ T += Tn;
+ }
+ return T.slice(0, len);
+ },
+
+ /**
+ * PBKDF2 implementation in Javascript.
+ *
+ * The arguments to this function correspond to items in
+ * PKCS #5, v2.0 pp. 9-10
+ *
+ * P: the passphrase, an octet string: e.g., "secret phrase"
+ * S: the salt, an octet string: e.g., "DNXPzPpiwn"
+ * c: the number of iterations, a positive integer: e.g., 4096
+ * dkLen: the length in octets of the destination
+ * key, a positive integer: e.g., 16
+ * hmacAlg: The algorithm to use for hmac
+ * hmacLen: The hmac length
+ *
+ * The default value of 20 for hmacLen is appropriate for SHA1. For SHA256,
+ * hmacLen should be 32.
+ *
+ * The output is an octet string of length dkLen, which you
+ * can encode as you wish.
+ */
+ pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen,
+ hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) {
+
+ // We don't have a default in the algo itself, as NSS does.
+ // Use the constant.
+ if (!dkLen) {
+ dkLen = SYNC_KEY_DECODED_LENGTH;
+ }
+
+ function F(S, c, i, h) {
+
+ function XOR(a, b, isA) {
+ if (a.length != b.length) {
+ return false;
+ }
+
+ let val = [];
+ for (let i = 0; i < a.length; i++) {
+ if (isA) {
+ val[i] = a[i] ^ b[i];
+ } else {
+ val[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
+ }
+ }
+
+ return val;
+ }
+
+ let ret;
+ let U = [];
+
+ /* Encode i into 4 octets: _INT */
+ let I = [];
+ I[0] = String.fromCharCode((i >> 24) & 0xff);
+ I[1] = String.fromCharCode((i >> 16) & 0xff);
+ I[2] = String.fromCharCode((i >> 8) & 0xff);
+ I[3] = String.fromCharCode(i & 0xff);
+
+ U[0] = CryptoUtils.digestBytes(S + I.join(''), h);
+ for (let j = 1; j < c; j++) {
+ U[j] = CryptoUtils.digestBytes(U[j - 1], h);
+ }
+
+ ret = U[0];
+ for (let j = 1; j < c; j++) {
+ ret = CommonUtils.byteArrayToString(XOR(ret, U[j]));
+ }
+
+ return ret;
+ }
+
+ let l = Math.ceil(dkLen / hmacLen);
+ let r = dkLen - ((l - 1) * hmacLen);
+
+ // Reuse the key and the hasher. Remaking them 4096 times is 'spensive.
+ let h = CryptoUtils.makeHMACHasher(hmacAlg,
+ CryptoUtils.makeHMACKey(P));
+
+ let T = [];
+ for (let i = 0; i < l;) {
+ T[i] = F(S, c, ++i, h);
+ }
+
+ let ret = "";
+ for (let i = 0; i < l-1;) {
+ ret += T[i++];
+ }
+ ret += T[l - 1].substr(0, r);
+
+ return ret;
+ },
+
+ deriveKeyFromPassphrase: function deriveKeyFromPassphrase(passphrase,
+ salt,
+ keyLength,
+ forceJS) {
+ if (Svc.Crypto.deriveKeyFromPassphrase && !forceJS) {
+ return Svc.Crypto.deriveKeyFromPassphrase(passphrase, salt, keyLength);
+ }
+ else {
+ // Fall back to JS implementation.
+ // 4096 is hardcoded in WeaveCrypto, so do so here.
+ return CryptoUtils.pbkdf2Generate(passphrase, atob(salt), 4096,
+ keyLength);
+ }
+ },
+
+ /**
+ * 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.
+ */
+ computeHTTPMACSHA1: function 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.generateRandomBytes(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.path + "\n" +
+ host + "\n" +
+ port + "\n" +
+ ext + "\n";
+
+ let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1,
+ CryptoUtils.makeHMACKey(key));
+ let mac = CryptoUtils.digestBytes(requestString, hasher);
+
+ function getHeader() {
+ return CryptoUtils.getHTTPMACSHA1Header(this.identifier, this.ts,
+ this.nonce, this.mac, this.ext);
+ }
+
+ return {
+ identifier: identifier,
+ key: key,
+ method: usedMethod,
+ hostname: host,
+ port: port,
+ mac: mac,
+ nonce: nonce,
+ ts: ts,
+ ext: ext,
+ getHeader: 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: function(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
+ * algorithm - (string) which hash to use: "sha1" or "sha256"
+ * 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
+ * (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)
+ */
+ computeHAWK: function(uri, method, options) {
+ let credentials = options.credentials;
+ let ts = options.ts || Math.floor(((options.now || Date.now()) +
+ (options.localtimeOffsetMsec || 0))
+ / 1000);
+
+ let hash_algo, hmac_algo;
+ if (credentials.algorithm == "sha1") {
+ hash_algo = Ci.nsICryptoHash.SHA1;
+ hmac_algo = Ci.nsICryptoHMAC.SHA1;
+ } else if (credentials.algorithm == "sha256") {
+ hash_algo = Ci.nsICryptoHash.SHA256;
+ hmac_algo = Ci.nsICryptoHMAC.SHA256;
+ } else {
+ throw new Error("Unsupported algorithm: " + credentials.algorithm);
+ }
+
+ 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: ts,
+ nonce: options.nonce || btoa(CryptoUtils.generateRandomBytes(8)),
+ method: method.toUpperCase(),
+ resource: uri.path, // 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) {
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hash_algo);
+ CryptoUtils.updateUTF8("hawk.1.payload\n", hasher);
+ CryptoUtils.updateUTF8(contentType+"\n", hasher);
+ CryptoUtils.updateUTF8(options.payload, hasher);
+ CryptoUtils.updateUTF8("\n", hasher);
+ let hash = hasher.finish(false);
+ // HAWK specifies this .hash to use +/ (not _-) and include the
+ // trailing "==" padding.
+ let hash_b64 = btoa(hash);
+ artifacts.hash = hash_b64;
+ }
+
+ 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";
+
+ let hasher = CryptoUtils.makeHMACHasher(hmac_algo,
+ CryptoUtils.makeHMACKey(credentials.key));
+ artifacts.mac = btoa(CryptoUtils.digestBytes(requestString, hasher));
+ // 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: artifacts,
+ field: header,
+ };
+ },
+
+};
+
+XPCOMUtils.defineLazyGetter(CryptoUtils, "_utf8Converter", function() {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ return converter;
+});
+
+var Svc = {};
+
+XPCOMUtils.defineLazyServiceGetter(Svc,
+ "KeyFactory",
+ "@mozilla.org/security/keyobjectfactory;1",
+ "nsIKeyObjectFactory");
+
+Svc.__defineGetter__("Crypto", function() {
+ let ns = {};
+ Cu.import("resource://services-crypto/WeaveCrypto.js", ns);
+
+ let wc = new ns.WeaveCrypto();
+ delete Svc.Crypto;
+ return Svc.Crypto = wc;
+});
+
+Observers.add("xpcom-shutdown", function unloadServices() {
+ Observers.remove("xpcom-shutdown", unloadServices);
+
+ for (let k in Svc) {
+ delete Svc[k];
+ }
+});