WebCrypto API is available in many environments, but there are cases like some Web Workers implementations where the crypto object cannot be used. This is a pure JavaScript implementation for such situations. However, you need to be prepared for performance to be inferior to native implementations.
Code
const K: Readonly<Uint32Array> = initUint32([ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,]);
const MAX_UINT32_PLUS_1 = 0x100000000;const N_64 = 64;const N_512 = 512;
function sha256(message: Uint8Array | string) { const state = initUint32([ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, ]);
const data = isString(message) ? strToBuffer(message) : message; const blocks = preprocess(data); const w = initUint32(N_64); const view = new DataView(blocks.buffer); for (let offset = 0; offset < blocks.length; offset += N_64) { // Initialize message schedule for (let i = 0; i < 16; i++) { w[i] = view.getUint32(offset + i * 4, false); }
// Extend message schedule for (let i = 16; i < N_64; i++) { const im15 = w[i - 15]; const im2 = w[i - 2]; const im16 = w[i - 16]; const im7 = w[i - 7];
assertIsDefined(im15); assertIsDefined(im2); assertIsDefined(im16); assertIsDefined(im7);
const s0 = rightRotate(im15, 7) ^ rightRotate(im15, 18) ^ (im15 >>> 3);
const s1 = rightRotate(im2, 17) ^ rightRotate(im2, 19) ^ (im2 >>> 10); w[i] = (im16 + s0 + im7 + s1) >>> 0; }
let a = nonNull(state[0]); let b = nonNull(state[1]); let c = nonNull(state[2]); let d = nonNull(state[3]); let e = nonNull(state[4]); let f = nonNull(state[5]); let g = nonNull(state[6]); let h = nonNull(state[7]);
// Main loop for (let i = 0; i < N_64; i++) { const S1 = rightRotate(nonNull(e), 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25); const ch = (e & f) ^ (~e & g); const temp1 = (h + S1 + ch + nonNull(K[i]) + nonNull(w[i])) >>> 0; const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22); const maj = (a & b) ^ (a & c) ^ (b & c); const temp2 = (S0 + maj) >>> 0;
h = g; g = f; f = e; e = (d + temp1) >>> 0; d = c; c = b; b = a; a = (temp1 + temp2) >>> 0; }
// Update state const current = state; state[0] = (nonNull(current[0]) + a) >>> 0; state[1] = (nonNull(current[1]) + b) >>> 0; state[2] = (nonNull(current[2]) + c) >>> 0; state[3] = (nonNull(current[3]) + d) >>> 0; state[4] = (nonNull(current[4]) + e) >>> 0; state[5] = (nonNull(current[5]) + f) >>> 0; state[6] = (nonNull(current[6]) + g) >>> 0; state[7] = (nonNull(current[7]) + h) >>> 0; }
const hash = new Uint8Array(32); const hashView = new DataView(hash.buffer); for (let i = 0; i < 8; i++) { hashView.setUint32(i * 4, nonNull(state[i]), false); }
return toHex(hash);}
function isString(arg: unknown): arg is string { return typeof arg === "string";}
let te: TextEncoderfunction strToBuffer(str: string): Uint8Array { if (!te) { te = new TextEncoder(); } return te.encode(str); }
function preprocess(data: Uint8Array): Uint8Array { const bitLength = data.length * 8; const paddingLength = (N_512 + 448 - ((bitLength + 1) % N_512)) % N_512; const paddedLength = Math.ceil((bitLength + 1 + paddingLength + N_64) / 8);
const padded = new Uint8Array(paddedLength); padded.set(data); padded[data.length] = 0x80; const view = new DataView(padded.buffer); const lengthBytes = paddedLength - 8;
view.setUint32(lengthBytes, Math.floor(bitLength / MAX_UINT32_PLUS_1), false); view.setUint32(lengthBytes + 4, bitLength % MAX_UINT32_PLUS_1, false);
return padded;}
function toHex(buffer: Uint8Array): string { return [...buffer].map((b) => b.toString(16).padStart(2, "0")).join("");}
function rightRotate(value: number, shift: number): number { return (value >>> shift) | (value << (32 - shift));}
function initUint32(length: number): Uint32Array;function initUint32(array: ArrayLike<number> | ArrayBufferLike): Uint32Array;function initUint32( buffer: ArrayBufferLike, byteOffset?: number, length?: number,): Uint32Array;function initUint32( arg1: number | ArrayLike<number> | ArrayBufferLike, arg2?: number, arg3?: number,): Uint32Array { return typeof arg1 === "number" ? new Uint32Array(arg1) : "byteLength" in arg1 ? new Uint32Array(arg1, arg2, arg3) : new Uint32Array(arg1);}
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> { if (value === undefined || value === null) { throw new Error(`Expected 'value' to be defined, but received ${value}`); }}
function nonNull<T>(value: T): NonNullable<T> { assertIsDefined(value); return value;}Usage Example
console.log(sha256("Hello, World!"));// dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986fImplementation Overview
- Padding: Aligns message into 512-bit blocks
- Message Schedule: Generates a 64-word schedule
- Main Loop: Updates eight working variables
- Hash Value: Outputs the final 256-bit hash
This implementation is for learning purposes and constrained environments. For normal use, the WebCrypto API is recommended.
Practical Note
This snippet fits well when I do not want to rewrite the same operation or check around TypeScript, JavaScript, Crypto over and over. Keeping it as a small helper makes the caller easier to read because the intent stays in the foreground.
If the branches and preconditions start growing, it is usually better not to force everything into one snippet. Splitting the steps and helper responsibilities is easier to maintain.
hsb.horse