From fb1377eb4b638208f27365ee565c9c93e184c4a5 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 30 Jan 2026 16:31:09 -0800 Subject: [PATCH] Generate SSS shares from mnemonic words --- src/screens/SentinelScreen.tsx | 86 ++++++++--- src/utils/index.ts | 5 + src/utils/sss.ts | 263 +++++++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 src/utils/index.ts create mode 100644 src/utils/sss.ts diff --git a/src/screens/SentinelScreen.tsx b/src/screens/SentinelScreen.tsx index 579c5b2..e433b6b 100644 --- a/src/screens/SentinelScreen.tsx +++ b/src/screens/SentinelScreen.tsx @@ -22,6 +22,14 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { SystemStatus, KillSwitchLog } from '../types'; import VaultScreen from './VaultScreen'; +import { + SSSShare, + mnemonicToEntropy, + splitSecret, + formatShareCompact, + serializeShare, + verifyShares, +} from '../utils/sss'; // Nautical-themed mnemonic word list (unique words only) const MNEMONIC_WORDS = [ @@ -52,11 +60,39 @@ const generateMnemonic = (wordCount = 12) => { return words; }; -const splitMnemonic = (words: string[]) => [ - words.slice(0, 4), - words.slice(4, 8), - words.slice(8, 12), -]; +/** + * Generate SSS shares from mnemonic words + * Uses Shamir's Secret Sharing (3,2) threshold scheme + */ +const generateSSSShares = (words: string[]): SSSShare[] => { + try { + // Convert mnemonic to entropy (big integer) + const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS); + + // Split entropy into 3 shares using SSS + const shares = splitSecret(entropy); + + // Verify shares can recover the original (optional, for debugging) + if (__DEV__) { + const isValid = verifyShares(shares, entropy); + if (!isValid) { + console.warn('SSS verification failed!'); + } else { + console.log('SSS shares verified successfully'); + } + } + + return shares; + } catch (error) { + console.error('Failed to generate SSS shares:', error); + // Fallback: return empty shares (should not happen in production) + return [ + { x: 1, y: BigInt(0), label: 'device' }, + { x: 2, y: BigInt(0), label: 'cloud' }, + { x: 3, y: BigInt(0), label: 'heir' }, + ]; + } +}; // Icon names type for type safety type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle'; @@ -127,7 +163,7 @@ export default function SentinelScreen() { const [showVault, setShowVault] = useState(false); const [showMnemonic, setShowMnemonic] = useState(false); const [mnemonicWords, setMnemonicWords] = useState([]); - const [mnemonicParts, setMnemonicParts] = useState([]); + const [sssShares, setSssShares] = useState([]); const [showEmailForm, setShowEmailForm] = useState(false); const [emailAddress, setEmailAddress] = useState(''); const [isCapturing, setIsCapturing] = useState(false); @@ -188,16 +224,20 @@ export default function SentinelScreen() { const openVaultWithMnemonic = () => { const words = generateMnemonic(); - const parts = splitMnemonic(words); + const shares = generateSSSShares(words); setMnemonicWords(words); - setMnemonicParts(parts); + setSssShares(shares); setShowMnemonic(true); setShowVault(false); setShowEmailForm(false); setEmailAddress(''); - AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => { - // Best-effort local store; UI remains available - }); + + // Store Share A (device share) locally + if (shares[0]) { + AsyncStorage.setItem('sentinel_share_device', serializeShare(shares[0])).catch(() => { + // Best-effort local store; UI remains available + }); + } }; const handleScreenshot = async () => { @@ -515,7 +555,7 @@ export default function SentinelScreen() { 12-Word Mnemonic - Your mnemonic is split into 3 parts (4/4/4). Part 1 is stored locally. + Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault. @@ -524,19 +564,25 @@ export default function SentinelScreen() { - PART 1 • LOCAL - {mnemonicParts[0]?.join(' ')} + SHARE A • DEVICE + + {sssShares[0] ? formatShareCompact(sssShares[0]) : '---'} + Stored on this device - PART 2 • CLOUD NODE - {mnemonicParts[1]?.join(' ')} - To be synced + SHARE B • CLOUD + + {sssShares[1] ? formatShareCompact(sssShares[1]) : '---'} + + To be synced to Sentinel - PART 3 • HEIR - {mnemonicParts[2]?.join(' ')} - To be assigned + SHARE C • HEIR + + {sssShares[2] ? formatShareCompact(sssShares[2]) : '---'} + + For your heir (2-of-3 required) { + 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; +}