feat: add new_derive_key, finalize_and_reset, doc comments, official test vectors, Makefile

This commit is contained in:
2026-04-24 02:15:51 +02:00
parent 8480a9136b
commit 70a1423cb8
6 changed files with 234 additions and 103 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.2"
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"
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "blake3-wasm-rs"
version = "0.1.2"
version = "0.1.3"
edition = "2024"
license = "MIT"
authors = ["UneBaguette <[email protected]>"]
@@ -19,3 +19,4 @@ wasm-bindgen = "0.2"
[dev-dependencies]
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
-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.2",
"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"
+136
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))]