mirror of
https://github.com/UneBaguette/blake3.wasm.git
synced 2026-08-07 00:00:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70a1423cb8
|
@@ -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
@@ -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"
|
||||
|
||||
+3
-2
@@ -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]>"]
|
||||
@@ -18,4 +18,5 @@ 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
+137
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user