gecko/services/crypto/modules/utils.js

578 lines
20 KiB
JavaScript

/* 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 {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 = [b.charCodeAt() for each (b in message)];
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));
},
/**
* 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) {
let 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;
});
let 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];
}
});