feat(vault): vault storage (user-isolated, multi-account)
This commit is contained in:
@@ -64,20 +64,39 @@ export const API_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
// =============================================================================
|
||||||
* Vault storage key names (AsyncStorage keys only — not user-editable "initial values").
|
// Vault storage (user-isolated, multi-account)
|
||||||
* - 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
|
// - AsyncStorage keys for vault state (S0 share, initialized flag).
|
||||||
* unless you are migrating storage (changing them would make existing data unfindable).
|
// - User-scoped: each account has its own keys so vault state is isolated.
|
||||||
* - Placed in config so VaultScreen and MeScreen (and others) use the same keys in one place.
|
// - Store: use getVaultStorageKeys(userId) and write to INITIALIZED / SHARE_DEVICE.
|
||||||
* - INITIALIZED: app sets to '1' after first mnemonic flow; SHARE_DEVICE: app stores serialized S0.
|
// - Clear: use same keys in multiRemove (e.g. MeScreen Reset Vault State).
|
||||||
* - "Reset Vault State" = remove both keys; next vault open sees no S0 and shows mnemonic flow.
|
// - 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 = {
|
export const VAULT_STORAGE_KEYS = {
|
||||||
INITIALIZED: 'sentinel_vault_initialized',
|
INITIALIZED: 'sentinel_vault_initialized',
|
||||||
SHARE_DEVICE: 'sentinel_vault_s0',
|
SHARE_DEVICE: 'sentinel_vault_s0',
|
||||||
} as const;
|
} 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
|
// Helper Functions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
||||||
import HeritageScreen from './HeritageScreen';
|
import HeritageScreen from './HeritageScreen';
|
||||||
import { VAULT_STORAGE_KEYS } from '../config';
|
import { getVaultStorageKeys } from '../config';
|
||||||
|
|
||||||
// Mock heirs data
|
// Mock heirs data
|
||||||
const initialHeirs: Heir[] = [
|
const initialHeirs: Heir[] = [
|
||||||
@@ -310,10 +310,11 @@ export default function MeScreen() {
|
|||||||
|
|
||||||
const handleResetVault = async () => {
|
const handleResetVault = async () => {
|
||||||
setResetVaultFeedback({ status: 'idle', message: '' });
|
setResetVaultFeedback({ status: 'idle', message: '' });
|
||||||
|
const vaultKeys = getVaultStorageKeys(user?.id ?? null);
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.multiRemove([
|
await AsyncStorage.multiRemove([
|
||||||
VAULT_STORAGE_KEYS.INITIALIZED,
|
vaultKeys.INITIALIZED,
|
||||||
VAULT_STORAGE_KEYS.SHARE_DEVICE,
|
vaultKeys.SHARE_DEVICE,
|
||||||
]);
|
]);
|
||||||
setResetVaultFeedback({
|
setResetVaultFeedback({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { VaultAsset, VaultAssetType, Heir } from '../types';
|
|||||||
import BiometricModal from '../components/common/BiometricModal';
|
import BiometricModal from '../components/common/BiometricModal';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useVaultAssets } from '../hooks/useVaultAssets';
|
import { useVaultAssets } from '../hooks/useVaultAssets';
|
||||||
import { VAULT_STORAGE_KEYS } from '../config';
|
import { getVaultStorageKeys } from '../config';
|
||||||
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
||||||
|
|
||||||
// Asset type configuration with nautical theme
|
// Asset type configuration with nautical theme
|
||||||
@@ -183,15 +183,16 @@ export default function VaultScreen() {
|
|||||||
const [progressIndex, setProgressIndex] = useState(0);
|
const [progressIndex, setProgressIndex] = useState(0);
|
||||||
const [progressAnim] = useState(new Animated.Value(0));
|
const [progressAnim] = useState(new Animated.Value(0));
|
||||||
const { user, token } = useAuth();
|
const { user, token } = useAuth();
|
||||||
|
const vaultKeys = React.useMemo(() => getVaultStorageKeys(user?.id ?? null), [user?.id]);
|
||||||
const [isCapturing, setIsCapturing] = useState(false);
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
const [treasureContent, setTreasureContent] = useState('');
|
const [treasureContent, setTreasureContent] = useState('');
|
||||||
const mnemonicRef = useRef<View>(null);
|
const mnemonicRef = useRef<View>(null);
|
||||||
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const progressTimerRef = useRef<ReturnType<typeof setInterval> | 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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
AsyncStorage.getItem(VAULT_STORAGE_KEYS.SHARE_DEVICE)
|
AsyncStorage.getItem(vaultKeys.SHARE_DEVICE)
|
||||||
.then((v) => {
|
.then((v) => {
|
||||||
if (!cancelled) setHasS0(!!v);
|
if (!cancelled) setHasS0(!!v);
|
||||||
})
|
})
|
||||||
@@ -199,7 +200,7 @@ export default function VaultScreen() {
|
|||||||
if (!cancelled) setHasS0(false);
|
if (!cancelled) setHasS0(false);
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}, [vaultKeys.SHARE_DEVICE]);
|
||||||
|
|
||||||
// Only when S0 exists and vault not unlocked: show biometric after short delay.
|
// 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.
|
// 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 entropy = mnemonicToEntropy(mnemonicWords, wordList);
|
||||||
const shares = splitSecret(entropy);
|
const shares = splitSecret(entropy);
|
||||||
const s0 = shares[0]; // device share (S0)
|
const s0 = shares[0]; // device share (S0)
|
||||||
// S0 is stored in AsyncStorage under SHARE_DEVICE — app-level storage, not hardware TEE/SE
|
// S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE
|
||||||
await AsyncStorage.setItem(VAULT_STORAGE_KEYS.SHARE_DEVICE, serializeShare(s0));
|
await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0));
|
||||||
await AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, '1');
|
await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1');
|
||||||
setHasS0(true);
|
setHasS0(true);
|
||||||
setShowMnemonic(false);
|
setShowMnemonic(false);
|
||||||
setShowBiometric(true);
|
setShowBiometric(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user