5.4 KiB

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, exportSelected(groupIds)
│   ├── 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 buildvite-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.

Export

  • exportSelected(groupIds) replaces the old exportAll() — accepts an array of group IDs to export. Pass null or [] for a full export. Vault meta (salt, test payload) is always included for import decryption.
  • ImportExport.svelte fetches groups/entries on modal open and shows a checkbox list for group selection with live entry count.

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.