import type { SecretStore } from '@chat-app/shared/auth'; import { appLocalDataDir } from '@tauri-apps/api/path'; import { type Client, type Store, Stronghold } from '@tauri-apps/plugin-stronghold'; // Stronghold-backed SecretStore. Vault file lives in Tauri's // `appLocalDataDir/chatapp.stronghold` and survives app re-installs (the // directory is preserved by the OS on macOS/Windows/Linux unless the user // manually removes it). Vault password is derived from the Supabase user-id // so the same user re-installing the app on the same machine recovers their // device key automatically. const VAULT_NAME = 'chatapp.stronghold'; const CLIENT_NAME = 'chatapp'; let strongholdRef: Stronghold | null = null; let storeRef: Store | null = null; let initPromise: Promise | null = null; let initializedFor: string | null = null; async function derivePassword(userId: string): Promise { const enc = new TextEncoder(); const buf = await crypto.subtle.digest( 'SHA-256', enc.encode('chatapp-stronghold-v1:' + userId), ); return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } async function ensureInit(userId: string): Promise { if (initializedFor === userId && storeRef) return; if (initPromise) return initPromise; initPromise = (async () => { const dir = await appLocalDataDir(); const sep = dir.endsWith('/') || dir.endsWith('\\') ? '' : '/'; const vaultPath = dir + sep + VAULT_NAME; const password = await derivePassword(userId); strongholdRef = await Stronghold.load(vaultPath, password); let client: Client; try { client = await strongholdRef.loadClient(CLIENT_NAME); } catch { client = await strongholdRef.createClient(CLIENT_NAME); } storeRef = client.getStore(); initializedFor = userId; })().finally(() => { initPromise = null; }); return initPromise; } export function makeStrongholdStore(userId: string): SecretStore { return { async getSecret(key: string): Promise { await ensureInit(userId); const val = await storeRef!.get(key); if (!val) return null; return val instanceof Uint8Array ? val : new Uint8Array(val); }, async setSecret(key: string, value: Uint8Array): Promise { await ensureInit(userId); await storeRef!.insert(key, Array.from(value)); await strongholdRef!.save(); }, async removeSecret(key: string): Promise { await ensureInit(userId); await storeRef!.remove(key); await strongholdRef!.save(); }, }; } // One-time migration: copies any keys we find in localStorage (the legacy // dev store) into Stronghold so a user who upgrades from a localStorage-only // build doesn't lose their device key. Safe to call multiple times — no-op // once the marker key is present. export async function migrateLocalStorageToStronghold( userId: string, prefix: string, ): Promise { await ensureInit(userId); if (!storeRef) return; const markerKey = '__migrated_from_localstorage'; const already = await storeRef.get(markerKey); if (already) return; for (let i = 0; i < window.localStorage.length; i++) { const fullKey = window.localStorage.key(i); if (!fullKey || !fullKey.startsWith(prefix)) continue; const raw = window.localStorage.getItem(fullKey); if (!raw) continue; try { const decoded = Uint8Array.from(atob(raw), (c) => c.charCodeAt(0)); const shortKey = fullKey.slice(prefix.length); await storeRef.insert(shortKey, Array.from(decoded)); } catch { // Skip malformed entries. } } await storeRef.insert(markerKey, [1]); if (strongholdRef) await strongholdRef.save(); }