● Add Rust WASM crypto backend with provider pattern for runtime switching

Phase 1: keccak256 and blake2b compiled from Rust via wasm-pack (23KB).
  Provider pattern enables switching between JS and WASM backends at runtime
  while keeping all existing JS crypto code intact. Equivalence tests confirm
  byte-for-byte matching across all inputs. Benchmarks: keccak256 ~4.6x faster,
  blake2b ~15x faster with WASM backend.
This commit is contained in:
Matt Hess
2026-01-31 20:50:04 +00:00
parent 4bbcb8afe1
commit 2486d1f00f
10 changed files with 605 additions and 1 deletions
+29
View File
@@ -0,0 +1,29 @@
/**
* JavaScript Crypto Backend
*
* Wraps existing pure-JS implementations behind the unified backend interface.
* All existing code remains untouched — this is just a thin adapter.
*
* @module crypto/backend-js
*/
import { keccak256 as jsKeccak } from '../keccak.js';
import { blake2b as jsBlake2b } from '../blake2b.js';
export class JsCryptoBackend {
constructor() {
this.name = 'js';
}
async init() {
// No initialization needed for JS backend
}
keccak256(data) {
return jsKeccak(data);
}
blake2b(data, outLen, key) {
return jsBlake2b(data, outLen, key);
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* WASM Crypto Backend
*
* Loads Rust-compiled WASM module and wraps it behind the unified backend interface.
* Falls back gracefully if WASM cannot be loaded.
*
* @module crypto/backend-wasm
*/
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
let wasmExports = null;
/**
* Load and instantiate the WASM module from disk
*/
async function loadWasm() {
if (wasmExports) return wasmExports;
const wasmPath = join(__dirname, 'wasm', 'salvium_crypto_bg.wasm');
const wasmBytes = await readFile(wasmPath);
// Import the JS glue to get the import object and init function
const glue = await import('./wasm/salvium_crypto.js');
// Use initSync with the raw WASM bytes (works in Bun/Node, no fetch needed)
glue.initSync({ module: wasmBytes });
wasmExports = glue;
return wasmExports;
}
export class WasmCryptoBackend {
constructor() {
this.name = 'wasm';
this.wasm = null;
}
async init() {
this.wasm = await loadWasm();
}
keccak256(data) {
if (!this.wasm) throw new Error('WASM backend not initialized. Call init() first.');
return this.wasm.keccak256(data);
}
blake2b(data, outLen, key) {
if (!this.wasm) throw new Error('WASM backend not initialized. Call init() first.');
if (key) {
return this.wasm.blake2b_keyed(data, outLen, key);
}
return this.wasm.blake2b_hash(data, outLen);
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Crypto Module — Public API
*
* Exports the provider (for backend switching) and both backends
* for direct access when needed.
*
* @module crypto
*/
// Provider (default usage — delegates to active backend)
export {
setCryptoBackend,
getCryptoBackend,
getCurrentBackendType,
keccak256,
blake2b,
} from './provider.js';
// Backends (for direct access / testing)
export { JsCryptoBackend } from './backend-js.js';
+70
View File
@@ -0,0 +1,70 @@
/**
* Crypto Provider — Switchable Backend
*
* Enables runtime switching between JS and WASM crypto implementations.
* Default: JS (no async init needed). Switch to WASM for performance.
*
* Usage:
* import { setCryptoBackend, keccak256, blake2b } from './crypto/provider.js';
* await setCryptoBackend('wasm'); // Switch to WASM
* const hash = keccak256(data); // Uses WASM backend
* await setCryptoBackend('js'); // Switch back to JS
*
* @module crypto/provider
*/
import { JsCryptoBackend } from './backend-js.js';
let currentBackend = null;
let backendType = 'js';
/**
* Set the active crypto backend
* @param {'js'|'wasm'} type - Backend type
*/
export async function setCryptoBackend(type) {
if (type === 'js') {
currentBackend = new JsCryptoBackend();
await currentBackend.init();
} else if (type === 'wasm') {
// Dynamic import to avoid loading WASM unless requested
const { WasmCryptoBackend } = await import('./backend-wasm.js');
currentBackend = new WasmCryptoBackend();
await currentBackend.init();
} else {
throw new Error(`Unknown crypto backend: ${type}. Use 'js' or 'wasm'.`);
}
backendType = type;
}
/**
* Get the current crypto backend instance
* @returns {Object} Backend with keccak256, blake2b, etc.
*/
export function getCryptoBackend() {
if (!currentBackend) {
// Lazy-init JS backend (sync, no await needed)
currentBackend = new JsCryptoBackend();
}
return currentBackend;
}
/**
* Get the name of the current backend
* @returns {'js'|'wasm'}
*/
export function getCurrentBackendType() {
return backendType;
}
// =============================================================================
// Delegating functions — use active backend transparently
// =============================================================================
export function keccak256(data) {
return getCryptoBackend().keccak256(data);
}
export function blake2b(data, outLen, key) {
return getCryptoBackend().blake2b(data, outLen, key);
}