# Password Vault — Agent Context ## Project Overview An offline-first, single-file password manager built with **Svelte 5** (runes-based reactivity). All data is encrypted in-browser with AES-256-GCM and stored in IndexedDB. The production build is a single `dist/index.html` with zero network dependencies — it works from `file://`, USB, or any static server. ## Architecture ``` src/ ├── App.svelte # Root component — routes between LockScreen, MainLayout ├── main.js # Entry point ├── components/ │ ├── LockScreen.svelte # Master password setup + unlock UI │ ├── MainLayout.svelte # Shell: sidebar + content area │ ├── Sidebar.svelte # Group list + search bar │ ├── EntryList.svelte # Credential entries grid │ ├── EntryForm.svelte # Create/edit credential form │ ├── EntryDetail.svelte # View single entry (copy password/username) │ ├── ImportExport.svelte # JSON import/export with merge/replace │ └── SettingsDialog.svelte # Auto-lock and tab-switch settings ├── lib/ │ ├── crypto/crypto.js # Web Crypto API: PBKDF2 key derivation, AES-GCM encrypt/decrypt, password generator │ ├── storage/db.js # IndexedDB layer (idb wrapper): entries, groups, meta stores │ ├── models/schema.js # Data models: CredentialEntry, Group; validation; ID generation │ ├── autofocus.js # Svelte action for autofocus on mount │ └── stores/ │ ├── app.svelte.js # AppStore: $state(isUnlocked, encryptionKey, salt) │ ├── security.svelte.js # Auto-lock timer, visibility change, beforeunload cleanup │ ├── search.svelte.js # Reactive search state │ └── settings.svelte.js # Reactive settings (auto-lock minutes, tab-switch lock) ``` ## Key Design Decisions - **Svelte 5 runes** — Use `$state`, `$derived`, `$effect`. Props-based event passing (no Svelte events). Event handler attributes are all lower-case (e.g. oninput) - **No external crypto libraries** — Uses the browser's native Web Crypto API exclusively. - **Key never persisted** — Encryption key lives only in `$state` memory; cleared on lock, tab switch, or page close. - **Single-file build** — `vite-plugin-singlefile` inlines all JS/CSS; post-build script inlines favicon. - **IndexedDB via `idb`** — Three stores: `entries`, `groups`, `meta`. Only `encryptedPassword` is encrypted at rest; titles, usernames, URLs, and notes are plaintext for searchability. - **PBKDF2 key derivation** — 600,000 iterations, SHA-256, 256-bit AES-GCM key. - **`GROUP_COLORS` exported from schema.js** — Shared between `createGroup()` and `Sidebar.svelte`. ## Encryption Flow ``` Master Password → PBKDF2 (600k iters, 16-byte salt) → AES-256-GCM Key → encrypt/decrypt credentials ``` Password verification uses a test payload (random string encrypted at vault creation). On unlock, the entered password derives a key that must successfully decrypt the test payload. ## Scripts | Command | Description | |---|---| | `npm run dev` | Vite dev server with HMR on `:5173` | | `npm run build` | Production build → `dist/index.html` (single self-contained file) | | `npm run preview` | Preview production build | | `npm run test` | Vitest (watch mode) | | `npm run test:run` | Vitest (single run) | ## Testing - Framework: **Vitest** with **jsdom** environment - Test setup: `tests/setup.js` (fake-indexeddb polyfill, Web Crypto API polyfill) - Test files: - `tests/lib/crypto/crypto.test.js` — Password generation, key derivation, encrypt/decrypt, verify, test payload, base64 utils - `tests/lib/models/schema.test.js` — ID generation, entry/group CRUD, validation, trash group - `tests/lib/storage/db.test.js` — Vault meta, settings, groups CRUD, entries CRUD, search, trash, export/import - `tests/lib/stores/app.test.js` — AppStore state and lockVault - `tests/lib/stores/search.test.js` — SearchStore query, group filter, clear - `tests/lib/stores/settings.test.js` — SettingsStore defaults, load, save - `tests/lib/stores/security.test.js` — Auto-lock timer, visibility change, beforeunload, activity reset - Run with `npm run test` or `npm run test:run` ## Security Notes - Only `encryptedPassword` is encrypted at rest; other fields (title, username, URL, notes) are plaintext in IndexedDB. - `testPlaintext` for password verification is stored unencrypted in the `meta` store. - Auto-lock triggers on tab visibility change and configurable inactivity timer (default 5 min). - Clipboard auto-clears after 15 seconds. - No browser fingerprinting or anti-keylogger protections. ## Known Bug Fixes - `base64ToUint8Array` was used in `db.js` `importAll()` but never imported — now imported from `crypto.js`. - `handlePermanentDelete()` in `EntryDetail.svelte` called `moveToTrash()` + `emptyTrash()` (wiping ALL trash) — now uses `deleteEntry()` directly.