- Add QRCode component using qrcode.react (SVG-based, H error correction) - Add QRScanner component using html5-qrcode for camera-based scanning - Update Receive page with real QR code and payment request amount - Update Send page with QR scan button to auto-fill recipient address - Support Synor payment URI format: synor:address?amount=X - Add Dockerfile for web wallet with nginx serving - Add web-wallet service to docker-compose.testnet.yml (port 17300) - Fix TypeScript WebCrypto ArrayBuffer type issues QR features: - SVG rendering for crisp display at any size - Error correction level H (30% recovery) for printed codes - Camera permission handling with user-friendly error states - Auto-fill amount when scanning payment requests
237 lines
6.9 KiB
TypeScript
237 lines
6.9 KiB
TypeScript
/**
|
|
* WASM Crypto Module Loader
|
|
*
|
|
* This module provides lazy-loading of the synor-crypto-wasm module
|
|
* for client-side Dilithium3 post-quantum signatures.
|
|
*
|
|
* ## Why Lazy Loading?
|
|
*
|
|
* The WASM module is ~2MB, so we only load it when needed:
|
|
* - User opts into client-side signing
|
|
* - Hardware wallet mode (all keys local)
|
|
* - Offline signing scenarios
|
|
*
|
|
* ## Usage
|
|
*
|
|
* ```typescript
|
|
* import { loadWasmCrypto, isWasmLoaded } from './wasm-crypto';
|
|
*
|
|
* // Load the WASM module (only loads once)
|
|
* const wasm = await loadWasmCrypto();
|
|
*
|
|
* // Create Dilithium keypair from seed
|
|
* const dilithiumKey = wasm.DilithiumSigningKey.fromSeed(seed);
|
|
* const signature = dilithiumKey.sign(message);
|
|
* ```
|
|
*/
|
|
|
|
// Re-export types from the generated WASM module
|
|
// These are auto-generated by wasm-bindgen and match synor-crypto-wasm exports
|
|
|
|
import type {
|
|
DilithiumSigningKey as WasmDilithiumSigningKeyType,
|
|
Keypair as WasmKeypairType,
|
|
Mnemonic as WasmMnemonicType,
|
|
} from '../wasm/synor_crypto';
|
|
|
|
// Export the types for external use
|
|
export type WasmDilithiumSigningKey = WasmDilithiumSigningKeyType;
|
|
export type WasmKeypair = WasmKeypairType;
|
|
export type WasmMnemonic = WasmMnemonicType;
|
|
|
|
// Interface for the loaded WASM module
|
|
export interface SynorCryptoWasm {
|
|
// Classes
|
|
Keypair: typeof WasmKeypairType;
|
|
DilithiumSigningKey: typeof WasmDilithiumSigningKeyType;
|
|
Mnemonic: typeof WasmMnemonicType;
|
|
|
|
// Standalone functions
|
|
verifyWithPublicKey(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
|
dilithiumVerify(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): boolean;
|
|
dilithiumSizes(): { publicKey: number; secretKey: number; signature: number };
|
|
sha3_256(data: Uint8Array): Uint8Array;
|
|
blake3(data: Uint8Array): Uint8Array;
|
|
deriveKey(inputKey: Uint8Array, salt: Uint8Array, info: Uint8Array, outputLen: number): Uint8Array;
|
|
validateAddress(address: string): boolean;
|
|
decodeAddress(address: string): unknown;
|
|
}
|
|
|
|
// Singleton instance of the loaded WASM module
|
|
let wasmModule: SynorCryptoWasm | null = null;
|
|
let loadingPromise: Promise<SynorCryptoWasm> | null = null;
|
|
|
|
/**
|
|
* Check if the WASM module is already loaded.
|
|
*/
|
|
export function isWasmLoaded(): boolean {
|
|
return wasmModule !== null;
|
|
}
|
|
|
|
/**
|
|
* Load the WASM crypto module.
|
|
*
|
|
* This function is idempotent - calling it multiple times will return
|
|
* the same module instance.
|
|
*
|
|
* @returns Promise resolving to the WASM module
|
|
* @throws Error if WASM loading fails
|
|
*/
|
|
export async function loadWasmCrypto(): Promise<SynorCryptoWasm> {
|
|
// Return cached module if already loaded
|
|
if (wasmModule) {
|
|
return wasmModule;
|
|
}
|
|
|
|
// Return existing loading promise to avoid parallel loads
|
|
if (loadingPromise) {
|
|
return loadingPromise;
|
|
}
|
|
|
|
// Start loading the WASM module
|
|
loadingPromise = (async () => {
|
|
try {
|
|
// Dynamic import for code splitting - Vite handles WASM bundling
|
|
const wasm = await import('../wasm/synor_crypto');
|
|
|
|
// For wasm-bindgen bundler target, the default export is the init function
|
|
// Some bundlers auto-init, others require explicit init call
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const initFn = (wasm as any).default;
|
|
if (typeof initFn === 'function') {
|
|
await initFn();
|
|
}
|
|
|
|
// Cast to our interface type
|
|
wasmModule = {
|
|
Keypair: wasm.Keypair,
|
|
DilithiumSigningKey: wasm.DilithiumSigningKey,
|
|
Mnemonic: wasm.Mnemonic,
|
|
verifyWithPublicKey: wasm.verifyWithPublicKey,
|
|
dilithiumVerify: wasm.dilithiumVerify,
|
|
dilithiumSizes: wasm.dilithiumSizes as () => { publicKey: number; secretKey: number; signature: number },
|
|
sha3_256: wasm.sha3_256,
|
|
blake3: wasm.blake3,
|
|
deriveKey: wasm.deriveKey,
|
|
validateAddress: wasm.validateAddress,
|
|
decodeAddress: wasm.decodeAddress,
|
|
};
|
|
|
|
return wasmModule;
|
|
} catch (error) {
|
|
loadingPromise = null; // Reset so it can be retried
|
|
throw new Error(
|
|
`Failed to load WASM crypto module: ${error instanceof Error ? error.message : 'Unknown error'}. ` +
|
|
'Make sure the WASM module is built and copied to apps/web/src/wasm/'
|
|
);
|
|
}
|
|
})();
|
|
|
|
return loadingPromise;
|
|
}
|
|
|
|
/**
|
|
* Unload the WASM module to free memory.
|
|
* This is useful for single-page apps that want to reclaim memory
|
|
* after signing operations are complete.
|
|
*/
|
|
export function unloadWasmCrypto(): void {
|
|
wasmModule = null;
|
|
loadingPromise = null;
|
|
}
|
|
|
|
// ==================== High-Level WASM Crypto Functions ====================
|
|
|
|
/**
|
|
* Create a Dilithium3 keypair from a 32-byte seed.
|
|
*
|
|
* The seed should be derived from the same mnemonic as the Ed25519 key
|
|
* to maintain key correlation for the hybrid signature scheme.
|
|
*
|
|
* @param seed - 32-byte seed (typically from BIP-39 mnemonic)
|
|
* @returns Dilithium signing key
|
|
*/
|
|
export async function createDilithiumKeyFromSeed(
|
|
seed: Uint8Array
|
|
): Promise<WasmDilithiumSigningKey> {
|
|
const wasm = await loadWasmCrypto();
|
|
return wasm.DilithiumSigningKey.fromSeed(seed);
|
|
}
|
|
|
|
/**
|
|
* Sign a message with Dilithium3.
|
|
*
|
|
* @param message - Message to sign
|
|
* @param seed - 32-byte seed for key derivation
|
|
* @returns 3293-byte Dilithium signature
|
|
*/
|
|
export async function signWithDilithium(
|
|
message: Uint8Array,
|
|
seed: Uint8Array
|
|
): Promise<Uint8Array> {
|
|
const key = await createDilithiumKeyFromSeed(seed);
|
|
try {
|
|
return key.sign(message);
|
|
} finally {
|
|
key.free(); // Clean up WASM memory
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a Dilithium3 signature.
|
|
*
|
|
* @param message - Original message
|
|
* @param signature - Dilithium signature (3293 bytes)
|
|
* @param publicKey - Dilithium public key (1952 bytes)
|
|
* @returns true if signature is valid
|
|
*/
|
|
export async function verifyDilithiumSignature(
|
|
message: Uint8Array,
|
|
signature: Uint8Array,
|
|
publicKey: Uint8Array
|
|
): Promise<boolean> {
|
|
const wasm = await loadWasmCrypto();
|
|
return wasm.dilithiumVerify(signature, message, publicKey);
|
|
}
|
|
|
|
/**
|
|
* Get Dilithium3 key and signature sizes.
|
|
*/
|
|
export async function getDilithiumSizes(): Promise<{
|
|
publicKey: number;
|
|
secretKey: number;
|
|
signature: number;
|
|
}> {
|
|
const wasm = await loadWasmCrypto();
|
|
return wasm.dilithiumSizes();
|
|
}
|
|
|
|
// ==================== Ed25519 WASM Functions ====================
|
|
|
|
/**
|
|
* Create an Ed25519 keypair from seed using WASM.
|
|
*
|
|
* This is an alternative to @noble/ed25519 that keeps all crypto in WASM.
|
|
* Useful for consistency or when noble packages aren't available.
|
|
*/
|
|
export async function createEd25519KeyFromSeed(
|
|
seed: Uint8Array
|
|
): Promise<WasmKeypair> {
|
|
const wasm = await loadWasmCrypto();
|
|
return wasm.Keypair.fromSeed(seed);
|
|
}
|
|
|
|
/**
|
|
* Sign a message with Ed25519 using WASM.
|
|
*/
|
|
export async function signWithEd25519Wasm(
|
|
message: Uint8Array,
|
|
seed: Uint8Array
|
|
): Promise<Uint8Array> {
|
|
const key = await createEd25519KeyFromSeed(seed);
|
|
try {
|
|
return key.sign(message);
|
|
} finally {
|
|
key.free();
|
|
}
|
|
}
|