Skip to content

fuzdev/blake3

Repository files navigation

blake3

BLAKE3 hashing for TypeScript/JS, compiled to WASM from the blake3 Rust crate, @BLAKE3-team/BLAKE3.

Install

Two builds with the same API:

npm i @fuzdev/blake3_wasm        # SIMD — faster, 47 KB
npm i @fuzdev/blake3_wasm_small  # no SIMD — smaller, 32 KB

Use @fuzdev/blake3_wasm unless optimizing bundle size (or using Bun with its current SIMD issue). It enables blake3's WASM SIMD optimizations and is 2-3x faster at 64KB+ inputs on Deno and Node. @fuzdev/blake3_wasm_small is ~30% smaller but loses the 2-3x SIMD gains (but it's up to ~2.5x faster on Bun, which has a WASM SIMD problem). See benchmarks for details (includes a comparison to the SIMD-lacking @connor4312/blake3, blake3-wasm on npm).

Also available via npm: specifier in Deno (jsr coming soon, user requests will speed it up):

import { hash } from 'npm:@fuzdev/blake3_wasm';

Only import one — importing both loads two separate WASM modules. To swap builds without changing imports, use a bundler alias:

// vite.config.ts — use the small build everywhere
export default {
	resolve: {
		alias: { '@fuzdev/blake3_wasm': '@fuzdev/blake3_wasm_small' },
	},
};

Usage

import { Blake3Hasher, derive_key, hash, keyed_hash } from '@fuzdev/blake3_wasm';

// one-shot hash — returns 32-byte Uint8Array
const digest = hash(new TextEncoder().encode('hello'));
console.log(digest.length); // 32

// keyed hash (MAC) — key must be 32 bytes
const key = new Uint8Array(32);
const mac = keyed_hash(key, new TextEncoder().encode('hello'));

// key derivation
const derived = derive_key('my-app', new TextEncoder().encode('secret'));

// streaming hasher — same result as one-shot above
const hasher = new Blake3Hasher();
hasher.update(new TextEncoder().encode('hel'));
hasher.update(new TextEncoder().encode('lo'));
const result = hasher.finalize(); // same bytes as `digest`
hasher.free(); // release WASM memory, or use `using` — see API below

Browser

In browsers, call init() once before using any hash functions. It's a no-op if already initialized, so calling it unconditionally is safe.

import { hash, init } from '@fuzdev/blake3_wasm';

await init();

const digest = hash(new TextEncoder().encode('hello'));

Node and Deno initialize automatically — init() is only needed in browsers. The browser entry uses new URL('./blake3_wasm_bg.wasm', import.meta.url) internally, which Vite and webpack handle automatically. Other bundlers may need a plugin for WASM assets.

Vite: Exclude from dependency pre-bundling so the .wasm file resolves correctly:

// vite.config.ts
export default {
	optimizeDeps: {
		exclude: ['@fuzdev/blake3_wasm'],
	},
};

Without this, Vite pre-bundles the JS into .vite/deps/ but doesn't copy the WASM binary alongside it, causing a 404 at runtime. This is a common Vite issue with WASM packages.

For synchronous initialization in Web Workers, use init_sync:

import { init_sync } from '@fuzdev/blake3_wasm';

const wasm = await fetch('/blake3_wasm_bg.wasm').then((r) => r.arrayBuffer());
init_sync({ module: wasm });

Hashing a ReadableStream

import { derive_key_stream, hash_stream, keyed_hash_stream } from '@fuzdev/blake3_wasm';

// file: File from <input>, drop event, or fetch Response
const digest = await hash_stream(file.stream());
const mac = await keyed_hash_stream(key, file.stream());
const derived = await derive_key_stream('my-app', file.stream());

API

All functions return 32-byte (256-bit) digests. BLAKE3 supports variable-length output (XOF) but this API does not currently expose it — 32 bytes covers most use cases and XOF may be added in the future (open a discussion if you'd like it).

One-shot functions

hash(data: Uint8Array): Uint8Array

Returns 32-byte digest.

keyed_hash(key: Uint8Array, data: Uint8Array): Uint8Array

Keyed hash (MAC). Throws if key is not exactly 32 bytes.

derive_key(context: string, key_material: Uint8Array): Uint8Array

Key derivation. Returns 32 bytes.

Streaming hasher

const hasher = new Blake3Hasher();

Create a hasher for plain hashing.

Blake3Hasher.new_keyed(key: Uint8Array): Blake3Hasher

Keyed hasher (MAC). Throws if key is not exactly 32 bytes.

Blake3Hasher.new_derive_key(context: string): Blake3Hasher

Hasher in key derivation mode.

hasher.update(data: Uint8Array): void

Feed data incrementally.

hasher.finalize(): Uint8Array

Returns 32-byte digest. Non-destructive — can call repeatedly.

hasher.finalize_and_reset(): Uint8Array

Finalize and reset in one call.

hasher.reset(): void

Reset to initial state. Preserves keyed/derive mode.

hasher.free(): void

Release WASM memory. Supports using for automatic cleanup:

using hasher = new Blake3Hasher();
hasher.update(data);
return hasher.finalize();
// hasher.free() called automatically

Stream functions

hash_stream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array>

Hash a stream. Returns 32-byte digest.

keyed_hash_stream(key: Uint8Array, stream: ReadableStream<Uint8Array>): Promise<Uint8Array>

Keyed hash a stream. Key must be exactly 32 bytes. Returns 32-byte digest.

derive_key_stream(context: string, stream: ReadableStream<Uint8Array>): Promise<Uint8Array>

Derive-key hash a stream. Returns 32-byte digest.

Stream functions handle hasher cleanup automatically, even if the stream throws mid-read.

Architecture

See docs/architecture.md for crate structure, build pipeline, and tooling details.

Building from source

Requires Rust, wasm-pack, Deno, and the WASM target (rustup target add wasm32-unknown-unknown).

deno task build:wasm           # build all WASM targets
deno task build:wasm:deno      # build for Deno (+ deno compile patch)
deno task build:wasm:web       # build for web/Node (+ npm package)

The Deno build patches wasm-bindgen's fetch() to Deno.readFileSync() for deno compile compatibility. Include the pkg/deno/ directory via --include so the WASM binary is embedded:

deno compile --allow-read --include=path/to/pkg/deno my_script.ts

For WASI component builds and Wasmtime benchmarks, also install cargo-component and add the WASI target (rustup target add wasm32-wasip1).

Development

cargo check --workspace        # fast typecheck
deno task check                # full check (typecheck + test:rust + clippy + fmt)
deno task compare              # build + correctness: all builds vs test vectors
deno task test                 # all correctness tests (requires built artifacts)
deno task validate             # everything: check + build + test + validate:*
deno task bench                # performance: cross-runtime benchmarks

Benchmarks

One snapshot of cross-runtime results (Deno, Node, Bun, Wasmtime) is in benches/results/report.md.

deno task bench                # full pipeline: build + all runtimes + report
deno task bench:deno           # Deno only
deno task bench:report         # regenerate report from existing results

Publish

Uses changesets for version management. Prerequisite: npm i -g @changesets/cli

changeset                      # add a changeset
deno task publish              # dry-run
deno task publish --wetrun     # version + check + build + validate + publish

License

MIT