hmac-obj
Version:
JavaScript implementation of HMAC generation and verification for the browser and node.js.
584 lines (474 loc) • 17.4 kB
JavaScript
'use strict';
var baseEx = require('base-ex');
class PermissionError extends Error {
constructor(message) {
super(message);
this.name = "PermissionError";
}
}
const cryptoSubtle = {
importKey: async (key, digestmod, format="raw", permitExports=false) => {
return await globalThis.crypto.subtle.importKey(
format,
key,
{
name: "HMAC",
hash: {name: digestmod}
},
permitExports,
["sign", "verify"]
);
},
generateKey: async (digestmod, permitExports=false) => {
return await globalThis.crypto.subtle.generateKey(
{
name: "HMAC",
hash: {name: digestmod}
},
permitExports,
["sign", "verify"]
);
},
exportKey: async (key, format="raw") => {
if (!key.extractable) {
throw new PermissionError("Key exports are not allowed. You can permit this during key-generation.");
}
return await globalThis.crypto.subtle.exportKey(format, key);
},
sign: async (msg, key) => {
return await globalThis.crypto.subtle.sign(
{
name: "HMAC",
hash: key.algorithm.hash.name
},
key,
msg
);
},
verify: async (msg, signature, key) => {
return await globalThis.crypto.subtle.verify(
"HMAC",
key,
signature,
msg
);
},
};
const getDigestModFromParam = (digestmod, digestmods) => {
if (!digestmod) {
throw new TypeError("Missing required parameter 'digestmod'.");
}
let bits = [].concat(String(digestmod).match(/[0-9]+/)).at(0)|0;
digestmod = `SHA-${bits}`;
if (!digestmods.includes(digestmod)) {
throw new TypeError(`Available digestmod are: '${digestmods.join(", ")}'.`);
}
if (bits === 1) {
bits = 160;
}
return [ digestmod, bits ];
};
/**
* [HMACObj]{@link https://github.com/UmamiAppearance/HMACObj}
*
* @version 1.0.6
* @author UmamiAppearance [mail@umamiappearance.eu]
* @license MIT
*/
let BASE_EX;
if (typeof baseEx.BaseEx === "undefined") {
throw new Error("BaseEx is required for this module to work. Please make sure the whole library or the BaseEx class can be globally found under the name 'BaseEx'.");
}
if ("BaseEx" in baseEx.BaseEx) {
BASE_EX = new baseEx.BaseEx.BaseEx();
} else {
BASE_EX = new baseEx.BaseEx();
}
const DIGESTMODS = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"];
const KEY_FORMATS = ["raw", "jwk"];
/**
* Creates a HMAC-SHA-(1-512) object for JavaScript.
* It is related to pythons hmac library in its methods
* and features but with many extras.
*
* It provides an easy access to the global Crypto.subtle
* method, and also makes it possible to get multiple
* different digest methods.
*
* @see: https://docs.python.org/3/library/hmac.html
*/
class HMACObj {
#bits = null;
#digest = null;
#digestmod = null;
#input = [];
#key = null;
#keyFormats = this.constructor.keyFormats();
#keyIsExportable = null;
/**
* Creates a HMAC Object.
* @param {string|number} digestmod - The parameter must contain one of the numbers (1/256/384/512), eg: SHA-1, sha256, 384, ...
*/
constructor(digestmod) {
[ this.#digestmod, this.#bits ] = getDigestModFromParam(digestmod, DIGESTMODS);
this.#addConverters();
}
/**
* BaseEx instance.
*/
static baseEx = BASE_EX;
/**
* Static method to receive information about the
* available digestmod.
* @returns {set} - A set of available digestmod.
*/
static digestmodsAvailable() {
return new Set(DIGESTMODS);
}
/**
* Static method to receive information about the
* available key formats.
* @returns {set} - A set of available key formats.
*/
static keyFormats() {
return new Set(KEY_FORMATS);
}
/**
* Static method to generate a crypto key for the HMAC algorithm.
* @param {string|number} digestmod - The parameter must contain one of the numbers (1/256/384/512), eg: SHA-1, sha256, 384, ...
* @param {boolean} [permitExports=false] - If true the key can get exported.
* @returns {Object} - Crypto Key.
*/
static async generateKey(digestmod="", permitExports=false) {
digestmod = getDigestModFromParam(digestmod, DIGESTMODS).at(0);
return await cryptoSubtle.generateKey(digestmod, permitExports);
}
/**
* Return a === b. This function uses an approach designed
* to prevent timing analysis by avoiding content-based
* short circuiting behavior, making it appropriate for
* cryptography.
* a and b (or more precisely their byte representation)
* must both be of the same type.
* @param {*} a
* @param {*} b
* @returns
*/
static compareDigest(a, b) {
if (typeof a === "undefined" || typeof b === "undefined") {
throw new Error("HMACobj.compareDigest takes exactly two positional arguments.");
}
a = BASE_EX.byteConverter.encode(a, "uint8");
b = BASE_EX.byteConverter.encode(b, "uint8");
// set the greater array as 'A'
let A, B;
if (a.byteLength > b.byteLength) {
A = a;
B = b;
} else {
A = b;
B = a;
}
// Walk through the greater (or equally sized) array and
// compare each value with the value at the corresponding
// index. (If B is smaller it will return undefined at a
// certain point).
const test = A.map((byte, i) => {
return byte === B.at(i);
});
// Only if every value is true the result of the
// reduced array will be 1. If one value is false
// the result will be zero.
const passed = Boolean(test.reduce((x, y) => x*y));
return passed;
}
/**
* Asynchronously creates a new instance. In contrast
* to the regular new operator a message and key can
* be provided. If a message is set, a key must also be
* handed over or a crypto key gets generated automatically.
* A message gets passed to the 'update' method.
*
* @param {*} key - Almost any input can be provided. It gets converted to bytes and used for the crypto key generation.
* @param {*} msg - Input gets converted to bytes and processed by crypto.subtle.digest.
* @param {string|number} digestmod - The parameter must contain one of the numbers (1/256/384/512), eg: SHA-1, sha256, 384, ...
* @param {string} [keyFormat="raw"] - As defined by KEY_FORMATS. If not set to raw, 'key' must match the format.
* @param {boolean} [permitExports=false] - If a key is getting generated, this bool sets it to exportable or not.
* @returns {Object} - A HMACObj instance.
*/
static async new(key=null, msg=null, digestmod="", keyFormat="raw", permitExports=false) {
const hmacObj = new this(digestmod);
if (key) {
if (keyFormat === "object") {
await hmacObj.setKey(key);
} else {
await hmacObj.importKey(key, keyFormat, permitExports);
}
}
if (msg !== null) {
if (!key) {
await hmacObj.generateKey();
console.warn("A message but no key was provided. The key was generated for you.");
}
await hmacObj.update(msg);
}
return hmacObj;
}
/**
* The size of the resulting hash in bytes.
*/
get digestSize() {
return this.#bits / 8;
}
/**
* The internal block size of the hash algorithm in bytes.
*/
get blockSize() {
return this.#bits > 256 ? 128 : 64;
}
/**
* The canonical name of this HMAC, always uppercase,
* e.g. HMAC-SHA-256.
*/
get name() {
return "HMAC-" + this.#digestmod;
}
/**
* Shortcut to the BaseEx byte converter.
* @param {*} input - Almost any input.
* @returns {Object} - Uint8Array/Byte representation of the input.
*/
#ensureBytes(input) {
return BASE_EX.byteConverter.encode(input, "bytes");
}
/**
* Test wether the provided format matches the
* predetermined formats.
*/
#testFormat(format) {
if (!this.#keyFormats.has(format)) {
throw new TypeError(
`Invalid key format '${format}'\n\nValid formats are: ${KEY_FORMATS.join(", ")}`
);
}
}
/**
* Test wether a key is assigned to the current instance.
*/
#testKeyAvail() {
if (this.#key === null) {
throw new Error("No key is assigned yet. Import or generate key.");
}
}
/**
* Convert buffer to many different representations.
* (Helper for method 'sign')
* @param {ArrayBuffer} buffer - ArrayBuffer.
* @param {string} base - Base Representation as required by BaseEx.
* @returns {string} - Base Representation.
*/
#bufferToBase(buffer, base) {
const decapitalize = str => str.charAt(0).toLowerCase().concat(str.slice(1));
const errMsg = "Invalid base conversion keyword.";
base = decapitalize(base.replace(/^to/, ""));
if (base === "hex" || base == "hexdigest") {
base = "base16";
}
else if (base === "bytes") {
base = "byteConverter";
}
else if ((/SimpleBase/i).test(base)) {
base = `base${[].concat(String(base).match(/[0-9]+/)).at(0)|0}`;
if (!(base in BASE_EX.simpleBase)) {
throw new TypeError(errMsg);
}
return BASE_EX.simpleBase[base].encode(buffer);
}
if (!(base in BASE_EX)) {
throw new TypeError(errMsg);
}
return BASE_EX[base].encode(buffer);
}
/**
* Update the HMAC object with almost any input. The input
* gets converted to a Uint8Array. Unless 'replace' is set
* to true, repeated calls are equivalent to a single call
* with the concatenation of all the arguments:
* hmacObj.update(a); hmacObj.update(b) is in many occasions
* equivalent to hmacObj.update(a+b).
*
* (Note: The process is a concatenation of bytes. Take as
* an exception for instance:
* hmacObj.update(1); hmacObj.update(2) which is not the same
* as hmacObj.update(1+2))
*
* @param {*} msg - Input gets converted to bytes and processed by crypto.subtle.digest.
* @param {boolean} replace - If true, the input is not concatenated with former input.
*/
async update(msg, replace=false) {
msg = this.#ensureBytes(msg);
this.#testKeyAvail();
if (replace) {
this.#input = Array.from(msg);
} else {
this.#input = this.#input.concat(Array.from(msg));
}
this.#digest = await cryptoSubtle.sign(
Uint8Array.from(this.#input),
this.#key
);
}
/**
* Shortcut to 'update(input, true)'.
* @param {*} msg - Input gets converted to bytes and processed by crypto.subtle.digest.
*/
async replace(msg) {
await this.update(msg, true);
}
/**
* Method to replace the assigned Crypto Key.
* @param {Object} cryptoKey - The new Crypto Key.
*/
async setKey(cryptoKey) {
this.#key = cryptoKey;
if (this.#input.length) {
console.warn("A new crypto key was established. A fresh digest is now getting calculated.");
this.#digest = await cryptoSubtle.sign(
Uint8Array.from(this.#input),
this.#key
);
}
}
/**
* Import a Crypto Key from almost any input or
* a pre existing key.
* @param {*} key - Almost any input can be provided. It gets converted to bytes and used for the crypto key generation.
* @param {string} [format="raw"] - As defined by KEY_FORMATS. If not set to raw, 'key' must match the format.
* @param {boolean} [permitExports=false] - This bool sets the generated key to exportable or not.
*/
async importKey(key, format="raw", permitExports=false) {
if (format === "raw") {
key = this.#ensureBytes(key);
} else {
this.#testFormat(format);
}
this.#keyIsExportable = permitExports;
const keyObj = await cryptoSubtle.importKey(key, this.#digestmod, format, permitExports);
await this.setKey(keyObj);
}
/**
* Method to apply a auto generated Crypto Key
* to the instance.
* @param {boolean} [permitExports=true] - This bool sets the generated key to exportable or not.
*/
async generateKey(permitExports=true) {
this.#keyIsExportable = Boolean(permitExports);
const keyObj = await cryptoSubtle.generateKey(this.#digestmod, this.#keyIsExportable);
await this.setKey(keyObj);
}
/**
* Exports the Crypto Key assigned to the instance,
* if it is a exportable key.
* @param {string} [format="raw"] - As defined by KEY_FORMATS.
* @returns {Object} - Crypto Key
*/
async exportKey(format="raw") {
this.#testFormat(format);
if (this.#key === null) {
throw new Error("Key is unset.");
}
if (!this.#keyIsExportable) {
throw new PermissionError("Key exports are not allowed. You have to permit this before key-generation.");
}
const key = await cryptoSubtle.exportKey(this.#key, format);
return key;
}
/**
* Return a copy (“clone”) of the hmac object. This can be used
* to efficiently compute the digests of strings that share a
* common initial substring.
* @returns {Object} - HMACObject instance.
*/
async copy() {
return await this.constructor.new(
this.#key,
this.#input.length ? Uint8Array.from(this.#input) : null,
this.#digestmod,
"object",
this.#keyIsExportable
);
}
/**
* Signs a single message independent from the current
* instance message.
* @param {*} msg - Input gets converted to bytes and processed by crypto.subtle.digest.
* @param {*} [base=null] - Optional Base Representation as required by BaseEx.
* @returns {ArrayBuffer|string} - ArrayBuffer or a representation of the signed message.
*/
async sign(msg, base=null) {
this.#testKeyAvail();
msg = this.#ensureBytes(msg);
const buffer = await cryptoSubtle.sign(msg, this.#key);
if (base !== null) {
return this.#bufferToBase(buffer, base);
}
return buffer;
}
/**
* A given message and signature can be tested
* if it is signed with the current instance
* crypto key.
* @param {*} msg - Message.
* @param {ArrayBuffer} signature - Signature as ArrayBuffer.
* @returns {boolean} - Verification result.
*/
async verify(msg, signature) {
msg = this.#ensureBytes(msg);
this.#testKeyAvail();
if (this.signature === null) {
throw new TypeError("Signature must be provided");
}
const isValid = await cryptoSubtle.verify(msg, signature, this.#key);
return isValid;
}
/**
* Returns the current digest as an ArrayBuffer;
* @returns {ArrayBuffer}
*/
digest() {
return this.#digest;
}
/**
* Appends BaseEx encoders to the returned object for the ability
* to covert the byte array of a hash to many representations.
*/
#addConverters() {
const detach = (arr, str) => arr.splice(arr.indexOf(str), 1);
const capitalize = str => str.charAt(0).toUpperCase().concat(str.slice(1));
this.hexdigest = () => this.#digest
? BASE_EX.base16.encode(this.#digest)
: null;
const converters = Object.keys(BASE_EX);
this.basedigest = {
toSimpleBase: {}
};
detach(converters, "base1");
detach(converters, "byteConverter");
detach(converters, "simpleBase");
for (const converter of converters) {
this.basedigest[`to${capitalize(converter)}`] = (...args) => this.#digest
? BASE_EX[converter].encode(this.#digest, ...args)
: null;
}
for (const converter in BASE_EX.simpleBase) {
this.basedigest.toSimpleBase[capitalize(converter)] = (...args) => this.#digest
? BASE_EX.simpleBase[converter].encode(this.#digest, ...args)
: null;
}
this.basedigest.toBytes = (...args) => this.#digest
? BASE_EX.byteConverter.encode(this.#digest, ...args)
: null;
}
}
module.exports = HMACObj;
//# sourceMappingURL=hmac-obj.cjs.map