crypto.hmac-sha256
HMAC-SHA256 of a message under a secret key (RFC 2104, RFC 4231), 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
`hmacSha256(key, message)` is the 32-byte HMAC-SHA256 tag of `message` under `key`, as RFC 2104 defines HMAC and RFC 4231 pins it for SHA-256. It is what signs an HS256 JWT (`auth.jwt`), a webhook payload or a signed cookie.
Bytes in and out are lists of integers 0 to 255, as everywhere in the registry (see `encoding.hex`). A text secret or message goes through `encoding.utf8` first.
For example
hmacSha256(11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 72, 105, 32, 84, 104, 101, 114, 101)→ 176, 52, 76, 97, 216, 219, 56, 83, 92, 168, 175, 206, 175, 11, 241, 43, 136, 29, 194, 0, 201, 131, 61, 167, 38, 233, 55, 108, 46, 50, 207, 247 RFC 4231 test case 1: a 20-byte keyhmacSha256(74, 101, 102, 101, 119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111, 114, 32, 110, 111, 116, 104, 105, 110, 103, 63)→ 91, 220, 193, 70, 191, 96, 117, 78, 106, 4, 36, 38, 8, 149, 117, 199, 90, 0, 63, 8, 157, 39, 57, 131, 157, 236, 88, 185, 100, 236, 56, 67 RFC 4231 test case 2: a key shorter than the outputhmacSha256(170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,…)→ 119, 62, 169, 30, 54, 128, 14, 70, 133, 77, 184, 235, 208, 145, 129, 167, 41, 89, 9, 139, 62, 248, 193, 34, 217, 99, 85, 20, 206, 213, 101, 254 RFC 4231 test case 3: 50 bytes of 0xdd under 20 bytes of 0xaa
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 hmacSha256(key: readonly number[], message: readonly number[]): readonly number[]
| key | int[] | the secret, any length; one longer than 64 bytes is hashed first, as RFC 2104 says |
| message | int[] | the bytes to authenticate |
| returns | int[] | the 32-byte tag; compare tags with crypto.constant-time-equal, never == |
Your code names it in one line, in the file that uses it
import { hmacSha256 } from "#fune/crypto.hmac-sha256@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { sha256 } from "./crypto_sha256.ts"; ← from crypto.sha256 ^1.0.0 · built alongside by fune
const BLOCK = 64;
function checkBytes(value: readonly number[], name: string): void {
if (!Array.isArray(value) && !(value instanceof Uint8Array)) {
throw new TypeError(`${name} must be a list of integers from 0 to 255`);
}
for (let i = 0; i < value.length; i++) {
const b = value[i];
if (typeof b !== "number" || !Number.isInteger(b) || b < 0 || b > 255) {
throw new RangeError(`${name} must be a list of integers from 0 to 255`);
}
}
}
/**
* HMAC-SHA256 (RFC 2104): H((K ^ opad) || H((K ^ ipad) || message)), with a
* key longer than the 64-byte block hashed down to 32 bytes first.
*/
export function hmacSha256(key: readonly number[], message: readonly number[]): readonly number[] {
checkBytes(key, "key");
checkBytes(message, "message");
const k = key.length > BLOCK ? sha256(key) : key;
const inner: number[] = new Array(BLOCK + message.length);
const outer: number[] = new Array(BLOCK + 32);
for (let i = 0; i < BLOCK; i++) {
const b = i < k.length ? k[i] : 0;
inner[i] = b ^ 0x36;
outer[i] = b ^ 0x5c;
}
for (let i = 0; i < message.length; i++) inner[BLOCK + i] = message[i];
const innerHash = sha256(inner);
for (let i = 0; i < 32; i++) outer[BLOCK + i] = innerHash[i];
return sha256(outer);
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, 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.hmac-sha256
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./crypto.hmac-sha256-1.0.0-typescript.fune, or fetch it from a terminal with fune pull crypto.hmac-sha256@1.0.0:typescript.
The whole function, every language, is one file too: crypto.hmac-sha256-1.0.0.fune, 14,277 bytes, sha256 aefbaef231a3cc8016bbff5c5226b4169f41ea671335aac1093c35e6d5bbf81e. 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.hmac-sha256
after — your function gets the result and the arguments, and returns the final result.
// fune: after crypto.hmac-sha256
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace crypto.sha256 in crypto.hmac-sha256
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.hmac-sha256 --steps.
// fune: step crypto.hmac-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 | |
|---|---|---|---|
| RFC 4231 test case 1: a 20-byte key | 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 72, 105, 32, 84, 104, 101, 114, 101 | → | 176, 52, 76, 97, 216, 219, 56, 83, 92, 168, 175, 206, 175, 11, 241, 43, 136, 29, 194, 0, 201, 131, 61, 167, 38, 233, 55, 108, 46, 50, 207, 247 |
| RFC 4231 test case 2: a key shorter than the output | 74, 101, 102, 101, 119, 104, 97, 116, 32, 100, 111, 32, 121, 97, 32, 119, 97, 110, 116, 32, 102, 111, 114, 32, 110, 111, 116, 104, 105, 110, 103, 63 | → | 91, 220, 193, 70, 191, 96, 117, 78, 106, 4, 36, 38, 8, 149, 117, 199, 90, 0, 63, 8, 157, 39, 57, 131, 157, 236, 88, 185, 100, 236, 56, 67 |
| RFC 4231 test case 3: 50 bytes of 0xdd under 20 bytes of 0xaa | 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221,… | → | 119, 62, 169, 30, 54, 128, 14, 70, 133, 77, 184, 235, 208, 145, 129, 167, 41, 89, 9, 139, 62, 248, 193, 34, 217, 99, 85, 20, 206, 213, 101, 254 |
| RFC 4231 test case 4: a 25-byte counting key | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205… | → | 130, 85, 138, 56, 154, 68, 60, 14, 164, 204, 129, 152, 153, 242, 8, 58, 133, 240, 250, 163, 229, 120, 248, 7, 122, 46, 63, 244, 103, 41, 102, 91 |
| RFC 4231 test case 5: the full tag, whose first 16 bytes the RFC publishes (a3b61674...) | 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 84, 101, 115, 116, 32, 87, 105, 116, 104, 32, 84, 114, 117, 110, 99, 97, 116, 105, 111, 110 | → | 163, 182, 22, 116, 115, 16, 14, 224, 110, 12, 121, 108, 41, 85, 85, 43, 250, 111, 124, 10, 106, 138, 239, 139, 147, 248, 96, 170, 176, 205, 32, 197 |
| RFC 4231 test case 6: a 131-byte key is hashed first | 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,… | → | 96, 228, 49, 89, 30, 224, 182, 127, 13, 138, 38, 170, 203, 245, 183, 127, 142, 11, 198, 33, 55, 40, 197, 20, 5, 70, 4, 15, 14, 227, 127, 84 |
| RFC 4231 test case 7: a long key and a message longer than a block | 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,… | → | 155, 9, 255, 167, 27, 148, 47, 203, 39, 99, 95, 188, 213, 176, 233, 68, 191, 220, 99, 100, 79, 7, 19, 147, 138, 127, 81, 83, 92, 58, 53, 226 |
| the empty key and the empty message | , | → | 182, 19, 103, 154, 8, 20, 217, 236, 119, 47, 149, 215, 120, 195, 95, 197, 255, 22, 151, 196, 147, 113, 86, 83, 198, 199, 18, 20, 66, 146, 197, 173 |
| a key of exactly one block (64 bytes) is used as is, not hashed (Python hmac as the reference) | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 4… | → | 106, 181, 65, 180, 134, 157, 202, 113, 196, 202, 17, 216, 187, 27, 2, 83, 59, 120, 154, 85, 117, 131, 22, 20, 41, 41, 44, 116, 4, 188, 33, 246 |
| a key one byte over the block is hashed (Python hmac as the reference) | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 4… | → | 223, 191, 254, 228, 103, 27, 173, 0, 237, 93, 30, 25, 153, 213, 94, 211, 176, 204, 119, 74, 195, 87, 249, 235, 246, 73, 193, 97, 36, 20, 252, 236 |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a key byte above 255 | 1, 2, 256, 97 | → | error: key must be a list of integers from 0 to 255 |
| a fractional message byte | 1, 2, 3, 97.5 | → | error: message must be a list of integers from 0 to 255 |
| a message given as text rather than bytes | 1, 2, 3, abc | → | error: message must be a list of integers from 0 to 255 |
More from the author
The construction is `H((K xor opad) || H((K xor ipad) || message))` with the key zero-padded to SHA-256's 64-byte block, or first hashed to 32 bytes when it is longer than 64. That last rule is the one a naive implementation misses, and RFC 4231 test cases 6 and 7 (a 131-byte key) are among the vectors. The empty key is allowed, as the RFC allows it, but a real key should be at least 32 random bytes (RFC 2104 section 3; RFC 7518 requires it for HS256).
**Compare tags with `crypto.constant-time-equal`.** Comparing with `==` stops at the first differing byte, and the time that takes tells an attacker how many leading bytes of a forged tag were right.
TypeScript and Rust build on `crypto.sha256` and so run anywhere, the browser included, with no `node:crypto` or `crypto.subtle`; Python uses the standard library's `hmac`. Test case 5 of RFC 4231 publishes only the first 16 bytes of its tag; its vector holds the full 32, computed with Python's `hmac` module as the reference, and begins with the RFC's 16.
Source: RFC 2104, HMAC: Keyed-Hashing for Message Authentication (https://www.rfc-editor.org/rfc/rfc2104); RFC 4231, Identifiers and Test Vectors for HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512, section 4 (https://www.rfc-editor.org/rfc/rfc4231).
Files
| Path | Bytes |
|---|---|
| README.md | 1,691 |
| impl/python.py | 842 |
| impl/rust.rs | 1,771 |
| impl/typescript.ts | 1,319 |
| vectors.json | 6,795 |