runtime:hashing

Digests, checksums, MACs and password hashing. crypto.subtle is the WebCrypto standard; this is the rest — the algorithms it has no name for, hashing that runs incrementally, encoded output, and passwords.

Capability: none

Hashing reads nothing and reaches nothing, so every function here works under nothing granted. The exception is password.hash(), which draws a random salt and so needs Entropy; password.verify() needs nothing. Status: Available.

Import

JavaScript
import { hash, Hasher, hashStream, hmac, timingSafeEqual, password, totp } from "runtime:hashing";

// Or the default aggregate:
import hashing from "runtime:hashing";

Exports

ExportTypeDescription
hashfunctionhash(algorithm, data, encoding?) — the digest, in one call.
Hasherclassnew Hasher(algorithm) — a hash across many chunks.
hashStreamasync functionhashStream(algorithm, stream, encoding?).
hmacfunctionhmac(algorithm, key, data, encoding?) — RFC 2104, synchronous.
timingSafeEqualfunctiontimingSafeEqual(a, b) — constant-time comparison.
passwordobjecthash(), verify(), needsRehash().
totpobjectgenerateSecret(), uri(), generate(), verify(), backupCodes().

data, key and the comparands are a string (hashed as UTF-8), an ArrayBuffer, or a view. encoding is "hex", "base64" or "base64url" for a string; omit it for a Uint8Array.

Algorithms

AlgorithmOutputNotes
sha1 sha256 sha384 sha51220 / 32 / 48 / 64 BAlso in crypto.subtle.
sha3-224 sha3-256 sha3-384 sha3-51228 / 32 / 48 / 64 B
blake332 BFast. The usual choice for large content.
md516 BInterop only — S3 ETags, CRAM-MD5.
ripemd16020 B
xxhash64 xxhash38 BNot cryptographic. Cache keys, ETags, sharding.
crc32 crc32c4 BNot cryptographic. Checksums, framing.

Names are case-insensitive, and WebCrypto's spellings work: "SHA-256" and "sha256" are the same algorithm. hmac refuses the two non-cryptographic rows.

JavaScript
hash("sha256", "hello", "hex");   // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
hash("blake3", "hello", "base64url");
hash("xxhash3", buffer);          // Uint8Array

Hasher

JavaScript
const h = new Hasher("sha256");
for await (const chunk of file.stream()) h.update(chunk);
h.digest("hex");
MemberTypeDescription
algorithmstringRead-only.
update(data)thisChains.
digest(encoding?)Uint8Array | stringEnds the hasher.

digest() releases the host state; calling either method again throws.

JavaScript
// The same, in one line.
await hashStream("sha256", request.body, "hex");

timingSafeEqual

For anything an attacker can submit repeatedly. === on hex strings leaks how much of the prefix was right, one request at a time.

JavaScript
const expected = hmac("sha256", secret, body, "hex");
if (!timingSafeEqual(request.headers.get("x-signature"), expected)) {
  return new Response("bad signature", { status: 401 });
}

Lengths are compared first, in ordinary time — a digest's length is fixed by its algorithm and public already.

password

Argon2id by default; bcrypt and scrypt for hashes that already exist.

JavaScript
const stored = await password.hash(input);       // "$argon2id$v=19$m=19456,t=2,p=1$…"
await password.verify(input, stored);
OptionApplies toDefault
algorithmall"argon2id" · "argon2i" "argon2d" "bcrypt" "scrypt"
memoryCostargon219456 KiB
timeCostargon22 passes
parallelismargon2, scrypt1
costbcrypt, scrypt12 rounds log₂ / 17 N log₂
blockSizescrypt8
saltall16 random bytes

Defaults follow the OWASP Password Storage Cheat Sheet.

MethodTypeCapability
hash(input, options?)Promise<string>Entropy
verify(input, stored)Promise<boolean>—
needsRehash(stored, options?)boolean—

The stored string carries the algorithm, parameters and salt, and verification reads them from it — so raising the cost never invalidates existing hashes.

JavaScript
if (await password.verify(input, user.hash)) {
  if (password.needsRehash(user.hash)) user.hash = await password.hash(input);
}
Two things to know

These are slow on the thread that calls them — that is the mechanism. Put a queue in front of a login endpoint. And bcrypt refuses a password past 71 bytes rather than truncating it; verification still truncates, since a stored hash may have been written by an implementation that did.

totp

Time-based one-time passwords (RFC 6238) for two-factor sign-in (D164).

FunctionDescriptionCapability
totp.generateSecret()A base32 secret of 20 random bytes.Entropy
totp.uri({ secret, issuer, account, algorithm?, digits?, period? })The otpauth://totp/issuer:account?… URI for a QR code. issuer and account are required and may not contain :.—
totp.generate(secret, { algorithm?, digits?, period?, time? })The code for time (a Date or milliseconds; default now).—
totp.verify(code, secret, { window?, after?, algorithm?, digits?, period?, time? })The time step code matched, or null.—
totp.backupCodes(count?)count (default 10, at most 100) recovery codes, xxxxx-xxxxx, from Crockford's base32 alphabet.Entropy

secret is a base32 string (any case; spaces and = ignored) or bytes, at least 16 bytes. The defaults are SHA-1, 6 digits and 30 seconds; algorithm may be "SHA256" or "SHA512", digits 7 or 8. Google Authenticator ignores any other setting.

verify checks window steps either side of time (default 1, at most 10). A step at or before after is refused: store the step verify returns and pass it as after next time, so each code is accepted once (RFC 6238 §5.2). A missing, malformed or wrong code is null; verify does not throw for one.

These are TypeErrors: a secret under 16 bytes or not base32, an unknown option, digits other than 6, 7 or 8, an unknown algorithm, a period that is not a positive whole number, a window out of range, an after that is not a whole number, and a URI without issuer or account.

Last updated on
Edit this page