538 lines
16 KiB
JavaScript
538 lines
16 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/. */
|
|
|
|
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 {BufferSource} key
|
|
* @param {BufferSource} data
|
|
* @returns {Promise<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];
|
|
}
|
|
});
|