/** * Persistent storage for keys and app settings. * On mobile: expo-secure-store for key material, AsyncStorage for settings. * On web: falls back to localStorage (dev only). */ import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; import type { KeyFile, Contact, NodeSettings } from './types'; // ─── Keys ───────────────────────────────────────────────────────────────────── const KEYFILE_KEY = 'dchain_keyfile'; const CONTACTS_KEY = 'dchain_contacts'; const SETTINGS_KEY = 'dchain_settings'; const CHATS_KEY = 'dchain_chats'; // Remembers (locally, per install) that this device's X25519 pub has been // successfully linked on-chain at least once. Distinguishes "first boot, // not registered yet" from "we were registered and then revoked by another // device". The second case triggers self-wipe. Stored in AsyncStorage — // if it's missing, we simply re-link. const DEVICE_REGISTERED_KEY = 'dchain_device_registered'; /** Save the key file in secure storage (encrypted on device). */ export async function saveKeyFile(kf: KeyFile): Promise { await SecureStore.setItemAsync(KEYFILE_KEY, JSON.stringify(kf)); } /** Load key file. Returns null if not set. */ export async function loadKeyFile(): Promise { const raw = await SecureStore.getItemAsync(KEYFILE_KEY); if (!raw) return null; return JSON.parse(raw) as KeyFile; } /** Delete key file (logout / factory reset). */ export async function deleteKeyFile(): Promise { await SecureStore.deleteItemAsync(KEYFILE_KEY); } // ─── Node settings ───────────────────────────────────────────────────────────── const DEFAULT_SETTINGS: NodeSettings = { nodeUrl: 'http://localhost:8081', contractId: '', }; export async function loadSettings(): Promise { const raw = await AsyncStorage.getItem(SETTINGS_KEY); if (!raw) return DEFAULT_SETTINGS; return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) }; } export async function saveSettings(s: Partial): Promise { const current = await loadSettings(); await AsyncStorage.setItem(SETTINGS_KEY, JSON.stringify({ ...current, ...s })); } // ─── Contacts ───────────────────────────────────────────────────────────────── export async function loadContacts(): Promise { const raw = await AsyncStorage.getItem(CONTACTS_KEY); if (!raw) return []; return JSON.parse(raw) as Contact[]; } export async function saveContact(c: Contact): Promise { const contacts = await loadContacts(); const idx = contacts.findIndex(x => x.address === c.address); if (idx >= 0) contacts[idx] = c; else contacts.push(c); await AsyncStorage.setItem(CONTACTS_KEY, JSON.stringify(contacts)); } export async function deleteContact(address: string): Promise { const contacts = await loadContacts(); await AsyncStorage.setItem( CONTACTS_KEY, JSON.stringify(contacts.filter(c => c.address !== address)), ); } // ─── Message cache (per-chat local store) ──────────────────────────────────── export interface CachedMessage { id: string; from: string; text: string; timestamp: number; mine: boolean; } export async function loadMessages(chatId: string): Promise { const raw = await AsyncStorage.getItem(`${CHATS_KEY}_${chatId}`); if (!raw) return []; return JSON.parse(raw) as CachedMessage[]; } export async function appendMessage(chatId: string, msg: CachedMessage): Promise { const msgs = await loadMessages(chatId); // Deduplicate by id if (msgs.find(m => m.id === msg.id)) return; msgs.push(msg); // Keep last 500 messages per chat const trimmed = msgs.slice(-500); await AsyncStorage.setItem(`${CHATS_KEY}_${chatId}`, JSON.stringify(trimmed)); } // ─── Multi-device bookkeeping (v2.2.0) ──────────────────────────────────── /** * isDeviceRegistered returns true if this device has ever successfully * linked its X25519 pub on-chain under the current master identity. * A true-then-absent transition (registered → not in chain's active list) * is interpreted as a remote revoke and triggers self-wipe. */ export async function isDeviceRegistered(): Promise { return (await AsyncStorage.getItem(DEVICE_REGISTERED_KEY)) === '1'; } /** markDeviceRegistered is called after a LINK_DEVICE commits or is observed in the registry on startup. */ export async function markDeviceRegistered(): Promise { await AsyncStorage.setItem(DEVICE_REGISTERED_KEY, '1'); } /** clearDeviceRegistered is part of wipeAllLocalState; also called on explicit logout. */ export async function clearDeviceRegistered(): Promise { await AsyncStorage.removeItem(DEVICE_REGISTERED_KEY); } /** * wipeAllLocalState zeroes out every on-device artifact tied to the * current identity: secure-store key, settings, contacts, chats cache, * registered-device marker. Safe to call multiple times. * * Called in two scenarios: * 1. Explicit "Delete account" in Settings. * 2. Self-detected revoke — the chain says our X25519 pub is no longer * in the active registry but we previously marked it registered, * so another device issued UNLINK_DEVICE against us. We must not * keep using the master priv any more — it still works at the * crypto level, but the social contract is that we're revoked. */ export async function wipeAllLocalState(): Promise { // Secure store (key). await SecureStore.deleteItemAsync(KEYFILE_KEY).catch(() => {}); // AsyncStorage — enumerate our known keys. We don't clear() the whole // store because a share-provider or other app shard could live there. const ks = await AsyncStorage.getAllKeys(); const ours = ks.filter(k => k === CONTACTS_KEY || k === SETTINGS_KEY || k === DEVICE_REGISTERED_KEY || k.startsWith(`${CHATS_KEY}_`), ); if (ours.length > 0) { await AsyncStorage.multiRemove(ours); } }