Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/src/encoding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createHash } from 'crypto';
import { createHash } from './hash';

// BN254 scalar field prime
// r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
Expand Down
121 changes: 121 additions & 0 deletions sdk/src/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Browser-safe hash functions.
*
* Provides SHA-256 and other common hashes that work across environments:
* - Node.js (native crypto module)
* - Browsers (SubtleCrypto)
*
* NOTE: For production use with ZK circuits, you should use a dedicated
* Poseidon hash implementation compatible with your proving system.
*/

import { detectEnv, RuntimeEnv } from './random';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/**
* A hash function that takes arbitrary bytes and returns a fixed-size digest.
*/
export interface HashFunction {
/**
* Compute the hash of the input data.
*/
update(data: Buffer): this;

/**
* Finalize and return the digest.
*/
digest(): Buffer;
}

// ---------------------------------------------------------------------------
// Hash implementations
// ---------------------------------------------------------------------------

/**
* Node.js SHA-256 implementation.
*/
export class NodeSha256 implements HashFunction {
private readonly hash: any;

constructor() {
const { createHash } = require('crypto');
this.hash = createHash('sha256');
}

update(data: Buffer): this {
this.hash.update(data);
return this;
}

digest(): Buffer {
return this.hash.digest();
}
}

/**
* Web Crypto SHA-256 implementation.
* Note: This is async - you must await the digest promise.
*/
export class WebCryptoSha256 {
private chunks: Buffer[] = [];

update(data: Buffer): this {
this.chunks.push(data);
return this;
}

/**
* WARNING: This returns a Promise, not a Buffer!
* If you need a sync API, use Node.js or a pure-JS SHA-256 implementation.
*/
async digest(): Promise<Buffer> {
const data = Buffer.concat(this.chunks);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return Buffer.from(new Uint8Array(hashBuffer));
}
}

// ---------------------------------------------------------------------------
// Convenience API - SHA-256 (Node.js only for now due to async)
// ---------------------------------------------------------------------------

/**
* Create a SHA-256 hash context.
* NOTE: In browsers, this will throw - use a pure JS implementation or SubtleCrypto directly.
*/
export function createHash(algorithm: 'sha256'): HashFunction {
if (algorithm !== 'sha256') {
throw new Error(`Unsupported hash algorithm: ${algorithm}. Only 'sha256' is available.`);
}

const env = detectEnv();

switch (env) {
case 'node':
return new NodeSha256();

default:
throw new Error(
`Synchronous SHA-256 is not available in environment '${env}'. ` +
`In browsers, use crypto.subtle.digest('SHA-256', data) which is async, ` +
`or use a pure-JS SHA-256 implementation.`
);
}
}

/**
* Compute SHA-256 hash of data in one call.
*/
export function sha256(data: Buffer): Buffer {
return createHash('sha256').update(data).digest();
}

export default {
createHash,
sha256,
NodeSha256,
WebCryptoSha256,
};
2 changes: 2 additions & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ export * from './backends';
export * from './benchmark';
export * from './encoding';
export * from './errors';
export * from './hash';
export * from './merkle';
export * from './note';
export * from './proof';
export * from './gas';
export * from './random';
export * from './stealth';
export * from './withdraw';
export {
Expand Down
3 changes: 2 additions & 1 deletion sdk/src/note.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createHash, randomBytes } from 'crypto';
import { createHash } from './hash';
import { randomBytes } from './random';

// ---------------------------------------------------------------------------
// Backup format constants
Expand Down
193 changes: 193 additions & 0 deletions sdk/src/random.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* Browser-safe cryptographically secure random number generation.
*
* Provides environment detection and graceful fallbacks for:
* - Node.js (native crypto module)
* - Browsers (Web Crypto API)
* - Cloudflare Workers / Deno (Web Crypto API)
* - Other runtimes (throws helpful error with instructions)
*/

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/**
* A secure random source that can generate cryptographically safe bytes.
*/
export interface RandomSource {
/**
* Generate `n` cryptographically secure random bytes.
*/
randomBytes(n: number): Buffer;
}

// ---------------------------------------------------------------------------
// Environment detection
// ---------------------------------------------------------------------------

/**
* Detected execution environment.
*/
export type RuntimeEnv =
| 'node' // Node.js
| 'browser' // Web browser
| 'worker' // Web Worker / Cloudflare Worker
| 'deno' // Deno
| 'unknown'; // ¯\_(ツ)_/¯

/**
* Detect the current execution environment.
*/
export function detectEnv(): RuntimeEnv {
if (typeof process !== 'undefined' && process.versions?.node) {
return 'node';
}

if (typeof self !== 'undefined' && self.crypto) {
// Check for Cloudflare Worker or Web Worker
if (typeof (self as any).addEventListener !== 'undefined' && !self.document) {
return 'worker';
}
return 'browser';
}

if (typeof (globalThis as any).Deno !== 'undefined') {
return 'deno';
}

return 'unknown';
}

// ---------------------------------------------------------------------------
// Random source implementations
// ---------------------------------------------------------------------------

/**
* Node.js random source using built-in crypto module.
*/
export class NodeRandomSource implements RandomSource {
private readonly rb: (n: number) => Buffer;

constructor() {
// Lazy-require to avoid breaking browser bundlers
const { randomBytes } = require('crypto');
this.rb = randomBytes;
}

randomBytes(n: number): Buffer {
return this.rb(n);
}
}

/**
* Web Crypto API random source (works in browsers, Deno, and Cloudflare Workers).
*/
export class WebCryptoRandomSource implements RandomSource {
private readonly crypto: Crypto;

constructor(cryptoImpl?: Crypto) {
this.crypto = cryptoImpl || self.crypto;
if (!this.crypto?.getRandomValues) {
throw new Error(
'Web Crypto API is not available in this environment. ' +
'You may need to use a Node.js polyfill or provide a custom RandomSource.'
);
}
}

randomBytes(n: number): Buffer {
const arr = new Uint8Array(n);
this.crypto.getRandomValues(arr);
return Buffer.from(arr);
}
}

/**
* Random source that always throws.
* Used as the default fallback when no secure RNG is available.
*/
export class ThrowingRandomSource implements RandomSource {
constructor(public readonly env: RuntimeEnv) {}

randomBytes(n: number): Buffer {
throw new Error(
`No cryptographically secure random source available in detected environment '${this.env}'. ` +
`Please provide a custom RandomSource implementation for this runtime. ` +
`In Node.js, ensure you can 'require("crypto")'. ` +
`In browsers, ensure you're running in a secure context (HTTPS or localhost).`
);
}
}

// ---------------------------------------------------------------------------
// Default source auto-selection
// ---------------------------------------------------------------------------

let defaultSource: RandomSource | undefined;

/**
* Get the default random source for this environment.
* The source is lazily detected on first call and cached.
*/
export function getDefaultRandomSource(): RandomSource {
if (defaultSource) {
return defaultSource;
}

const env = detectEnv();

switch (env) {
case 'node':
defaultSource = new NodeRandomSource();
break;

case 'browser':
case 'worker':
case 'deno':
defaultSource = new WebCryptoRandomSource();
break;

default:
defaultSource = new ThrowingRandomSource(env);
}

return defaultSource;
}

/**
* Override the default random source.
* Useful for:
* - Testing with deterministic mocks
* - Using an HSM or hardware RNG
* - Unsupported runtimes
*/
export function setDefaultRandomSource(source: RandomSource): void {
defaultSource = source;
}

/**
* Clear the cached default source, forcing re-detection on next use.
*/
export function clearDefaultRandomSource(): void {
defaultSource = undefined;
}

/**
* Generate random bytes using the default source.
* Convenience export for callers.
*/
export function randomBytes(n: number): Buffer {
return getDefaultRandomSource().randomBytes(n);
}

export default {
randomBytes,
getDefaultRandomSource,
setDefaultRandomSource,
clearDefaultRandomSource,
detectEnv,
NodeRandomSource,
WebCryptoRandomSource,
ThrowingRandomSource,
};
3 changes: 2 additions & 1 deletion sdk/src/stealth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as elliptic from 'elliptic';
import { randomBytes, createHash } from 'crypto';
import { randomBytes } from './random';
import { createHash } from './hash';

const ed25519 = new elliptic.eddsa('ed25519');

Expand Down
Loading