269 lines
7.3 KiB
TypeScript
269 lines
7.3 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|