Personal password vault. Crypto core + CLI + Avalonia desktop client + browser extension via native messaging.
docker run --rm -it \
-v $HOME/vaults:/vaults \
-e VAULT_SECRET_KEY="A1-..." \
ghcr.io/YOUR_USER/vaultcore-cli:latest list /vaults/my.vault# Backend
dotnet build
dotnet test
dotnet run --project src/Vault.Desktop
# Extension
cd extension && npm install && npm run buildSee COMPLETION.md for the full wire-up checklist.
Implemented:
- Crypto primitives (Argon2id, AES-256-GCM, HKDF, TOTP)
- Vault file format v2 with HMAC integrity + audit log section
- CLI with full feature surface
- CRUD operations + master password rotation (without re-encrypting items)
- HIBP breach check
- Import: Bitwarden JSON, 1Password .1pux
- Export: encrypted backup (file copy) + plaintext JSON (with double confirmation)
- Encrypted append-only audit log per vault
- Avalonia 11 desktop client with unlock + vault view + Add/Edit/Delete dialogs + audit viewer + settings + clipboard auto-clear + idle auto-lock + failed-attempt rate limiting
- Native messaging host (
vault-mh) — stateless proxy between extension and desktop - Browser extension (Manifest V3, TypeScript) — opt-in click-to-fill on password fields, popup with status & quick-copy
- Public Suffix List based registrable-domain matching for autofill
- S3-compatible sync layer with optimistic concurrency, item-level merge, audit log union — works with AWS, Backblaze B2, MinIO, R2, Wasabi
- Tombstones for delete propagation across synced devices, with resurrection rule
- KeePass 2.x XML import
- Sync button in desktop with credential prompt
- Background sync (opt-in) — interval timer + FileSystemWatcher debounced trigger
- Save-on-submit in browser extension — form submit detection + save banner, distinguishes signup from login
- TOTP autofill directly into one-time-code inputs (single input or 6-cell cluster), with fallback to popup copy
- Sync remote configuration UI in desktop with test-connection button, no longer CLI-only
- Tombstone GC via
vault tombstone-list/vault tombstone-prune - Scriptable sync with
vault sync --quietandVAULT_MASTER_PASSWORD/VAULT_SECRET_KEYenv vars (cron-friendly)
src/
Vault.Crypto/ primitives
Vault.Core/ vault format, items, audit, import, export, session
Vault.Cli/ AOT-compiled `vault` CLI binary
Vault.Desktop/ Avalonia 11 GUI
Vault.Ipc/ shared IPC contracts + native-messaging framing + PSL
Vault.Host/ AOT-compiled `vault-mh` native messaging host
Vault.Sync/ S3-compatible sync (push/pull/merge with optimistic concurrency)
extension/ TypeScript MV3 extension (Vite-built)
tests/
Vault.Crypto.Tests/
Vault.Core.Tests/
Vault.Ipc.Tests/
Vault.Sync.Tests/
[Page] form with password input
│
▼
content.ts ── chrome.runtime.sendMessage({find_credentials, url}) ──► background.ts
│ chrome.runtime.connectNative
▼
vault-mh (stdio JSON)
│ named pipe vaultcore-{user}
▼
VaultDesktop.exe
(PipeServerService)
│
uses unlocked VaultSession to
compute matches by registrable domain
The extension never sees keys or item ciphertext. The host is stateless. Only the desktop holds the unlocked session.
Requires .NET 10 SDK + Node.js 20+ for the extension.
# Backend
dotnet build
dotnet test
# Native messaging host (AOT-compiled, single binary)
dotnet publish src/Vault.Host -c Release -r linux-x64 # or win-x64, osx-arm64
# Desktop
dotnet run --project src/Vault.Desktop
# Extension
cd extension
npm install
npm run build- Build the extension (
npm run build); loadextension/distas unpacked in Chrome/Edge (Developer mode). - Note the extension ID shown by the browser.
- Build the host:
dotnet publish src/Vault.Host -c Release -r <rid>. The binary isvault-mh(Linux/macOS) orvault-mh.exe(Windows). - Register the host:
Windows:
./src/Vault.Host/install.sh <extension-id> /full/path/to/vault-mh
.\src\Vault.Host\install.ps1 -ExtensionId <id> -HostPath C:\path\vault-mh.exe
- Restart the browser. Open the extension popup — should report "Vault unlocked" while VaultDesktop is running.
master password ──► Argon2id(salt, ad=accountId) ──► MUK (32B)
│
Secret Key (string) ──SHA256──► salt
│
HKDF-SHA256
│
AUK (32B)
│
┌────────────────────────────┼──────────────────────┐
▼ ▼ ▼
AES-GCM("vault-verify-v1") AES-GCM(VaultKey) (used only at unlock)
VaultKey ─ HKDF("vault-hmac-v1") ──► HmacKey
─ HKDF("vault-audit-v1") ──► AuditKey
─ HKDF("vault-item-v1" || uuid)──► ItemKey
vault init <path> Create a new vault
vault unlock <path> Verify password (test unlock)
vault list <path> List item titles
vault search <path> <query> Search items by title/username/url
vault find-url <path> <url> Find logins matching a URL (eTLD+1)
vault get <path> <id> Show one item (decrypted, audit-logged)
vault add-login <path> Add a login interactively
vault delete <path> <id> Delete an item
vault change-password <path> Rotate master password
vault import-bitwarden <path> <file> Import Bitwarden JSON export
vault import-1pux <path> <file> Import 1Password .1pux archive
vault import-keepass-xml <path> <file> Import KeePass 2.x XML export
vault export-encrypted <path> <out> Backup vault (still encrypted)
vault export-plaintext <path> <out> DANGEROUS: dump everything as JSON
vault audit <path> [--all] Show audit log (last 50, or all)
vault audit-truncate <path> <keep> Drop all but most recent N entries
vault tombstone-list <path> Show pending deletes
vault tombstone-prune <path> <days> Drop tombstones older than N days
vault sync-configure <path> Set up S3-compatible remote
vault sync <path> [--quiet] Pull, merge, push (optimistic concurrency)
vault sync-status <path> Show last-known sync state
vault genpass [--len N] [--no-symbols]
vault totp <base32-secret> Print current TOTP code
vault check-pwned Check a password against HIBP
S3-compatible bucket. Tested mentally against AWS, Backblaze B2, MinIO, R2, Wasabi —
the SDK handles the dialect differences via ForcePathStyle + custom endpoint.
vault sync-configure ./my.vault
# Endpoint URL (e.g. https://s3.us-west-002.backblazeb2.com): https://s3.us-west-002.backblazeb2.com
# Region [us-east-1]: us-west-002
# Bucket name: my-vault-bucket
# Object key [vault.bin]: vault.bin
# Access key ID: 0021xxxx
# Secret access key: ********
# Force path style? [Y/n]: YState written to ./my.vault.sync (next to the vault file). Contains the
remote config + last known ETag — no secret material from the vault itself.
vault sync ./my.vault- HEAD remote → current ETag
- If ETag matches
LastKnownETag→ push local withIf-Match, done. - Otherwise pull remote, decrypt with same password+SecretKey, compare VaultKey fingerprints. Refuse if they differ (different vaults).
- Merge: union of items (newer UpdatedAt wins on collision), audit log concatenated and sorted by timestamp.
- Push merged file with
If-Match=current_remote_etag. - On conflict (someone pushed during step 4): retry up to 3 times.
- Item add / update on either device merges deterministically
- Audit log stays union-of-events across devices
- Master password rotation propagates (the new manifest is on the merged file; next sync from the other device will reject the old password — user has to unlock with new password locally before next sync)
Format v3 adds a tombstones section. When you delete an item, a small record
(itemId, deletedAt) is added. The merger uses tombstones to drop the item
on remote when their UpdatedAt < tombstone DeletedAt.
Resurrection: if remote has the item with UpdatedAt > DeletedAt (you re-created it explicitly after deleting), the tombstone is dropped and the item is kept. Otherwise the deletion propagates.
Tombstones are stored plaintext (just UUID + timestamp) — they leak only that "some item used to exist". The vault HMAC binds them to the rest of the file, so deletion of a tombstone (to revive a deleted item) breaks the file integrity.
- Schema upgrades are file-format-version-bumped (v3 now), so devices must be on compatible builds. v1/v2 files open as v3 read-only-style and upgrade on next save.
- Sync is manual via the CLI or the desktop "Sync" button. No background daemon yet.
- Tombstones never auto-GC. They're tiny (~24 bytes each), so for a personal vault this is fine — but a future cleanup tool could prune tombstones older than N days IF the user confirms all devices have synced past that date.
Autofill from the extension logs once per find_credentials call when
matches are returned, with details autofill request for <domain>, N match(es).
Individual credential decrypts during search are NOT logged (would flood).
- Click-to-fill, never auto-fill on load. The extension overlays a small badge on password fields. Filling is an explicit user action.
- PSL-based matching. A login for
https://accounts.google.comwill matchhttps://mail.google.combecause they share registrable domaingoogle.com, but won't matchgoogle-impostor.com. Replacesrc/Vault.Ipc/public_suffix_list.datwith the full https://publicsuffix.org/list/public_suffix_list.dat for production. - Per-user pipe. The named pipe is
vaultcore-{username}, isolating multi-user systems. - Native host gated by browser. Only extensions whose ID is in
allowed_origins(from the install script) can launchvault-mh. - Host is stateless. A compromised host process leaks only one in-flight request/response pair.
Optional, off by default. When enabled in Settings, the master password and secret key entered at unlock are kept in process memory (SecureBytes) for the session. The service then syncs:
- On a configurable interval (default 5 minutes)
- 30 seconds after any local vault file change (debounced FileSystemWatcher)
Lock or app close zeroes the credentials and stops the watchers. Trade-off: this is weaker than the default (credentials only briefly during unlock). For single-user multi-device convenience it's reasonable; for shared workstations or hostile environments, leave it off.
The content script listens for form submissions on pages with password
fields, plus Enter keypresses in password / text / email inputs (covers SPAs
without <form>). On submit:
- Capture username + password
- If matching credentials already exist for this site, do nothing
- Otherwise show a save banner (auto-dismisses after 30s)
- User clicks Save →
add_credentialIPC → desktop creates a Login item
The page URL is sent untrimmed; the desktop derives title from the host.
- OS-keychain / biometric quick-unlock. Per-platform (DPAPI on Windows, libsecret on Linux, Keychain on macOS, Hello/TouchID for biometric) — kept out for now to avoid platform-specific dependencies.
- Sync test connection from CLI. Available in desktop "Sync setup..." but not as a standalone CLI command yet.
- Add/Edit dialogs for non-Login item types in desktop GUI. SecureNote / CreditCard / Identity / SshKey / TotpSeed are CLI only.
- 6-cell TOTP cluster fill on shadow-DOM-encapsulated inputs. Some banks use closed shadow roots; the extension's content script can't pierce those.
- Auto-prune of tombstones. Manual via
vault tombstone-prune. A daemon could schedule pruning when all known devices have synced past a watermark, but tracking that requires a sync-state-vector field, not implemented. - Multi-vault support in desktop. UnlockView assumes one vault path at a time. Switching vaults = change the path field, unlock again.