2 Commits

Author SHA1 Message Date
Ada
fb1377eb4b Generate SSS shares from mnemonic words 2026-01-30 16:31:09 -08:00
Ada
c07f1f20d5 fix the warning 2026-01-30 15:22:13 -08:00
3 changed files with 399 additions and 47 deletions

View File

@@ -22,7 +22,16 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { SystemStatus, KillSwitchLog } from '../types'; import { SystemStatus, KillSwitchLog } from '../types';
import VaultScreen from './VaultScreen'; 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 = [ const MNEMONIC_WORDS = [
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage', 'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef', 'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
@@ -30,9 +39,17 @@ const MNEMONIC_WORDS = [
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom', 'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam', 'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud', 'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
'salt', 'wave', 'grotto', 'lagoon', 'storm', 'north', 'south', 'east', 'salt', 'wave', 'grotto', 'storm', 'north', 'south', 'east', 'west',
'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath', 'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain',
]; ] as const;
// Animation timing constants
const ANIMATION_DURATION = {
pulse: 1200,
glow: 1500,
rotate: 30000,
heartbeatPress: 150,
} as const;
const generateMnemonic = (wordCount = 12) => { const generateMnemonic = (wordCount = 12) => {
const words: string[] = []; const words: string[] = [];
@@ -43,17 +60,48 @@ const generateMnemonic = (wordCount = 12) => {
return words; return words;
}; };
const splitMnemonic = (words: string[]) => [ /**
words.slice(0, 4), * Generate SSS shares from mnemonic words
words.slice(4, 8), * Uses Shamir's Secret Sharing (3,2) threshold scheme
words.slice(8, 12), */
]; 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';
// Status configuration with nautical theme // Status configuration with nautical theme
const statusConfig: Record<SystemStatus, { const statusConfig: Record<SystemStatus, {
color: string; color: string;
label: string; label: string;
icon: string; icon: StatusIconName;
description: string; description: string;
gradientColors: [string, string]; gradientColors: [string, string];
}> = { }> = {
@@ -115,7 +163,7 @@ export default function SentinelScreen() {
const [showVault, setShowVault] = useState(false); const [showVault, setShowVault] = useState(false);
const [showMnemonic, setShowMnemonic] = useState(false); const [showMnemonic, setShowMnemonic] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]); const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]); const [sssShares, setSssShares] = useState<SSSShare[]>([]);
const [showEmailForm, setShowEmailForm] = useState(false); const [showEmailForm, setShowEmailForm] = useState(false);
const [emailAddress, setEmailAddress] = useState(''); const [emailAddress, setEmailAddress] = useState('');
const [isCapturing, setIsCapturing] = useState(false); const [isCapturing, setIsCapturing] = useState(false);
@@ -123,59 +171,73 @@ export default function SentinelScreen() {
useEffect(() => { useEffect(() => {
// Pulse animation // Pulse animation
Animated.loop( const pulseAnimation = Animated.loop(
Animated.sequence([ Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1.06, toValue: 1.06,
duration: 1200, duration: ANIMATION_DURATION.pulse,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1, toValue: 1,
duration: 1200, duration: ANIMATION_DURATION.pulse,
useNativeDriver: true, useNativeDriver: true,
}), }),
]) ])
).start(); );
pulseAnimation.start();
// Glow animation // Glow animation
Animated.loop( const glowAnimation = Animated.loop(
Animated.sequence([ Animated.sequence([
Animated.timing(glowAnim, { Animated.timing(glowAnim, {
toValue: 1, toValue: 1,
duration: 1500, duration: ANIMATION_DURATION.glow,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(glowAnim, { Animated.timing(glowAnim, {
toValue: 0.5, toValue: 0.5,
duration: 1500, duration: ANIMATION_DURATION.glow,
useNativeDriver: true, useNativeDriver: true,
}), }),
]) ])
).start(); );
glowAnimation.start();
// Slow rotate for ship wheel // Slow rotate for ship wheel
Animated.loop( const rotateAnimation = Animated.loop(
Animated.timing(rotateAnim, { Animated.timing(rotateAnim, {
toValue: 1, toValue: 1,
duration: 30000, duration: ANIMATION_DURATION.rotate,
useNativeDriver: true, useNativeDriver: true,
}) })
).start(); );
}, []); rotateAnimation.start();
// Cleanup animations on unmount to prevent memory leaks
return () => {
pulseAnimation.stop();
glowAnimation.stop();
rotateAnimation.stop();
};
}, [pulseAnim, glowAnim, rotateAnim]);
const openVaultWithMnemonic = () => { const openVaultWithMnemonic = () => {
const words = generateMnemonic(); const words = generateMnemonic();
const parts = splitMnemonic(words); const shares = generateSSSShares(words);
setMnemonicWords(words); setMnemonicWords(words);
setMnemonicParts(parts); setSssShares(shares);
setShowMnemonic(true); setShowMnemonic(true);
setShowVault(false); setShowVault(false);
setShowEmailForm(false); setShowEmailForm(false);
setEmailAddress(''); 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 () => { const handleScreenshot = async () => {
@@ -230,23 +292,23 @@ export default function SentinelScreen() {
Animated.sequence([ Animated.sequence([
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1.15, toValue: 1.15,
duration: 150, duration: ANIMATION_DURATION.heartbeatPress,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(pulseAnim, { Animated.timing(pulseAnim, {
toValue: 1, toValue: 1,
duration: 150, duration: ANIMATION_DURATION.heartbeatPress,
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(); ]).start();
// Add new log // Add new log using functional update to avoid stale closure
const newLog: KillSwitchLog = { const newLog: KillSwitchLog = {
id: Date.now().toString(), id: Date.now().toString(),
action: 'HEARTBEAT_CONFIRMED', action: 'HEARTBEAT_CONFIRMED',
timestamp: new Date(), timestamp: new Date(),
}; };
setLogs([newLog, ...logs]); setLogs((prevLogs) => [newLog, ...prevLogs]);
// Reset status if warning // Reset status if warning
if (status === 'warning') { if (status === 'warning') {
@@ -324,7 +386,7 @@ export default function SentinelScreen() {
colors={currentStatus.gradientColors} colors={currentStatus.gradientColors}
style={styles.statusCircle} style={styles.statusCircle}
> >
<Ionicons name={currentStatus.icon as any} size={56} color="#fff" /> <Ionicons name={currentStatus.icon} size={56} color="#fff" />
</LinearGradient> </LinearGradient>
</Animated.View> </Animated.View>
<Text style={[styles.statusLabel, { color: currentStatus.color }]}> <Text style={[styles.statusLabel, { color: currentStatus.color }]}>
@@ -390,6 +452,8 @@ export default function SentinelScreen() {
style={styles.vaultAccessButton} style={styles.vaultAccessButton}
onPress={openVaultWithMnemonic} onPress={openVaultWithMnemonic}
activeOpacity={0.8} activeOpacity={0.8}
accessibilityLabel="Open Shadow Vault"
accessibilityRole="button"
> >
<Text style={styles.vaultAccessButtonText}>Open</Text> <Text style={styles.vaultAccessButtonText}>Open</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -400,6 +464,8 @@ export default function SentinelScreen() {
style={styles.heartbeatButton} style={styles.heartbeatButton}
onPress={handleHeartbeat} onPress={handleHeartbeat}
activeOpacity={0.9} activeOpacity={0.9}
accessibilityLabel="Signal the watch - Confirm your presence"
accessibilityRole="button"
> >
<LinearGradient <LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]} colors={[colors.nautical.teal, colors.nautical.seafoam]}
@@ -451,6 +517,8 @@ export default function SentinelScreen() {
style={styles.vaultCloseButton} style={styles.vaultCloseButton}
onPress={() => setShowVault(false)} onPress={() => setShowVault(false)}
activeOpacity={0.85} activeOpacity={0.85}
accessibilityLabel="Close vault"
accessibilityRole="button"
> >
<Ionicons name="close" size={20} color={colors.nautical.cream} /> <Ionicons name="close" size={20} color={colors.nautical.cream} />
</TouchableOpacity> </TouchableOpacity>
@@ -468,15 +536,17 @@ export default function SentinelScreen() {
style={styles.mnemonicOverlay} style={styles.mnemonicOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
> >
<LinearGradient <View ref={mnemonicRef} collapsable={false}>
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]} <LinearGradient
style={styles.mnemonicCard} colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
ref={mnemonicRef} style={styles.mnemonicCard}
> >
<TouchableOpacity <TouchableOpacity
style={styles.mnemonicClose} style={styles.mnemonicClose}
onPress={() => setShowMnemonic(false)} onPress={() => setShowMnemonic(false)}
activeOpacity={0.85} activeOpacity={0.85}
accessibilityLabel="Close mnemonic modal"
accessibilityRole="button"
> >
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} /> <Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
</TouchableOpacity> </TouchableOpacity>
@@ -485,7 +555,7 @@ export default function SentinelScreen() {
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text> <Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
</View> </View>
<Text style={styles.mnemonicSubtitle}> <Text style={styles.mnemonicSubtitle}>
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.
</Text> </Text>
<View style={styles.mnemonicBlock}> <View style={styles.mnemonicBlock}>
<Text style={styles.mnemonicBlockText}> <Text style={styles.mnemonicBlockText}>
@@ -494,19 +564,25 @@ export default function SentinelScreen() {
</View> </View>
<View style={styles.partGrid}> <View style={styles.partGrid}>
<View style={[styles.partCard, styles.partCardStored]}> <View style={[styles.partCard, styles.partCardStored]}>
<Text style={styles.partLabel}>PART 1 LOCAL</Text> <Text style={styles.partLabel}>SHARE A DEVICE</Text>
<Text style={styles.partValue}>{mnemonicParts[0]?.join(' ')}</Text> <Text style={styles.partValue}>
{sssShares[0] ? formatShareCompact(sssShares[0]) : '---'}
</Text>
<Text style={styles.partHint}>Stored on this device</Text> <Text style={styles.partHint}>Stored on this device</Text>
</View> </View>
<View style={styles.partCard}> <View style={styles.partCard}>
<Text style={styles.partLabel}>PART 2 CLOUD NODE</Text> <Text style={styles.partLabel}>SHARE B CLOUD</Text>
<Text style={styles.partValue}>{mnemonicParts[1]?.join(' ')}</Text> <Text style={styles.partValue}>
<Text style={styles.partHint}>To be synced</Text> {sssShares[1] ? formatShareCompact(sssShares[1]) : '---'}
</Text>
<Text style={styles.partHint}>To be synced to Sentinel</Text>
</View> </View>
<View style={styles.partCard}> <View style={styles.partCard}>
<Text style={styles.partLabel}>PART 3 HEIR</Text> <Text style={styles.partLabel}>SHARE C HEIR</Text>
<Text style={styles.partValue}>{mnemonicParts[2]?.join(' ')}</Text> <Text style={styles.partValue}>
<Text style={styles.partHint}>To be assigned</Text> {sssShares[2] ? formatShareCompact(sssShares[2]) : '---'}
</Text>
<Text style={styles.partHint}>For your heir (2-of-3 required)</Text>
</View> </View>
</View> </View>
<TouchableOpacity <TouchableOpacity
@@ -514,6 +590,9 @@ export default function SentinelScreen() {
onPress={handleScreenshot} onPress={handleScreenshot}
activeOpacity={0.85} activeOpacity={0.85}
disabled={isCapturing} disabled={isCapturing}
accessibilityLabel="Take screenshot backup of mnemonic"
accessibilityRole="button"
accessibilityState={{ disabled: isCapturing }}
> >
<Text style={styles.mnemonicPrimaryText}> <Text style={styles.mnemonicPrimaryText}>
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'} {isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
@@ -523,6 +602,8 @@ export default function SentinelScreen() {
style={styles.mnemonicSecondaryButton} style={styles.mnemonicSecondaryButton}
onPress={handleEmailBackup} onPress={handleEmailBackup}
activeOpacity={0.85} activeOpacity={0.85}
accessibilityLabel="Send mnemonic backup via email"
accessibilityRole="button"
> >
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text> <Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
</TouchableOpacity> </TouchableOpacity>
@@ -542,12 +623,15 @@ export default function SentinelScreen() {
style={styles.emailSendButton} style={styles.emailSendButton}
onPress={handleSendEmail} onPress={handleSendEmail}
activeOpacity={0.85} activeOpacity={0.85}
accessibilityLabel="Send backup email"
accessibilityRole="button"
> >
<Text style={styles.emailSendText}>SEND EMAIL</Text> <Text style={styles.emailSendText}>SEND EMAIL</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) : null} ) : null}
</LinearGradient> </LinearGradient>
</View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</Modal> </Modal>
</View> </View>

5
src/utils/index.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Utility functions for Sentinel
*/
export * from './sss';

263
src/utils/sss.ts Normal file
View File

@@ -0,0 +1,263 @@
/**
* 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
*
* Based on the Sentinel crypto_core_demo Python implementation.
*/
// 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;
}