/** * Shamir's Secret Sharing (SSS) Implementation * * This implements a (3,2) threshold scheme where: * - Secret is split into 3 shares * - Any 2 shares can recover the original secret * * Correspondence with crypto_core_demo (Python): * - sp_trust_sharding.py: split_to_shares(), recover_from_shares() * - Same algorithm: f(x) = secret + a*x (mod P), Lagrange interpolation * - Difference: entropy conversion. Python uses BIP-39 (mnemonic.to_entropy); * we use custom word list index-based encoding for compatibility with * existing MNEMONIC_WORDS. SSS split/recover logic is identical. */ // Use a large prime for the finite field // We use 2^127 - 1 (a Mersenne prime) which fits well in BigInt // This is smaller than the Python version's 2^521 - 1 but sufficient for our 128-bit entropy const PRIME = BigInt('170141183460469231731687303715884105727'); // 2^127 - 1 /** * Represents an SSS share as a coordinate point (x, y) */ export interface SSSShare { x: number; y: bigint; label: string; // 'device' | 'cloud' | 'heir' } /** * Generate a cryptographically secure random BigInt in range [0, max) */ function secureRandomBigInt(max: bigint): bigint { // Get the number of bytes needed const byteLength = Math.ceil(max.toString(2).length / 8); const randomBytes = new Uint8Array(byteLength); // Use crypto.getRandomValues for secure randomness if (typeof crypto !== 'undefined' && crypto.getRandomValues) { crypto.getRandomValues(randomBytes); } else { // Fallback for environments without crypto for (let i = 0; i < byteLength; i++) { randomBytes[i] = Math.floor(Math.random() * 256); } } // Convert to BigInt let result = BigInt(0); for (let i = 0; i < randomBytes.length; i++) { result = (result << BigInt(8)) + BigInt(randomBytes[i]); } // Ensure result is within range return result % max; } /** * Convert mnemonic words to entropy (as BigInt) * Each word is mapped to its index, then combined into a single large number */ export function mnemonicToEntropy(words: string[], wordList: readonly string[]): bigint { let entropy = BigInt(0); const wordListLength = BigInt(wordList.length); for (const word of words) { const index = wordList.indexOf(word); if (index === -1) { throw new Error(`Word "${word}" not found in word list`); } entropy = entropy * wordListLength + BigInt(index); } return entropy; } /** * Convert entropy back to mnemonic words */ export function entropyToMnemonic(entropy: bigint, wordCount: number, wordList: readonly string[]): string[] { const words: string[] = []; const wordListLength = BigInt(wordList.length); let remaining = entropy; for (let i = 0; i < wordCount; i++) { const index = Number(remaining % wordListLength); words.unshift(wordList[index]); remaining = remaining / wordListLength; } return words; } /** * Modular inverse using extended Euclidean algorithm * Returns x such that (a * x) % p === 1 */ function modInverse(a: bigint, p: bigint): bigint { let [oldR, r] = [a % p, p]; let [oldS, s] = [BigInt(1), BigInt(0)]; while (r !== BigInt(0)) { const quotient = oldR / r; [oldR, r] = [r, oldR - quotient * r]; [oldS, s] = [s, oldS - quotient * s]; } // Ensure positive result return ((oldS % p) + p) % p; } /** * Modular arithmetic helper to ensure positive results */ function mod(n: bigint, p: bigint): bigint { return ((n % p) + p) % p; } /** * Split a secret into 3 shares using SSS (3,2) threshold scheme * * Uses linear polynomial: f(x) = secret + a*x (mod p) * where 'a' is a random coefficient * * Any 2 points on this line can recover the y-intercept (secret) */ export function splitSecret(secret: bigint): SSSShare[] { // Generate random coefficient for the polynomial const a = secureRandomBigInt(PRIME); // Polynomial: f(x) = secret + a*x (mod PRIME) const f = (x: number): bigint => { return mod(secret + a * BigInt(x), PRIME); }; // Generate 3 shares at x = 1, 2, 3 return [ { x: 1, y: f(1), label: 'device' }, { x: 2, y: f(2), label: 'cloud' }, { x: 3, y: f(3), label: 'heir' }, ]; } /** * Recover the secret from any 2 shares using Lagrange interpolation * * For 2 points (x1, y1) and (x2, y2), the secret (y-intercept) is: * S = (x2*y1 - x1*y2) / (x2 - x1) (mod p) */ export function recoverSecret(shareA: SSSShare, shareB: SSSShare): bigint { const { x: x1, y: y1 } = shareA; const { x: x2, y: y2 } = shareB; // Numerator: x2*y1 - x1*y2 const numerator = mod( BigInt(x2) * y1 - BigInt(x1) * y2, PRIME ); // Denominator: x2 - x1 const denominator = mod(BigInt(x2 - x1), PRIME); // Division in modular arithmetic = multiply by modular inverse const invDenominator = modInverse(denominator, PRIME); return mod(numerator * invDenominator, PRIME); } /** * Format a share for display (truncated for readability) * Shows first 8 and last 4 characters of the y-value */ export function formatShareForDisplay(share: SSSShare): string { const yStr = share.y.toString(); if (yStr.length <= 16) { return `(${share.x}, ${yStr})`; } return `(${share.x}, ${yStr.slice(0, 8)}...${yStr.slice(-4)})`; } /** * Format a share as a compact display string (for UI cards) * Returns a shorter format showing the share index and a hash-like preview */ export function formatShareCompact(share: SSSShare): string { const yStr = share.y.toString(); // Create a "fingerprint" from the y value const fingerprint = yStr.slice(0, 4) + '-' + yStr.slice(4, 8) + '-' + yStr.slice(-4); return fingerprint; } /** * Serialize a share to a string for storage/transmission */ export function serializeShare(share: SSSShare): string { return JSON.stringify({ x: share.x, y: share.y.toString(), label: share.label, }); } /** * Deserialize a share from a string */ export function deserializeShare(str: string): SSSShare { const parsed = JSON.parse(str); return { x: parsed.x, y: BigInt(parsed.y), label: parsed.label, }; } /** * Main function to generate mnemonic and SSS shares * This is the entry point for the vault initialization flow */ export interface VaultKeyData { mnemonic: string[]; shares: SSSShare[]; entropy: bigint; } export function generateVaultKeys( wordList: readonly string[], wordCount: number = 12 ): VaultKeyData { // Generate random mnemonic const mnemonic: string[] = []; for (let i = 0; i < wordCount; i++) { const index = Math.floor(Math.random() * wordList.length); mnemonic.push(wordList[index]); } // Convert to entropy const entropy = mnemonicToEntropy(mnemonic, wordList); // Split into shares const shares = splitSecret(entropy); return { mnemonic, shares, entropy }; } /** * Verify that shares can recover the original entropy * Useful for testing and validation */ export function verifyShares( shares: SSSShare[], originalEntropy: bigint ): boolean { // Test all 3 combinations of 2 shares const combinations = [ [shares[0], shares[1]], // Device + Cloud [shares[1], shares[2]], // Cloud + Heir [shares[0], shares[2]], // Device + Heir ]; for (const [a, b] of combinations) { const recovered = recoverSecret(a, b); if (recovered !== originalEntropy) { return false; } } return true; }