9 Commits
Author SHA1 Message Date
breakingbread 4397fa0beb ci: pin wasm-pack and wasm-bindgen-cli versions 2026-04-25 16:21:51 +02:00
breakingbread 9441c3a7cd fix: add wasm-bindgen-test-runner 2026-04-25 16:20:22 +02:00
breakingbread ed3a4d5fbc ci: use prebuilt binaries for wasm-pack, wasm-bindgen, wasm-opt 2026-04-25 16:13:43 +02:00
breakingbread 1f9d01b073 fix: pin wasm-bindgen to 0.2.118, update CI 2026-04-25 16:10:48 +02:00
breakingbread 84b2e75967 feat: camelCase JS exports, node-esm, index entry point 2026-04-25 15:56:36 +02:00
breakingbread 70a1423cb8 feat: add new_derive_key, finalize_and_reset, doc comments, official test vectors, Makefile 2026-04-24 02:15:51 +02:00
breakingbread 8480a9136b chore: migrate version in cargo.toml and bump version in package.json build.sh 2026-04-23 14:30:00 +02:00
Paul MillerandGitHub 69eab8091d readme: add awasm-noble entry (#1)
The `@awasm/noble/wasm_threads.js` target works without any additional setup in node.js. It can be used as-is.

Same goes for browsers. Web workers are managed automatically, without any input from user.

This is very different from most threaded libraries which require user to set up their own threading infra.
2026-04-22 23:07:10 +02:00
breakingbread dda079279f docs: add shields badges 2026-04-22 19:08:40 +02:00
11 changed files with 654 additions and 153 deletions
+11 -3
View File
@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [master]
branches: [ master ]
pull_request:
branches: [master]
branches: [ master ]
env:
CARGO_TERM_COLOR: always
@@ -16,7 +16,15 @@ jobs:
- uses: actions/checkout@v6
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
run: |
curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo mv wasm-pack-*/wasm-pack /usr/local/bin/
- name: Install wasm-bindgen-cli
run: |
curl -L https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.118/wasm-bindgen-0.2.118-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo mv wasm-bindgen-*/wasm-bindgen /usr/local/bin/
sudo mv wasm-bindgen-*/wasm-bindgen-test-runner /usr/local/bin/
- name: Rust tests
run: cargo test
+16 -3
View File
@@ -2,7 +2,7 @@ name: Publish
on:
push:
tags: ["v*"]
tags: [ "v*" ]
env:
CARGO_TERM_COLOR: always
@@ -16,7 +16,20 @@ jobs:
- uses: actions/checkout@v6
- name: Install wasm-pack
run: cargo install wasm-pack
run: |
curl -L https://github.com/wasm-bindgen/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo mv wasm-pack-*/wasm-pack /usr/local/bin/
- name: Install wasm-bindgen-cli
run: |
curl -L https://github.com/wasm-bindgen/wasm-bindgen/releases/download/0.2.118/wasm-bindgen-0.2.118-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo mv wasm-bindgen-*/wasm-bindgen /usr/local/bin/
sudo mv wasm-bindgen-*/wasm-bindgen-test-runner /usr/local/bin/
- name: Install wasm-opt
run: |
curl -L https://github.com/WebAssembly/binaryen/releases/latest/download/binaryen-version_129-x86_64-linux.tar.gz | tar xz
sudo mv binaryen-*/bin/wasm-opt /usr/local/bin/
- name: Rust tests
run: cargo test
@@ -25,7 +38,7 @@ jobs:
run: wasm-pack test --node
- name: Build package
run: bash build.sh
run: make
- name: Publish to npm
run: |
Generated
+40 -1
View File
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "allocator-api2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880"
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -47,9 +53,11 @@ dependencies = [
[[package]]
name = "blake3-wasm-rs"
version = "0.1.1"
version = "0.1.4"
dependencies = [
"blake3",
"hex",
"talc",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -127,6 +135,12 @@ dependencies = [
"slab",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "itoa"
version = "1.0.18"
@@ -157,6 +171,15 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -243,6 +266,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
@@ -309,6 +338,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "talc"
version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3081e3f3ad542dc4cd0ae720f752598f8880fd364dc2d031132e3509f673aa23"
dependencies = [
"allocator-api2",
"lock_api",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
+13 -4
View File
@@ -1,8 +1,10 @@
[package]
name = "blake3-wasm-rs"
version = "0.1.1"
version = "0.1.4"
edition = "2024"
license = "MIT"
authors = ["UneBaguette <[email protected]>"]
publish = false
repository = "https://github.com/UneBaguette/blake3.wasm"
description = "Simple blake3 to wasm project"
keywords = ["blake3", "wasm", "hash", "cryptography", "wasm-bindgen"]
@@ -11,9 +13,16 @@ categories = ["cryptography", "wasm"]
[lib]
crate-type = ["cdylib", "rlib"]
[features]
talc = ["dep:talc"]
[dependencies]
blake3 = { version = "1.8.4", features = ["wasm32_simd"] }
wasm-bindgen = "0.2.118"
blake3 = { version = "1.8", features = ["wasm32_simd"] }
wasm-bindgen = "=0.2.118"
[dev-dependencies]
wasm-bindgen-test = "0.3"
wasm-bindgen-test = "0.3"
hex = "0.4"
[target.'cfg(target_family = "wasm")'.dependencies]
talc = { version = "5", optional = true }
+90
View File
@@ -0,0 +1,90 @@
ROOT := pkg
CRATE := blake3_wasm_rs
VERSION := 0.4.0
CARGO := cargo
WASM_BINDGEN := wasm-bindgen
WASM_OPT := wasm-opt
WASM_OPT_FLAGS := --enable-bulk-memory --enable-nontrapping-float-to-int -O
WASM_TARGET := wasm32-unknown-unknown
TARGET_DIR := target/$(WASM_TARGET)/release
.PHONY: all clean build build-all node-esm package verify
all: clean build package
clean:
rm -rf $(ROOT)
build: build-all node-esm
build-all:
@echo "Building all target..."
RUSTFLAGS='-C opt-level=s' $(CARGO) build --target $(WASM_TARGET) --release --features talc
@mkdir -p $(ROOT)/bundler $(ROOT)/web $(ROOT)/node
$(WASM_BINDGEN) --target bundler --out-dir $(ROOT)/bundler $(TARGET_DIR)/$(CRATE).wasm
$(WASM_BINDGEN) --target web --out-dir $(ROOT)/web $(TARGET_DIR)/$(CRATE).wasm
$(WASM_BINDGEN) --target nodejs --out-dir $(ROOT)/node $(TARGET_DIR)/$(CRATE).wasm
@mv $(ROOT)/bundler/$(CRATE)_bg.wasm $(ROOT)/$(CRATE)_bg.wasm
@rm -f $(ROOT)/web/$(CRATE)_bg.wasm $(ROOT)/node/$(CRATE)_bg.wasm
$(WASM_OPT) $(WASM_OPT_FLAGS) $(ROOT)/$(CRATE)_bg.wasm -o $(ROOT)/$(CRATE)_bg.wasm
@node -e "['bundler','web','node'].forEach(d=>{const f='$(ROOT)/'+d+'/$(CRATE).js';require('fs').writeFileSync(f,require('fs').readFileSync(f,'utf8').replace(/$(CRATE)_bg\.wasm/g,'../$(CRATE)_bg.wasm'))})"
@rm -f $(ROOT)/bundler/package.json $(ROOT)/web/package.json
@rm -f $(ROOT)/bundler/.gitignore $(ROOT)/web/.gitignore $(ROOT)/node/.gitignore
@echo '{"type":"commonjs"}' > $(ROOT)/node/package.json
@cp scripts/tpl/index.js.template $(ROOT)/index.js
@cp scripts/tpl/index.d.ts $(ROOT)/index.d.ts
@cp scripts/tpl/README.md $(ROOT)/README.md
@cp LICENSE $(ROOT)/LICENSE
@sed -i 's|// @ts-nocheck|// Types|' $(ROOT)/index.d.ts
@node -e "\
const pkg = {\
name: 'blake3-wasm-rs',\
type: 'module',\
version: '$(VERSION)',\
description: 'BLAKE3 hashing via Rust/WASM - works in Node.js (CJS + ESM), browsers, and bundlers',\
license: 'MIT',\
repository: { type: 'git', url: 'https://github.com/UneBaguette/blake3.wasm' },\
main: 'index.js',\
types: 'index.d.ts',\
exports: { '.': {\
node: { require: './node/$(CRATE).js', import: './node-esm/index.mjs' },\
import: './bundler/$(CRATE).js',\
default: './web/$(CRATE).js'\
}},\
files: ['bundler/', 'web/', 'node/', 'node-esm/', '$(CRATE)_bg.wasm', 'README.md', 'LICENSE'],\
keywords: ['blake3', 'wasm', 'hash', 'cryptography', 'wasm-bindgen']\
};\
require('fs').writeFileSync('./$(ROOT)/package.json', JSON.stringify(pkg, null, 2) + '\n');"
node-esm:
@echo "Generating node-esm wrapper..."
@mkdir -p $(ROOT)/node-esm
@node -e "\
const fs = require('fs'); \
const dts = fs.readFileSync('$(ROOT)/node/$(CRATE).d.ts', 'utf8'); \
const fns = [...dts.matchAll(/export function (\w+)/g)].map(m => m[1]); \
const cls = [...dts.matchAll(/export class (\w+)/g)].map(m => m[1]); \
const lines = [ \
\"import { createRequire } from 'module';\", \
\"const require = createRequire(import.meta.url);\", \
\"const mod = require('../node/$(CRATE).js');\", \
\"export default mod;\", \
...fns.map(n => 'export const ' + n + ' = mod.' + n + ';'), \
...cls.map(n => 'export const ' + n + ' = mod.' + n + ';'), \
]; \
fs.writeFileSync('$(ROOT)/node-esm/index.mjs', lines.join('\n') + '\n'); \
"
package:
@echo ""
@echo "Done! Package ready in $(ROOT)/"
@echo " node (CJS): $(ROOT)/node/"
@echo " node (ESM): $(ROOT)/node-esm/"
@echo " bundler: $(ROOT)/bundler/"
@echo " web: $(ROOT)/web/"
verify: package
cd $(ROOT) && npm pack --dry-run
+137 -40
View File
@@ -1,6 +1,11 @@
# blake3 to wasm
# blake3-wasm-rs
**blake3.wasm** is a WebAssembly port of the BLAKE3 cryptographic hash function written in **Rust**. It enables fast and secure hashing right inside **browsers** and **Node.js**.
[![npm version](https://img.shields.io/npm/v/blake3-wasm-rs)](https://www.npmjs.com/package/blake3-wasm-rs)
[![CI](https://github.com/UneBaguette/blake3.wasm/actions/workflows/ci.yml/badge.svg)](https://github.com/UneBaguette/blake3.wasm/actions/workflows/ci.yml)
[![license](https://img.shields.io/npm/l/blake3-wasm-rs)](https://github.com/UneBaguette/blake3.wasm/blob/master/LICENSE)
[![npm downloads](https://img.shields.io/npm/dm/blake3-wasm-rs)](https://www.npmjs.com/package/blake3-wasm-rs)
**blake3-wasm-rs** is a WebAssembly port of the BLAKE3 cryptographic hash function written in **Rust**. It enables fast and secure hashing right inside **browsers** and **Node.js**.
## build
@@ -31,54 +36,146 @@ const key = new Uint8Array(32).fill(1);
// One-shot hashing
blake3.hash(data);
blake3.hashXof(data, 64); // variable output length
// MAC and key derivation
blake3.keyedHash(data, key);
blake3.hashXof(data, 64); // variable output length
blake3.keyedHash(data, key); // key must be exactly 32 bytes
blake3.deriveKey('my context', key);
// Conctruct for streaming
const hasher = new blake3.Hasher();
hasher.update(data.slice(0, 5));
hasher.update(data.slice(5));
hasher.finalize();
```
#### OR
```js
import { hash, hashXof, keyedHash, deriveKey, Hasher } from 'blake3-wasm-rs'
const data = new TextEncoder().encode('hello world')
const key = new Uint8Array(32).fill(1)
// One-shot hashing
hash(data)
hashXof(data, 64) // variable output length
// MAC and key derivation
keyedHash(data, key)
deriveKey('my context', key)
// Conctruct for Streaming
{
using h = new blake3.Hasher();
h.update(data.slice(0, 5));
h.update(data.slice(5));
h.finalize();
h.finalizeXof(64);
h.reset();
}
// Streaming
const h = new Hasher()
h.update(data.slice(0, 5))
h.update(data.slice(5))
h.finalize()
// Keyed (MAC mode)
{
using mac = blake3.Hasher.newKeyed(key);
mac.update(data);
mac.finalize();
}
// Streaming
// Key derivation mode
{
using kdf = blake3.Hasher.newDeriveKey('my app v1 :: subkey');
kdf.update(key);
kdf.finalize();
}
// Batch hashing without re-allocating
{
using h = new blake3.Hasher();
h.update(chunk1);
const first = h.finalizeAndReset();
h.update(chunk2);
const second = h.finalizeAndReset();
}
```
#### Named imports
```ts
import { hash, hashXof, keyedHash, deriveKey, Hasher } from 'blake3-wasm-rs';
const data = new TextEncoder().encode('hello world');
const key = new Uint8Array(32).fill(1);
// One-shot hashing
hash(data);
hashXof(data, 64); // variable output length
keyedHash(data, key); // key must be exactly 32 bytes
deriveKey('my context', key);
// Streaming
{
using h = new Hasher();
h.update(data.slice(0, 5));
h.update(data.slice(5));
h.finalize();
h.finalizeXof(64);
h.reset();
}
// Keyed (MAC mode)
{
using mac = Hasher.newKeyed(key);
mac.update(data);
mac.finalize();
}
// Key derivation mode
{
using kdf = Hasher.newDeriveKey('my app v1 :: subkey');
kdf.update(key);
kdf.finalize();
}
// Batch hashing without re-allocating
{
using h = new Hasher();
h.update(chunk1);
const first = h.finalizeAndReset();
h.update(chunk2);
const second = h.finalizeAndReset();
}
```
## API
### Functions
| Function | Returns | Description |
|-----------------------------------|--------------|-------------------------------------------------|
| `hash(data)` | `Uint8Array` | One-shot 32-byte BLAKE3 digest |
| `hashXof(data, outLen)` | `Uint8Array` | Variable-length digest (XOF mode) |
| `keyedHash(data, key)` | `Uint8Array` | Keyed hash / MAC (key must be exactly 32 bytes) |
| `deriveKey(context, keyMaterial)` | `Uint8Array` | Derive a 32-byte subkey |
### Hasher class
| Method | Returns | Description |
|--------------------------------|--------------|-----------------------------------------------------------|
| `new Hasher()` | `Hasher` | Streaming hasher, unkeyed |
| `Hasher.newKeyed(key)` | `Hasher` | Streaming hasher, MAC mode (key must be exactly 32 bytes) |
| `Hasher.newDeriveKey(context)` | `Hasher` | Streaming hasher, KDF mode |
| `.update(data)` | `void` | Feed data, can be called multiple times |
| `.finalize()` | `Uint8Array` | 32-byte digest, non-destructive |
| `.finalizeXof(outLen)` | `Uint8Array` | Variable-length digest, non-destructive |
| `.finalizeAndReset()` | `Uint8Array` | Finalize then reset (useful for batch hashing) |
| `.reset()` | `void` | Reset to initial state, preserves mode |
| `.free()` | `void` | Release WASM memory manually (prefer `using` instead) |
> **Memory management:** In all modern browsers (and wasm-bindgen ≥ 0.2.91), WASM memory is freed automatically via the TC39 weak references proposal when the JS object goes out of scope.
>
> In practice, you often don't need to think about this. For deterministic cleanup or environments without weak reference support (older browsers, some Node.js setups), use `using` (TypeScript 5.2+ / ES2026) or call `.free()` manually.
>
> Never call `.free()` on a `using`-managed instance otherwise it will double-free.
## Benchmarks
Tested on **Apple M4**, Node.js v24.
| Size | @noble/hashes | awasm-noble | awasm-noble (threads) | blake3-wasm |
|-------|---------------|-------------|-----------------------|-------------|
| 32 B | 28 MB/s | 105 MB/s | 94 MB/s | 129 MB/s |
| 1 KB | 105 MB/s | 843 MB/s | 819 MB/s | 568 MB/s |
| 64 KB | 102 MB/s | 1,898 MB/s | 1,855 MB/s | 2,004 MB/s |
| 1 MB | 101 MB/s | 1,943 MB/s | 4,711 MB/s | 1,893 MB/s |
| 10 MB | 101 MB/s | 1,911 MB/s | 6,456 MB/s | 2,185 MB/s |
Tested on **Ryzen 7 5800X**, Node.js v24.
| Size | @noble/hashes | awasm-noble | blake3-wasm |
|-------|---------------|-------------|-------------|
| 32 B | 11 MB/s | 34 MB/s | 81 MB/s |
| 1 KB | 60 MB/s | 360 MB/s | 781 MB/s |
| 64 KB | 49 MB/s | 1,518 MB/s | 2,004 MB/s |
| 1 MB | 51 MB/s | 1,671 MB/s | 1,893 MB/s |
| 10 MB | 51 MB/s | 1,572 MB/s | 1,812 MB/s |
| Size | @noble/hashes | awasm-noble | awasm-noble (threads) | blake3-wasm |
|-------|---------------|-------------|-----------------------|-------------|
| 32 B | 11 MB/s | 34 MB/s | 45 MB/s | 70 MB/s |
| 1 KB | 56 MB/s | 499 MB/s | 526 MB/s | 885 MB/s |
| 64 KB | 52 MB/s | 1,729 MB/s | 1,684 MB/s | 2,037 MB/s |
| 1 MB | 51 MB/s | 1,550 MB/s | 4,036 MB/s | 1,829 MB/s |
| 10 MB | 50 MB/s | 1,497 MB/s | 4,946 MB/s | 1,877 MB/s |
## Security
-98
View File
@@ -1,98 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="pkg"
rm -rf "$ROOT"
echo "Building bundler target..."
wasm-pack build --target bundler -d "$ROOT/bundler"
echo "Building nodejs target..."
wasm-pack build --target nodejs -d "$ROOT/node"
echo "Building web target..."
wasm-pack build --target web -d "$ROOT/web"
# Generate node-esm wrapper from the nodejs build exports
echo "Generating node-esm wrapper..."
mkdir -p "$ROOT/node-esm"
cp README.md "$ROOT/README.md"
# Extract export names from the nodejs .js file
EXPORTS=$(grep -oP '(?<=module\.exports\.)\w+' "$ROOT/node/blake3_wasm_rs.js" | sort -u || true)
# Fallback: parse from .d.ts if module.exports pattern not found
if [ -z "$EXPORTS" ]; then
EXPORTS=$(grep -oP '(?<=export function )\w+' "$ROOT/node/blake3_wasm_rs.d.ts" | sort -u || true)
fi
# Also check for exported classes
CLASSES=$(grep -oP '(?<=export class )\w+' "$ROOT/node/blake3_wasm_rs.d.ts" | sort -u || true)
{
echo "import { createRequire } from 'module';"
echo "const require = createRequire(import.meta.url);"
echo "const mod = require('../node/blake3_wasm_rs.js');"
echo "export default mod;"
for name in $EXPORTS; do
echo "export const $name = mod.$name;"
done
for name in $CLASSES; do
echo "export const $name = mod.$name;"
done
} > "$ROOT/node-esm/index.mjs"
# Clean up wasm-pack generated package.json in each subfolder
rm -f "$ROOT/bundler/package.json" "$ROOT/node/package.json" "$ROOT/web/package.json"
rm -f "$ROOT/bundler/.gitignore" "$ROOT/node/.gitignore" "$ROOT/web/.gitignore"
# Generate root package.json
cat > "$ROOT/package.json" << 'EOF'
{
"name": "blake3-wasm-rs",
"version": "0.2.1",
"description": "BLAKE3 hashing via Rust/WASM - works in Node.js (CJS + ESM), browsers, and bundlers",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/UneBaguette/blake3.wasm"
},
"exports": {
".": {
"node": {
"require": "./node/blake3_wasm_rs.js",
"import": "./node-esm/index.mjs"
},
"import": "./bundler/blake3_wasm_rs.js",
"default": "./web/blake3_wasm_rs.js"
}
},
"types": "./bundler/blake3_wasm_rs.d.ts",
"files": [
"node/",
"node-esm/",
"bundler/",
"web/",
"README.md"
],
"keywords": [
"blake3",
"wasm",
"hash",
"cryptography",
"wasm-bindgen"
]
}
EOF
echo ""
echo "Done! Package ready in $ROOT/"
echo " node (CJS): $ROOT/node/"
echo " node (ESM): $ROOT/node-esm/"
echo " bundler: $ROOT/bundler/"
echo " web: $ROOT/web/"
echo ""
echo "Verify with: cd $ROOT && npm pack --dry-run"
+188
View File
@@ -0,0 +1,188 @@
# blake3-wasm-rs
[![npm version](https://img.shields.io/npm/v/blake3-wasm-rs)](https://www.npmjs.com/package/blake3-wasm-rs)
[![CI](https://github.com/UneBaguette/blake3.wasm/actions/workflows/ci.yml/badge.svg)](https://github.com/UneBaguette/blake3.wasm/actions/workflows/ci.yml)
[![license](https://img.shields.io/npm/l/blake3-wasm-rs)](https://github.com/UneBaguette/blake3.wasm/blob/master/LICENSE)
[![npm downloads](https://img.shields.io/npm/dm/blake3-wasm-rs)](https://www.npmjs.com/package/blake3-wasm-rs)
**blake3-wasm-rs** is a WebAssembly port of the BLAKE3 cryptographic hash function written in **Rust**. It enables fast and secure hashing right inside **browsers** and **Node.js**.
## build
```sh
# lets build it!
# For NodeJS
wasm-pack build --target nodejs --release
# For the web!
wasm-pack build --target web --release
```
SIMD is enabled via `wasm32_simd` feature in `Cargo.toml` and requires:
```toml
# .cargo/config.toml
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+simd128"]
```
## Usage
```ts
import * as blake3 from 'blake3-wasm-rs';
const data = new TextEncoder().encode('hello world');
const key = new Uint8Array(32).fill(1);
// One-shot hashing
blake3.hash(data);
blake3.hashXof(data, 64); // variable output length
blake3.keyedHash(data, key); // key must be exactly 32 bytes
blake3.deriveKey('my context', key);
// Conctruct for Streaming
{
using h = new blake3.Hasher();
h.update(data.slice(0, 5));
h.update(data.slice(5));
h.finalize();
h.finalizeXof(64);
h.reset();
}
// Streaming
// Keyed (MAC mode)
{
using mac = blake3.Hasher.newKeyed(key);
mac.update(data);
mac.finalize();
}
// Streaming
// Key derivation mode
{
using kdf = blake3.Hasher.newDeriveKey('my app v1 :: subkey');
kdf.update(key);
kdf.finalize();
}
// Batch hashing without re-allocating
{
using h = new blake3.Hasher();
h.update(chunk1);
const first = h.finalizeAndReset();
h.update(chunk2);
const second = h.finalizeAndReset();
}
```
#### Named imports
```ts
import { hash, hashXof, keyedHash, deriveKey, Hasher } from 'blake3-wasm-rs';
const data = new TextEncoder().encode('hello world');
const key = new Uint8Array(32).fill(1);
// One-shot hashing
hash(data);
hashXof(data, 64); // variable output length
keyedHash(data, key); // key must be exactly 32 bytes
deriveKey('my context', key);
// Streaming
{
using h = new Hasher();
h.update(data.slice(0, 5));
h.update(data.slice(5));
h.finalize();
h.finalizeXof(64);
h.reset();
}
// Keyed (MAC mode)
{
using mac = Hasher.newKeyed(key);
mac.update(data);
mac.finalize();
}
// Key derivation mode
{
using kdf = Hasher.newDeriveKey('my app v1 :: subkey');
kdf.update(key);
kdf.finalize();
}
// Batch hashing without re-allocating
{
using h = new Hasher();
h.update(chunk1);
const first = h.finalizeAndReset();
h.update(chunk2);
const second = h.finalizeAndReset();
}
```
## API
### Functions
| Function | Returns | Description |
|-----------------------------------|--------------|-------------------------------------------------|
| `hash(data)` | `Uint8Array` | One-shot 32-byte BLAKE3 digest |
| `hashXof(data, outLen)` | `Uint8Array` | Variable-length digest (XOF mode) |
| `keyedHash(data, key)` | `Uint8Array` | Keyed hash / MAC (key must be exactly 32 bytes) |
| `deriveKey(context, keyMaterial)` | `Uint8Array` | Derive a 32-byte subkey |
### Hasher class
| Method | Returns | Description |
|--------------------------------|--------------|-----------------------------------------------------------|
| `new Hasher()` | `Hasher` | Streaming hasher, unkeyed |
| `Hasher.newKeyed(key)` | `Hasher` | Streaming hasher, MAC mode (key must be exactly 32 bytes) |
| `Hasher.newDeriveKey(context)` | `Hasher` | Streaming hasher, KDF mode |
| `.update(data)` | `void` | Feed data, can be called multiple times |
| `.finalize()` | `Uint8Array` | 32-byte digest, non-destructive |
| `.finalizeXof(outLen)` | `Uint8Array` | Variable-length digest, non-destructive |
| `.finalizeAndReset()` | `Uint8Array` | Finalize then reset (useful for batch hashing) |
| `.reset()` | `void` | Reset to initial state, preserves mode |
| `.free()` | `void` | Release WASM memory manually (prefer `using` instead) |
> **Memory management:** In all modern browsers (and wasm-bindgen ≥ 0.2.91), WASM memory is freed automatically via the TC39 weak references proposal when the JS object goes out of scope.
>
> In practice, you often don't need to think about this. For deterministic cleanup or environments without weak reference support (older browsers, some Node.js setups), use `using` (TypeScript 5.2+ / ES2026) or call `.free()` manually.
>
> Never call `.free()` on a `using`-managed instance otherwise it will double-free.
## Benchmarks
Tested on **Apple M4**, Node.js v24.
| Size | @noble/hashes | awasm-noble | awasm-noble (threads) | blake3-wasm |
|-------|---------------|-------------|-----------------------|-------------|
| 32 B | 28 MB/s | 105 MB/s | 94 MB/s | 129 MB/s |
| 1 KB | 105 MB/s | 843 MB/s | 819 MB/s | 568 MB/s |
| 64 KB | 102 MB/s | 1,898 MB/s | 1,855 MB/s | 2,004 MB/s |
| 1 MB | 101 MB/s | 1,943 MB/s | 4,711 MB/s | 1,893 MB/s |
| 10 MB | 101 MB/s | 1,911 MB/s | 6,456 MB/s | 2,185 MB/s |
Tested on **Ryzen 7 5800X**, Node.js v24.
| Size | @noble/hashes | awasm-noble | awasm-noble (threads) | blake3-wasm |
|-------|---------------|-------------|-----------------------|-------------|
| 32 B | 11 MB/s | 34 MB/s | 45 MB/s | 70 MB/s |
| 1 KB | 56 MB/s | 499 MB/s | 526 MB/s | 885 MB/s |
| 64 KB | 52 MB/s | 1,729 MB/s | 1,684 MB/s | 2,037 MB/s |
| 1 MB | 51 MB/s | 1,550 MB/s | 4,036 MB/s | 1,829 MB/s |
| 10 MB | 50 MB/s | 1,497 MB/s | 4,946 MB/s | 1,877 MB/s |
## Security
The underlying `blake3` Rust crate targets algorithmic constant time. However, the JavaScript boundary (via napi-rs or WASM) introduces non-determinism from the V8 runtime that is outside our control. For absolute security, use the `blake3` Rust crate directly in a Rust program.
## See also
- [@noble/hashes](https://github.com/paulmillr/noble-hashes) | pure JS implementation
- [awasm-noble](https://github.com/paulmillr/awasm-noble) | auditable WASM implementation
- [blake3-napi](https://github.com/UneBaguette/blake3-napi) | native Node.js addon, faster for large inputs
+3
View File
@@ -0,0 +1,3 @@
// @ts-nocheck
export { Hasher, deriveKey, hash, hashXof, keyedHash } from './bundler/blake3_wasm_rs';
+4
View File
@@ -0,0 +1,4 @@
// blake3-wasm-rs. Auto-generated entry point
// Do not edit directly!
export { Hasher, deriveKey, hash, hashXof, keyedHash } from './bundler/blake3_wasm_rs.js';
+152 -4
View File
@@ -1,11 +1,22 @@
use wasm_bindgen::prelude::*;
#[cfg(all(
not(target_feature = "atomics"),
target_family = "wasm",
feature = "talc"
))]
#[global_allocator]
static TALC: talc::wasm::WasmDynamicTalc = talc::wasm::new_wasm_dynamic_allocator();
/// Hash data and return a 32-byte BLAKE3 digest.
#[wasm_bindgen]
pub fn hash(value: &[u8]) -> Vec<u8> {
blake3::hash(value).as_bytes().to_vec()
}
#[wasm_bindgen]
/// Hash data with variable-length output (XOF mode).
/// Returns `out_len` bytes of BLAKE3 extended output.
#[wasm_bindgen(js_name = "hashXof")]
pub fn hash_xof(data: &[u8], out_len: usize) -> Vec<u8> {
let mut out = vec![0u8; out_len];
let mut reader = blake3::Hasher::new().update(data).finalize_xof();
@@ -15,50 +26,92 @@ pub fn hash_xof(data: &[u8], out_len: usize) -> Vec<u8> {
out
}
#[wasm_bindgen]
/// Compute a keyed BLAKE3 hash (MAC). Key must be exactly 32 bytes.
/// Throws if the key length is wrong.
#[wasm_bindgen(js_name = "keyedHash")]
pub fn keyed_hash(data: &[u8], key: &[u8]) -> Result<Vec<u8>, JsError> {
let key: &[u8; 32] = key
.try_into()
.map_err(|_| JsError::new("key must be exactly 32 bytes"))?;
Ok(blake3::keyed_hash(key, data).as_bytes().to_vec())
}
#[wasm_bindgen]
/// Derive a 32-byte key from a context string and key material.
/// Context should be a hardcoded, globally unique, application-specific string.
#[wasm_bindgen(js_name = "deriveKey")]
pub fn derive_key(context: &str, key_material: &[u8]) -> Vec<u8> {
blake3::derive_key(context, key_material).to_vec()
}
/// Incremental BLAKE3 hasher for streaming data.
///
/// ```js
/// const hasher = new Hasher();
/// hasher.update(chunk1);
/// hasher.update(chunk2);
/// const digest = hasher.finalize(); // 32 bytes
/// ```
#[wasm_bindgen]
pub struct Hasher(blake3::Hasher);
#[wasm_bindgen]
impl Hasher {
/// Create a new hasher for unkeyed hashing.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Hasher(blake3::Hasher::new())
}
/// Create a keyed hasher (MAC mode). Key must be exactly 32 bytes.
/// Throws if the key length is wrong.
#[wasm_bindgen(js_name = "newKeyed")]
pub fn new_keyed(key: &[u8]) -> Result<Hasher, JsError> {
let key: &[u8; 32] = key
.try_into()
.map_err(|_| JsError::new("key must be exactly 32 bytes"))?;
Ok(Hasher(blake3::Hasher::new_keyed(key)))
}
/// Create a hasher in derive-key mode.
/// Context should be a hardcoded, globally unique, application-specific string.
/// Feed key material via `update()`, then call `finalize()`.
#[wasm_bindgen(js_name = "newDeriveKey")]
pub fn new_derive_key(context: &str) -> Hasher {
Hasher(blake3::Hasher::new_derive_key(context))
}
/// Feed data into the hasher. Can be called multiple times for streaming.
pub fn update(&mut self, data: &[u8]) {
self.0.update(data);
}
/// Return the 32-byte hash digest. Non-destructive. Can be called multiple times.
pub fn finalize(&self) -> Vec<u8> {
self.0.finalize().as_bytes().to_vec()
}
/// Return `out_len` bytes of extended output (XOF mode). Non-destructive.
#[wasm_bindgen(js_name = "finalizeXof")]
pub fn finalize_xof(&self, out_len: usize) -> Vec<u8> {
let mut out = vec![0u8; out_len];
self.0.finalize_xof().fill(&mut out);
out
}
/// Finalize the hash and reset the hasher in one call.
/// Useful for hashing multiple inputs sequentially without creating new instances.
#[wasm_bindgen(js_name = "finalizeAndReset")]
pub fn finalize_and_reset(&mut self) -> Vec<u8> {
let out = self.0.finalize().as_bytes().to_vec();
self.0.reset();
out
}
/// Reset the hasher to its initial state. Preserves the mode (keyed/derive-key).
pub fn reset(&mut self) {
self.0.reset();
}
@@ -112,6 +165,54 @@ mod tests {
assert_eq!(h.finalize(), oneshot);
}
#[test]
fn test_hasher_derive_key() {
let oneshot = derive_key("my-ctx", b"material");
let mut h = Hasher::new_derive_key("my-ctx");
h.update(b"material");
assert_eq!(h.finalize(), oneshot);
}
#[test]
fn test_hasher_finalize_and_reset() {
let mut h = Hasher::new();
h.update(b"hello");
let first = h.finalize_and_reset();
assert_eq!(first, hash(b"hello"));
// After reset, hashing different data should produce different output
h.update(b"world");
let second = h.finalize();
assert_eq!(second, hash(b"world"));
assert_ne!(first, second);
}
#[test]
fn test_keyed_hash_matches_streaming() {
let key = [42u8; 32];
let oneshot = keyed_hash(b"hello world", &key).unwrap();
let mut h = Hasher::new_keyed(&key).unwrap();
h.update(b"hello ");
h.update(b"world");
assert_eq!(h.finalize(), oneshot);
}
#[test]
fn test_hasher_finalize_xof() {
let oneshot = hash_xof(b"hello", 64);
let mut h = Hasher::new();
h.update(b"hello");
assert_eq!(h.finalize_xof(64), oneshot);
}
#[test]
fn test_hasher_finalize_xof_prefix_matches_hash() {
// First 32 bytes of XOF output should equal the standard hash
let standard = hash(b"test data");
let xof = hash_xof(b"test data", 64);
assert_eq!(&xof[..32], standard.as_slice());
}
#[test]
fn test_hasher_reset() {
let mut h = Hasher::new();
@@ -127,6 +228,53 @@ mod tests {
let h = Hasher::new_keyed(&key).unwrap();
assert_ne!(h.finalize(), hash(b"").as_slice());
}
// BLAKE3 official test vectors (from BLAKE3 spec)
// Reference: https://github.com/BLAKE3-team/BLAKE3/blob/master/test_vectors/test_vectors.json
// Input: 0x00..0xfa repeating pattern, first N bytes
fn test_input(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
#[test]
fn test_vector_empty() {
let out = hash(&[]);
let expected =
hex::decode("af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262")
.unwrap();
assert_eq!(out, expected);
}
#[test]
fn test_vector_1_byte() {
let input = test_input(1);
let out = hash(&input);
let expected =
hex::decode("2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213")
.unwrap();
assert_eq!(out, expected);
}
#[test]
fn test_vector_1025_bytes() {
let input = test_input(1025);
let out = hash(&input);
let expected =
hex::decode("d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444")
.unwrap();
assert_eq!(out, expected);
}
#[test]
fn test_vector_keyed_empty() {
let key = b"whats the Elvish word for friend";
let out = keyed_hash(&[], key).unwrap();
let expected =
hex::decode("92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26")
.unwrap();
assert_eq!(out, expected);
}
}
#[cfg(all(target_arch = "wasm32", test))]
@@ -143,4 +291,4 @@ mod wasm_tests {
fn test_hasher_keyed_bad_key() {
assert!(Hasher::new_keyed(&[0u8; 10]).is_err());
}
}
}