UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

176 lines 6.66 kB
"use strict"; /** * EntropyCollector * * Collects entropy from user interactions (mouse movements) and mixes it with * cryptographically secure random numbers to generate high-quality random seeds * for key generation. * * This provides defense-in-depth: even if the system's CSPRNG is compromised, * the user-provided entropy adds unpredictability. Conversely, if the user's * mouse movements are predictable, the CSPRNG ensures security. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.EntropyCollector = void 0; const sdk_1 = require("@bsv/sdk"); class EntropyCollector { constructor(config = {}) { var _a, _b; this.rawEntropy = { mousePositions: [], timingDeltas: [] }; this.lastSampleTime = 0; this.config = { targetSamples: (_a = config.targetSamples) !== null && _a !== void 0 ? _a : 256, minSampleInterval: (_b = config.minSampleInterval) !== null && _b !== void 0 ? _b : 10 }; } /** * Reset collected entropy data */ reset() { this.rawEntropy = { mousePositions: [], timingDeltas: [] }; this.lastSampleTime = 0; } /** * Add a mouse movement sample * Call this from a mousemove event handler * * @returns Progress information, or null if sample was rejected (too soon) */ addMouseSample(x, y) { const now = performance.now(); // Rate limit samples to reduce correlation if (now - this.lastSampleTime < this.config.minSampleInterval) { return null; } // Record timing delta (high precision timing adds entropy) if (this.lastSampleTime > 0) { this.rawEntropy.timingDeltas.push(now - this.lastSampleTime); } this.lastSampleTime = now; // Record position this.rawEntropy.mousePositions.push({ x, y, time: now }); return this.getProgress(); } /** * Get current collection progress */ getProgress() { const collected = this.rawEntropy.mousePositions.length; return { collected, target: this.config.targetSamples, percent: Math.min(100, Math.floor((collected / this.config.targetSamples) * 100)) }; } /** * Check if enough entropy has been collected */ isComplete() { return this.rawEntropy.mousePositions.length >= this.config.targetSamples; } /** * Extract entropy bytes from collected mouse data * Uses SHA-256 to compress and whiten the data */ extractRawEntropy() { // Serialize all collected data const dataPoints = []; // Add mouse positions for (const pos of this.rawEntropy.mousePositions) { // Use lower bits of coordinates (more random due to jitter) dataPoints.push(pos.x & 0xff); dataPoints.push(pos.y & 0xff); // Include fractional timing (microsecond precision) dataPoints.push(Math.floor(pos.time * 1000) & 0xff); dataPoints.push(Math.floor(pos.time * 1000000) & 0xff); } // Add timing deltas (high entropy source) for (const delta of this.rawEntropy.timingDeltas) { // Timing deltas converted to bytes dataPoints.push(Math.floor(delta * 1000) & 0xff); dataPoints.push(Math.floor(delta * 1000000) & 0xff); } // Hash to compress and whiten the entropy const hash = sdk_1.Hash.sha256(dataPoints); return new Uint8Array(hash); } /** * Mix user entropy with system CSPRNG for defense-in-depth * * @returns 32 bytes of high-quality random data suitable for key generation */ mixWithCSPRNG() { const userEntropy = this.extractRawEntropy(); const systemEntropy = new Uint8Array((0, sdk_1.Random)(32)); // XOR user entropy with system entropy const mixed = new Uint8Array(32); for (let i = 0; i < 32; i++) { mixed[i] = userEntropy[i] ^ systemEntropy[i]; } // Final hash to ensure uniform distribution const final = sdk_1.Hash.sha256(Array.from(mixed)); return new Uint8Array(final); } /** * Generate entropy with automatic CSPRNG fallback * * If not enough user entropy has been collected, supplements with CSPRNG. * Always mixes with CSPRNG regardless. * * @returns 32 bytes suitable for private key generation */ generateEntropy() { if (!this.isComplete()) { console.warn(`EntropyCollector: Only ${this.rawEntropy.mousePositions.length}/${this.config.targetSamples} ` + `samples collected. Supplementing with additional CSPRNG entropy.`); } return this.mixWithCSPRNG(); } /** * Convenience method for browser environments. * Creates event listeners and resolves when enough entropy is collected. * * @param element The HTML element to listen on (default: document) * @param onProgress Optional callback for progress updates * @returns Promise that resolves with 32 bytes of entropy */ async collectFromBrowser(element = document, onProgress) { return new Promise(resolve => { const handler = (event) => { if (event instanceof MouseEvent) { const progress = this.addMouseSample(event.clientX, event.clientY); if (progress) { onProgress === null || onProgress === void 0 ? void 0 : onProgress(progress); if (this.isComplete()) { element.removeEventListener('mousemove', handler); resolve(this.generateEntropy()); } } } }; element.addEventListener('mousemove', handler); }); } /** * Estimate the quality of collected entropy in bits * This is a rough heuristic, not a cryptographic guarantee */ estimateEntropyBits() { const samples = this.rawEntropy.mousePositions.length; if (samples === 0) return 0; // Assume roughly 4-6 bits per mouse position (jitter in lower bits) // Plus 2-4 bits from timing const bitsPerSample = 6; // Cap at 256 bits (32 bytes) since that's our output size return Math.min(256, samples * bitsPerSample); } } exports.EntropyCollector = EntropyCollector; //# sourceMappingURL=EntropyCollector.js.map