diff --git a/src/config/index.ts b/src/config/index.ts index 2eec012..b09ca54 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -64,20 +64,39 @@ export const API_ENDPOINTS = { }, } as const; -/** - * Vault storage key names (AsyncStorage keys only — not user-editable "initial values"). - * - These are constants: the key names used to read/write/remove vault state in AsyncStorage. - * - The actual stored values (S0 data, '1') are set by the app; do not change these key strings - * unless you are migrating storage (changing them would make existing data unfindable). - * - Placed in config so VaultScreen and MeScreen (and others) use the same keys in one place. - * - INITIALIZED: app sets to '1' after first mnemonic flow; SHARE_DEVICE: app stores serialized S0. - * - "Reset Vault State" = remove both keys; next vault open sees no S0 and shows mnemonic flow. - */ +// ============================================================================= +// Vault storage (user-isolated, multi-account) +// ============================================================================= +// - AsyncStorage keys for vault state (S0 share, initialized flag). +// - User-scoped: each account has its own keys so vault state is isolated. +// - Store: use getVaultStorageKeys(userId) and write to INITIALIZED / SHARE_DEVICE. +// - Clear: use same keys in multiRemove (e.g. MeScreen Reset Vault State). +// - Multi-account: same device, multiple users → each has independent vault (no cross-user leakage). + +const VAULT_KEY_PREFIX = 'sentinel_vault'; + +/** Base key names (for reference). Prefer getVaultStorageKeys(userId) for all reads/writes. */ export const VAULT_STORAGE_KEYS = { INITIALIZED: 'sentinel_vault_initialized', SHARE_DEVICE: 'sentinel_vault_s0', } as const; +/** + * Returns vault storage keys for the given user (user isolation). + * - Use for: reading S0, writing S0 after mnemonic, clearing on Reset Vault State. + * - userId null → guest namespace (_guest). userId set → per-user namespace (_u{userId}). + */ +export function getVaultStorageKeys(userId: number | string | null): { + INITIALIZED: string; + SHARE_DEVICE: string; +} { + const suffix = userId != null ? `_u${userId}` : '_guest'; + return { + INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`, + SHARE_DEVICE: `${VAULT_KEY_PREFIX}_s0${suffix}`, + }; +} + // ============================================================================= // Helper Functions // ============================================================================= diff --git a/src/screens/MeScreen.tsx b/src/screens/MeScreen.tsx index 24e360b..7f97aba 100644 --- a/src/screens/MeScreen.tsx +++ b/src/screens/MeScreen.tsx @@ -18,7 +18,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { useAuth } from '../context/AuthContext'; import { Heir, HeirStatus, PaymentStrategy } from '../types'; import HeritageScreen from './HeritageScreen'; -import { VAULT_STORAGE_KEYS } from '../config'; +import { getVaultStorageKeys } from '../config'; // Mock heirs data const initialHeirs: Heir[] = [ @@ -310,10 +310,11 @@ export default function MeScreen() { const handleResetVault = async () => { setResetVaultFeedback({ status: 'idle', message: '' }); + const vaultKeys = getVaultStorageKeys(user?.id ?? null); try { await AsyncStorage.multiRemove([ - VAULT_STORAGE_KEYS.INITIALIZED, - VAULT_STORAGE_KEYS.SHARE_DEVICE, + vaultKeys.INITIALIZED, + vaultKeys.SHARE_DEVICE, ]); setResetVaultFeedback({ status: 'success', diff --git a/src/screens/VaultScreen.tsx b/src/screens/VaultScreen.tsx index d7d9576..749701e 100644 --- a/src/screens/VaultScreen.tsx +++ b/src/screens/VaultScreen.tsx @@ -25,7 +25,7 @@ import { VaultAsset, VaultAssetType, Heir } from '../types'; import BiometricModal from '../components/common/BiometricModal'; import { useAuth } from '../context/AuthContext'; import { useVaultAssets } from '../hooks/useVaultAssets'; -import { VAULT_STORAGE_KEYS } from '../config'; +import { getVaultStorageKeys } from '../config'; import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss'; // Asset type configuration with nautical theme @@ -183,15 +183,16 @@ export default function VaultScreen() { const [progressIndex, setProgressIndex] = useState(0); const [progressAnim] = useState(new Animated.Value(0)); const { user, token } = useAuth(); + const vaultKeys = React.useMemo(() => getVaultStorageKeys(user?.id ?? null), [user?.id]); const [isCapturing, setIsCapturing] = useState(false); const [treasureContent, setTreasureContent] = useState(''); const mnemonicRef = useRef(null); const progressTimerRef = useRef | null>(null); - // Detect S0 (TEE/SE): if present, later open shows biometric only; if not, mnemonic flow + // Detect S0 (TEE/SE) for current user: if present, later open shows biometric only; if not, mnemonic flow useEffect(() => { let cancelled = false; - AsyncStorage.getItem(VAULT_STORAGE_KEYS.SHARE_DEVICE) + AsyncStorage.getItem(vaultKeys.SHARE_DEVICE) .then((v) => { if (!cancelled) setHasS0(!!v); }) @@ -199,7 +200,7 @@ export default function VaultScreen() { if (!cancelled) setHasS0(false); }); return () => { cancelled = true; }; - }, []); + }, [vaultKeys.SHARE_DEVICE]); // Only when S0 exists and vault not unlocked: show biometric after short delay. // When hasS0 is false or null, never show biometric — go straight to mnemonic flow. @@ -379,9 +380,9 @@ export default function VaultScreen() { const entropy = mnemonicToEntropy(mnemonicWords, wordList); const shares = splitSecret(entropy); const s0 = shares[0]; // device share (S0) - // S0 is stored in AsyncStorage under SHARE_DEVICE — app-level storage, not hardware TEE/SE - await AsyncStorage.setItem(VAULT_STORAGE_KEYS.SHARE_DEVICE, serializeShare(s0)); - await AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, '1'); + // S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE + await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0)); + await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1'); setHasS0(true); setShowMnemonic(false); setShowBiometric(true);