crypto.sha256
The SHA-256 digest of a byte string (FIPS 180-4), in pure code that also runs in the browser.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 13 tests, run in TypeScript, Python and Rust.
What it does
`sha256(utf8Encode("abc"))` is the 32 bytes `ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad` (print them with `hexEncode`). This is SHA-256 exactly as FIPS 180-4 defines it.
Bytes in and out are lists of integers 0 to 255, as everywhere in the registry (see `encoding.hex`); hash text by encoding it with `encoding.utf8` first, so all three languages hash the same bytes.
For example
sha256()→ 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85 FIPS 180-4 example: the empty messagesha256(97, 98, 99)→ 186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173 FIPS 180-4 example: "abc", one blocksha256(97, 98, 99, 100, 98, 99, 100, 101, 99, 100, 101, 102, 100, 101, 102, 103, 101, 102, 103, 104, 102, 103, 104, 105, 103, 104, 105, 106, 104, 105, 106, 107, 105, 106, 107, 108, 106, …)→ 36, 141, 106, 97, 210, 6, 56, 184, 229, 192, 38, 147, 12, 62, 96, 57, 163, 60, 228, 89, 100, 255, 33, 103, 246, 236, 237, 212, 25, 219, 6, 193 FIPS 180-4 example: the 448-bit message, whose padding needs a second block
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function sha256(bytes: readonly number[]): readonly number[]
| bytes | int[] | the message, each an integer from 0 to 255; hash text with encoding.utf8 first |
| returns | int[] | the 32-byte digest; encoding.hex prints it the way sha256sum does |
Your code names it in one line, in the file that uses it
import { sha256 } from "#fune/crypto.sha256@^1";
/** FIPS 180-4 section 4.2.2: the first 32 bits of the fractional parts of the cube roots of the first 64 primes. */
const K = new Int32Array([
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,
]);
/** FIPS 180-4 section 5.3.3: the initial hash value. */
const H0 = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
/**
* The SHA-256 digest of a byte string (FIPS 180-4).
*
* Pure code, synchronous, with no node:crypto or crypto.subtle, so the same
* function runs in the browser and on the server. Arithmetic is on signed
* 32-bit integers (`| 0`), which wrap exactly as the standard's mod 2^32
* additions require.
*/
export function sha256(bytes: readonly number[]): readonly number[] {
if (!Array.isArray(bytes) && !(bytes instanceof Uint8Array)) {
throw new TypeError("bytes must be a list of integers from 0 to 255");
}
const length = bytes.length;
for (let i = 0; i < length; i++) {
const b = bytes[i];
if (typeof b !== "number" || !Number.isInteger(b) || b < 0 || b > 255) {
throw new RangeError("bytes must be a list of integers from 0 to 255");
}
}
// Padding (section 5.1.1): a 1 bit, zeros, then the length in bits as a
// 64-bit big-endian integer, to a multiple of 64 bytes.
const blocks = Math.floor((length + 8) / 64) + 1;
const padded = new Uint8Array(blocks * 64);
for (let i = 0; i < length; i++) padded[i] = bytes[i];
padded[length] = 0x80;
const bitsHigh = Math.floor(length / 0x20000000); // length * 8 / 2^32
const bitsLow = (length * 8) >>> 0;
const end = padded.length;
padded[end - 8] = bitsHigh >>> 24;
padded[end - 7] = (bitsHigh >>> 16) & 255;
padded[end - 6] = (bitsHigh >>> 8) & 255;
padded[end - 5] = bitsHigh & 255;
padded[end - 4] = bitsLow >>> 24;
padded[end - 3] = (bitsLow >>> 16) & 255;
padded[end - 2] = (bitsLow >>> 8) & 255;
padded[end - 1] = bitsLow & 255;
const h = new Int32Array(H0);
const w = new Int32Array(64);
for (let block = 0; block < blocks; block++) {
const base = block * 64;
for (let t = 0; t < 16; t++) {
const j = base + t * 4;
w[t] = (padded[j] << 24) | (padded[j + 1] << 16) | (padded[j + 2] << 8) | padded[j + 3];
}
for (let t = 16; t < 64; t++) {
const x = w[t - 15];
const y = w[t - 2];
const s0 = ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
const s1 = ((y >>> 17) | (y << 15)) ^ ((y >>> 19) | (y << 13)) ^ (y >>> 10);
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) | 0;
}
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
for (let t = 0; t < 64; t++) {
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
const ch = (e & f) ^ (~e & g);
const t1 = (hh + S1 + ch + K[t] + w[t]) | 0;
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
const maj = (a & b) ^ (a & c) ^ (b & c);
const t2 = (S0 + maj) | 0;
hh = g;
g = f;
f = e;
e = (d + t1) | 0;
d = c;
c = b;
b = a;
a = (t1 + t2) | 0;
}
h[0] = (h[0] + a) | 0;
h[1] = (h[1] + b) | 0;
h[2] = (h[2] + c) | 0;
h[3] = (h[3] + d) | 0;
h[4] = (h[4] + e) | 0;
h[5] = (h[5] + f) | 0;
h[6] = (h[6] + g) | 0;
h[7] = (h[7] + hh) | 0;
}
const out: number[] = [];
for (let i = 0; i < 8; i++) {
out.push((h[i] >>> 24) & 255, (h[i] >>> 16) & 255, (h[i] >>> 8) & 255, h[i] & 255);
}
return out;
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add crypto.sha256
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./crypto.sha256-1.0.0-typescript.fune, or fetch it from a terminal with fune pull crypto.sha256@1.0.0:typescript.
The whole function, every language, is one file too: crypto.sha256-1.0.0.fune, 17,379 bytes, sha256 beb0733f73c235f9276b554934618e517a26461d4bf4350f5465f14bfd89c645. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before crypto.sha256
after — your function gets the result and the arguments, and returns the final result.
// fune: after crypto.sha256
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show crypto.sha256 --steps.
// fune: step crypto.sha256 after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| FIPS 180-4 example: the empty message | → | 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85 | |
| FIPS 180-4 example: "abc", one block | 97, 98, 99 | → | 186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173 |
| FIPS 180-4 example: the 448-bit message, whose padding needs a second block | 97, 98, 99, 100, 98, 99, 100, 101, 99, 100, 101, 102, 100, 101, 102, 103, 101, 102, 103, 104, 102, 103, 104, 105, 103, 104, 105, 106, 104, 105, 106, 107, 105, 106, 107, 108, 106, … | → | 36, 141, 106, 97, 210, 6, 56, 184, 229, 192, 38, 147, 12, 62, 96, 57, 163, 60, 228, 89, 100, 255, 33, 103, 246, 236, 237, 212, 25, 219, 6, 193 |
| FIPS 180-4 example: the 896-bit message | 97, 98, 99, 100, 101, 102, 103, 104, 98, 99, 100, 101, 102, 103, 104, 105, 99, 100, 101, 102, 103, 104, 105, 106, 100, 101, 102, 103, 104, 105, 106, 107, 101, 102, 103, 104, 105, … | → | 207, 91, 22, 167, 120, 175, 131, 128, 3, 108, 229, 158, 123, 4, 146, 55, 11, 36, 155, 17, 232, 240, 122, 81, 175, 172, 69, 3, 122, 254, 233, 209 |
| the pangram | 84, 104, 101, 32, 113, 117, 105, 99, 107, 32, 98, 114, 111, 119, 110, 32, 102, 111, 120, 32, 106, 117, 109, 112, 115, 32, 111, 118, 101, 114, 32, 116, 104, 101, 32, 108, 97, 122, … | → | 215, 168, 251, 179, 7, 215, 128, 148, 105, 202, 154, 188, 176, 8, 46, 79, 141, 86, 81, 228, 109, 60, 219, 118, 45, 2, 208, 191, 55, 201, 229, 146 |
| 55 bytes, the most that fits one block with its padding (hashlib as the reference) | 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97,… | → | 159, 67, 144, 248, 211, 12, 45, 217, 46, 201, 240, 149, 182, 94, 43, 154, 233, 176, 169, 37, 165, 37, 142, 36, 28, 159, 30, 145, 15, 115, 67, 24 |
| 56 bytes, one too many for one block (hashlib as the reference) | 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97,… | → | 179, 84, 57, 164, 172, 111, 9, 72, 182, 214, 249, 227, 198, 175, 15, 95, 89, 12, 226, 15, 27, 222, 112, 144, 239, 121, 112, 104, 110, 198, 115, 138 |
| 64 bytes, exactly one block of message (hashlib as the reference) | 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97,… | → | 255, 224, 84, 254, 122, 224, 203, 109, 198, 92, 58, 249, 182, 29, 82, 9, 244, 57, 133, 29, 180, 61, 11, 165, 153, 115, 55, 223, 21, 70, 104, 235 |
| 119 bytes, the most that fits two blocks (hashlib as the reference) | 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97,… | → | 49, 235, 165, 28, 49, 58, 92, 8, 34, 106, 223, 24, 212, 163, 89, 207, 223, 216, 210, 232, 22, 177, 63, 74, 249, 82, 247, 234, 101, 132, 220, 251 |
| a single zero byte is not the empty message | 0 | → | 110, 52, 11, 156, 255, 179, 122, 152, 156, 165, 68, 230, 187, 120, 10, 44, 120, 144, 29, 63, 179, 55, 56, 118, 133, 17, 163, 6, 23, 175, 160, 29 |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 256 is not a byte | 97, 256 | → | error: bytes must be a list of integers from 0 to 255 |
| a fraction is not a byte | 97.5 | → | error: bytes must be a list of integers from 0 to 255 |
| a character is not a byte: encode text with encoding.utf8 first | a | → | error: bytes must be a list of integers from 0 to 255 |
More from the author
The TypeScript implementation is plain code with no `node:crypto` and no `crypto.subtle`, so it runs unchanged in a browser, in Node and in a worker, synchronously (`crypto.subtle.digest` is asynchronous and only exists in secure contexts). The Python implementation uses the standard library's `hashlib`, and the Rust one is written out with the standard library only. All three are checked against the same vectors.
A hash is not a password hash: a single SHA-256 of a password can be guessed billions of times a second. Store passwords with `auth.password-hash`, which uses PBKDF2 with a salt and many iterations. A hash is not a MAC either: to prove a message came from someone holding a key, use `crypto.hmac-sha256` (prefixing the key and hashing is open to length extension).
The digests in the vectors are the published ones: "abc" and the 448-bit message from the NIST example values for FIPS 180-4 (SHA256.pdf), the empty string, the pangram, and messages of 55, 56, 64 and 119 bytes, which sit on either side of the points where the padding needs a second block.
Source: FIPS 180-4, Secure Hash Standard, section 6.2 (https://csrc.nist.gov/pubs/fips/180-4/upd1/final) and the NIST cryptographic standards example values (https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines/example-values).
Files
| Path | Bytes |
|---|---|
| README.md | 1,731 |
| impl/python.py | 750 |
| impl/rust.rs | 3,849 |
| impl/typescript.ts | 4,284 |
| vectors.json | 5,012 |