4 Commits
Author SHA1 Message Date
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
7 changed files with 260 additions and 112 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
run: wasm-pack test --node
- name: Build package
run: bash build.sh
run: make
- name: Publish to npm
run: |
Generated
+8 -1
View File
@@ -47,9 +47,10 @@ dependencies = [
[[package]]
name = "blake3-wasm-rs"
version = "0.1.1"
version = "0.1.3"
dependencies = [
"blake3",
"hex",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -127,6 +128,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"
+7 -4
View File
@@ -1,8 +1,10 @@
[package]
name = "blake3-wasm-rs"
version = "0.1.1"
version = "0.1.3"
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"]
@@ -12,8 +14,9 @@ categories = ["cryptography", "wasm"]
crate-type = ["cdylib", "rlib"]
[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"
[dev-dependencies]
wasm-bindgen-test = "0.3"
wasm-bindgen-test = "0.3"
hex = "0.4"
+85
View File
@@ -0,0 +1,85 @@
ROOT := pkg
CRATE := blake3_wasm_rs
VERSION := 0.3.0
.PHONY: all clean build bundler node web node-esm package verify
all: clean build package
clean:
rm -rf $(ROOT)
build: bundler node web node-esm
bundler:
@echo "Building bundler target..."
wasm-pack build --target bundler -d $(ROOT)/bundler
node:
@echo "Building nodejs target..."
wasm-pack build --target nodejs -d $(ROOT)/node
web:
@echo "Building web target..."
wasm-pack build --target web -d $(ROOT)/web
node-esm: node
@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: build
@cp README.md $(ROOT)/README.md
@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
@node -e "\
const pkg = { \
name: 'blake3-wasm-rs', \
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', \
}, \
exports: { \
'.': { \
node: { \
require: './node/$(CRATE).js', \
import: './node-esm/index.mjs', \
}, \
import: './bundler/$(CRATE).js', \
default: './web/$(CRATE).js', \
}, \
}, \
types: './bundler/$(CRATE).d.ts', \
files: ['node/', 'node-esm/', 'bundler/', 'web/', 'README.md'], \
keywords: ['blake3', 'wasm', 'hash', 'cryptography', 'wasm-bindgen'], \
}; \
require('fs').writeFileSync('$(ROOT)/package.json', JSON.stringify(pkg, null, 2) + '\n'); \
"
@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
+22 -7
View File
@@ -1,5 +1,10 @@
# blake3 to wasm
[![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** 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
@@ -70,15 +75,25 @@ h.finalize()
## 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"
+137 -1
View File
@@ -1,10 +1,13 @@
use wasm_bindgen::prelude::*;
/// 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()
}
/// Hash data with variable-length output (XOF mode).
/// Returns `out_len` bytes of BLAKE3 extended output.
#[wasm_bindgen]
pub fn hash_xof(data: &[u8], out_len: usize) -> Vec<u8> {
let mut out = vec![0u8; out_len];
@@ -15,50 +18,88 @@ pub fn hash_xof(data: &[u8], out_len: usize) -> Vec<u8> {
out
}
/// Compute a keyed BLAKE3 hash (MAC). Key must be exactly 32 bytes.
/// Throws if the key length is wrong.
#[wasm_bindgen]
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())
}
/// Derive a 32-byte key from a context string and key material.
/// Context should be a hardcoded, globally unique, application-specific string.
#[wasm_bindgen]
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.
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()`.
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.
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.
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 +153,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 +216,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 +279,4 @@ mod wasm_tests {
fn test_hasher_keyed_bad_key() {
assert!(Hasher::new_keyed(&[0u8; 10]).is_err());
}
}
}