// From https://gist.github.com/guillermodlpa/f6d955f838e9b10d1ef95b8e259b2c58 // From https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8 // To ensure cross-browser support even without a proper SubtleCrypto // impelmentation (or without access to the impelmentation, as is the case with // Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256 // HMAC signatures using nothing but raw JavaScript /* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */ // By giving internal functions names that we can mangle, future calls to // them are reduced to a single byte (minor space savings in minified file) const uint8Array = Uint8Array; const uint32Array = Uint32Array; const pow = Math.pow; // Will be initialized below // Using a Uint32Array instead of a simple array makes the minified code // a bit bigger (we lose our `unshift()` hack), but comes with huge // performance gains const DEFAULT_STATE = new uint32Array(8); const ROUND_CONSTANTS: number[] = []; // Reusable object for expanded message // Using a Uint32Array instead of a simple array makes the minified code // 7 bytes larger, but comes with huge performance gains const M = new uint32Array(64); // After minification the code to compute the default state and round // constants is smaller than the output. More importantly, this serves as a // good educational aide for anyone wondering where the magic numbers come // from. No magic numbers FTW! function getFractionalBits(n: number) { return ((n - (n | 0)) * pow(2, 32)) | 0; } let n = 2; let nPrime = 0; while (nPrime < 64) { // isPrime() was in-lined from its original function form to save // a few bytes let isPrime = true; // Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes // var sqrtN = pow(n, 1 / 2); // So technically to determine if a number is prime you only need to // check numbers up to the square root. However this function only runs // once and we're only computing the first 64 primes (up to 311), so on // any modern CPU this whole function runs in a couple milliseconds. // By going to n / 2 instead of sqrt(n) we net 8 byte savings and no // scaling performance cost for (let factor = 2; factor <= n / 2; factor++) { if (n % factor === 0) { isPrime = false; } } if (isPrime) { if (nPrime < 8) { DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2)); } ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3)); nPrime++; } n++; } // For cross-platform support we need to ensure that all 32-bit words are // in the same endianness. A UTF-8 TextEncoder will return BigEndian data, // so upon reading or writing to our ArrayBuffer we'll only swap the bytes // if our system is LittleEndian (which is about 99% of CPUs) const LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0]; function convertEndian(word: number) { if (LittleEndian) { return ( // byte 1 -> byte 4 (word >>> 24) | // byte 2 -> byte 3 (((word >>> 16) & 0xff) << 8) | // byte 3 -> byte 2 ((word & 0xff00) << 8) | // byte 4 -> byte 1 (word << 24) ); } else { return word; } } function rightRotate(word: number, bits: number) { return (word >>> bits) | (word << (32 - bits)); } function sha256(data: Uint8Array) { // Copy default state const STATE = DEFAULT_STATE.slice(); // Caching this reduces occurrences of ".length" in minified JavaScript // 3 more byte savings! :D const legth = data.length; // Pad data const bitLength = legth * 8; const newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65; // "bytes" and "words" are stored BigEndian const bytes = new uint8Array(newBitLength / 8); const words = new uint32Array(bytes.buffer); bytes.set(data, 0); // Append a 1 bytes[legth] = 0b10000000; // Store length in BigEndian words[words.length - 1] = convertEndian(bitLength); // Loop iterator (avoid two instances of "var") -- saves 2 bytes let round; // Process blocks (512 bits / 64 bytes / 16 words at a time) for (let block = 0; block < newBitLength / 32; block += 16) { const workingState = STATE.slice(); // Rounds for (round = 0; round < 64; round++) { let MRound; // Expand message if (round < 16) { // Convert to platform Endianness for later math MRound = convertEndian(words[block + round]); } else { const gamma0x = M[round - 15]; const gamma1x = M[round - 2]; MRound = M[round - 7] + M[round - 16] + (rightRotate(gamma0x, 7) ^ rightRotate(gamma0x, 18) ^ (gamma0x >>> 3)) + (rightRotate(gamma1x, 17) ^ rightRotate(gamma1x, 19) ^ (gamma1x >>> 10)); } // M array matches platform endianness M[round] = MRound |= 0; // Computation const t1 = (rightRotate(workingState[4], 6) ^ rightRotate(workingState[4], 11) ^ rightRotate(workingState[4], 25)) + ((workingState[4] & workingState[5]) ^ (~workingState[4] & workingState[6])) + workingState[7] + MRound + ROUND_CONSTANTS[round]; const t2 = (rightRotate(workingState[0], 2) ^ rightRotate(workingState[0], 13) ^ rightRotate(workingState[0], 22)) + ((workingState[0] & workingState[1]) ^ (workingState[2] & (workingState[0] ^ workingState[1]))); for (let i = 7; i > 0; i--) { workingState[i] = workingState[i - 1]; } workingState[0] = (t1 + t2) | 0; workingState[4] = (workingState[4] + t1) | 0; } // Update state for (round = 0; round < 8; round++) { STATE[round] = (STATE[round] + workingState[round]) | 0; } } // Finally the state needs to be converted to BigEndian for output // And we want to return a Uint8Array, not a Uint32Array return new uint8Array( new uint32Array( STATE.map(function (val) { return convertEndian(val); }), ).buffer, ); } function hmac(key: Uint8Array, data: ArrayLike<number>) { if (key.length > 64) key = sha256(key); if (key.length < 64) { const tmp = new Uint8Array(64); tmp.set(key, 0); key = tmp; } // Generate inner and outer keys const innerKey = new Uint8Array(64); const outerKey = new Uint8Array(64); for (let i = 0; i < 64; i++) { innerKey[i] = 0x36 ^ key[i]; outerKey[i] = 0x5c ^ key[i]; } // Append the innerKey const msg = new Uint8Array(data.length + 64); msg.set(innerKey, 0); msg.set(data, 64); // Has the previous message and append the outerKey const result = new Uint8Array(64 + 32); result.set(outerKey, 0); result.set(sha256(msg), 64); // Hash the previous message return sha256(result); } // Convert a string to a Uint8Array, SHA-256 it, and convert back to string const encoder = new TextEncoder(); export function sign( inputKey: string | Uint8Array, inputData: string | Uint8Array, ) { const key = typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey; const data = typeof inputData === "string" ? encoder.encode(inputData) : inputData; return hmac(key, data); } export function hex(bin: Uint8Array) { return bin.reduce((acc, val) => { const hexVal = "00" + val.toString(16); return acc + hexVal.substring(hexVal.length - 2); }, ""); } export function hash(str: string) { return hex(sha256(encoder.encode(str))); } export function hashWithSecret(str: string, secret: string) { return hex(sign(secret, str)).toString(); }