mirror of
https://github.com/UneBaguette/blake3.wasm.git
synced 2026-08-07 00:00:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35dbf0f97e
|
||
|
|
08e01269bc
|
||
|
|
a0e262bf02
|
||
|
|
55db5a6575
|
@@ -1,2 +0,0 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["-C", "target-feature=+simd128"]
|
||||
Generated
+1
-1
@@ -53,7 +53,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "blake3-wasm-rs"
|
||||
version = "0.1.4"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"hex",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "blake3-wasm-rs"
|
||||
version = "0.1.4"
|
||||
version = "0.5.1"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["UneBaguette <[email protected]>"]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
ROOT := pkg
|
||||
CRATE := blake3_wasm_rs
|
||||
VERSION := 0.4.0
|
||||
VERSION := $(shell grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
|
||||
CARGO := cargo
|
||||
WASM_BINDGEN := wasm-bindgen
|
||||
WASM_OPT := wasm-opt
|
||||
WASM_OPT_FLAGS := --enable-bulk-memory --enable-nontrapping-float-to-int -O
|
||||
WASM_OPT_FLAGS := --enable-bulk-memory --enable-nontrapping-float-to-int -O3
|
||||
WASM_TARGET := wasm32-unknown-unknown
|
||||
TARGET_DIR := target/$(WASM_TARGET)/release
|
||||
|
||||
@@ -20,7 +20,7 @@ build: build-all node-esm
|
||||
|
||||
build-all:
|
||||
@echo "Building all target..."
|
||||
RUSTFLAGS='-C opt-level=s' $(CARGO) build --target $(WASM_TARGET) --release --features talc
|
||||
RUSTFLAGS='-C opt-level=3 -C target-feature=+simd128' $(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
|
||||
@@ -34,7 +34,7 @@ build-all:
|
||||
@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 README.md $(ROOT)/README.md
|
||||
@cp LICENSE $(ROOT)/LICENSE
|
||||
@sed -i 's|// @ts-nocheck|// Types|' $(ROOT)/index.d.ts
|
||||
@node -e "\
|
||||
@@ -52,7 +52,7 @@ build-all:
|
||||
import: './bundler/$(CRATE).js',\
|
||||
default: './web/$(CRATE).js'\
|
||||
}},\
|
||||
files: ['bundler/', 'web/', 'node/', 'node-esm/', '$(CRATE)_bg.wasm', 'README.md', 'LICENSE'],\
|
||||
files: ['bundler/', 'web/', 'node/', 'node-esm/', 'index.js', 'index.d.ts', '$(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');"
|
||||
|
||||
@@ -12,18 +12,11 @@
|
||||
```sh
|
||||
# lets build it!
|
||||
|
||||
# For NodeJS
|
||||
wasm-pack build --target nodejs --release
|
||||
# normal
|
||||
make build
|
||||
|
||||
# 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"]
|
||||
# clean
|
||||
make
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -171,11 +164,11 @@ 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 |
|
||||
| 32 B | 11 MB/s | 34 MB/s | 45 MB/s | 86 MB/s |
|
||||
| 1 KB | 56 MB/s | 499 MB/s | 526 MB/s | 919 MB/s |
|
||||
| 64 KB | 52 MB/s | 1,729 MB/s | 1,684 MB/s | 2,067 MB/s |
|
||||
| 1 MB | 51 MB/s | 1,550 MB/s | 4,036 MB/s | 1,848 MB/s |
|
||||
| 10 MB | 50 MB/s | 1,497 MB/s | 4,946 MB/s | 1,776 MB/s |
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
# blake3-wasm-rs
|
||||
|
||||
[](https://www.npmjs.com/package/blake3-wasm-rs)
|
||||
[](https://github.com/UneBaguette/blake3.wasm/actions/workflows/ci.yml)
|
||||
[](https://github.com/UneBaguette/blake3.wasm/blob/master/LICENSE)
|
||||
[](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
|
||||
+44
-10
@@ -8,6 +8,16 @@ use wasm_bindgen::prelude::*;
|
||||
#[global_allocator]
|
||||
static TALC: talc::wasm::WasmDynamicTalc = talc::wasm::new_wasm_dynamic_allocator();
|
||||
|
||||
const MAX_XOF_LEN: usize = 1 << 16; // 65_536
|
||||
|
||||
fn checked_xof_len(out_len: usize) -> Result<usize, JsError> {
|
||||
if out_len == 0 || out_len > MAX_XOF_LEN {
|
||||
return Err(JsError::new("out_len must be between 1 and 65536"));
|
||||
}
|
||||
|
||||
Ok(out_len)
|
||||
}
|
||||
|
||||
/// Hash data and return a 32-byte BLAKE3 digest.
|
||||
#[wasm_bindgen]
|
||||
pub fn hash(value: &[u8]) -> Vec<u8> {
|
||||
@@ -17,13 +27,14 @@ pub fn hash(value: &[u8]) -> Vec<u8> {
|
||||
/// 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> {
|
||||
pub fn hash_xof(data: &[u8], out_len: usize) -> Result<Vec<u8>, JsError> {
|
||||
let out_len = checked_xof_len(out_len)?;
|
||||
let mut out = vec![0u8; out_len];
|
||||
let mut reader = blake3::Hasher::new().update(data).finalize_xof();
|
||||
|
||||
reader.fill(&mut out);
|
||||
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Compute a keyed BLAKE3 hash (MAC). Key must be exactly 32 bytes.
|
||||
@@ -94,11 +105,12 @@ impl Hasher {
|
||||
|
||||
/// 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> {
|
||||
pub fn finalize_xof(&self, out_len: usize) -> Result<Vec<u8>, JsError> {
|
||||
let out_len = checked_xof_len(out_len)?;
|
||||
let mut out = vec![0u8; out_len];
|
||||
self.0.finalize_xof().fill(&mut out);
|
||||
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Finalize the hash and reset the hasher in one call.
|
||||
@@ -135,8 +147,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hash_xof_length() {
|
||||
assert_eq!(hash_xof(b"hello", 64).len(), 64);
|
||||
assert_eq!(hash_xof(b"hello", 16).len(), 16);
|
||||
assert_eq!(hash_xof(b"hello", 64).unwrap().len(), 64);
|
||||
assert_eq!(hash_xof(b"hello", 16).unwrap().len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -199,17 +211,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hasher_finalize_xof() {
|
||||
let oneshot = hash_xof(b"hello", 64);
|
||||
let oneshot = hash_xof(b"hello", 64).unwrap();
|
||||
let mut h = Hasher::new();
|
||||
h.update(b"hello");
|
||||
assert_eq!(h.finalize_xof(64), oneshot);
|
||||
assert_eq!(h.finalize_xof(64).unwrap(), 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);
|
||||
let xof = hash_xof(b"test data", 64).unwrap();
|
||||
assert_eq!(&xof[..32], standard.as_slice());
|
||||
}
|
||||
|
||||
@@ -291,4 +303,26 @@ mod wasm_tests {
|
||||
fn test_hasher_keyed_bad_key() {
|
||||
assert!(Hasher::new_keyed(&[0u8; 10]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_hash_xof_zero_len() {
|
||||
assert!(hash_xof(b"hello", 0).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_hash_xof_over_max() {
|
||||
assert!(hash_xof(b"hello", 65_537).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_hasher_finalize_xof_zero_len() {
|
||||
let h = Hasher::new();
|
||||
assert!(h.finalize_xof(0).is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_hasher_finalize_xof_over_max() {
|
||||
let h = Hasher::new();
|
||||
assert!(h.finalize_xof(65_537).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user