389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
# ClawVault — SPEC v0.1
|
|
|
|
*A personal vault built for humans who have AI assistants.*
|
|
|
|
---
|
|
|
|
## The Problem
|
|
|
|
Existing password managers are built for human→human sharing.
|
|
The AI assistant use case is different:
|
|
- Your AI needs ~30 of your 500 credentials to do its job
|
|
- Some credentials should never reach an AI regardless of trust
|
|
- Card numbers, CVV, passport data — client-side decrypt only
|
|
- Everything else — AI can read, use, act on
|
|
|
|
Bitwarden MCP server exists but is all-or-nothing. Nothing has two-tier encryption
|
|
with field-level AI visibility, WebAuthn unlock, and LLM-powered field mapping.
|
|
|
|
---
|
|
|
|
## Design Principles
|
|
|
|
1. **One binary** — Go, cross-compiles mac/linux/windows, single port
|
|
2. **One file** — SQLite DB, platform-independent, portable
|
|
3. **Entry model** — same tree as inou/dealspace: everything is an entry
|
|
4. **Two-tier encryption** — L1 (server key, AI-readable) + L2 (client-side WebAuthn only)
|
|
5. **LLM import** — any format, no format-specific parsers
|
|
6. **LLM field mapping** — extension fills forms intelligently
|
|
7. **No external dependencies** — no cloud, no subscriptions
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
clawvault/
|
|
├── cmd/clawvault/main.go # single entrypoint
|
|
├── api/ # REST + MCP handlers
|
|
├── lib/ # crypto, dbcore, types
|
|
├── web/ # embedded SPA (go:embed, vanilla JS)
|
|
└── extension/ # Chrome extension (no build step)
|
|
```
|
|
|
|
Single binary, one port (default 8765):
|
|
- `GET /` → embedded web UI
|
|
- `/api/*` → REST API
|
|
- `/mcp` → MCP endpoint (AI, L1 only)
|
|
- `/ext/*` → extension API (full access)
|
|
|
|
---
|
|
|
|
## Data Model
|
|
|
|
### Entry table (single table for everything)
|
|
|
|
```go
|
|
type Entry struct {
|
|
EntryID string // uuid
|
|
ParentID string // folder entry_id, or "" for root
|
|
Type string // credential|note|identity|card|ssh_key|totp|folder|any
|
|
Title string // plaintext title
|
|
TitleIdx string // HMAC-SHA256 blind index for search
|
|
Data []byte // zstd + AES-256-GCM (L1 key server-side)
|
|
// OR: zstd + AES-256-GCM (L2 key client-side only)
|
|
DataLevel int // 1=L1, 2=L2
|
|
CreatedAt int64
|
|
UpdatedAt int64
|
|
Version int // optimistic locking
|
|
}
|
|
```
|
|
|
|
### VaultData (packed into Entry.Data)
|
|
|
|
```go
|
|
type VaultData struct {
|
|
Title string `json:"title"`
|
|
Type string `json:"type"`
|
|
Fields []VaultField `json:"fields"`
|
|
URLs []string `json:"urls,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Expires string `json:"expires,omitempty"` // YYYY-MM-DD
|
|
Notes string `json:"notes,omitempty"`
|
|
Files []VaultFile `json:"files,omitempty"`
|
|
}
|
|
|
|
type VaultField struct {
|
|
Label string `json:"label"` // "Username", "Password", "CVV" — anything
|
|
Value string `json:"value"` // plaintext after decrypt
|
|
Kind string `json:"kind"` // text|password|totp|url|file
|
|
Section string `json:"section,omitempty"` // visual grouping
|
|
L2 bool `json:"l2,omitempty"` // true = client-side decrypt only
|
|
}
|
|
|
|
type VaultFile struct {
|
|
Name string `json:"name"`
|
|
MimeType string `json:"mime_type"`
|
|
Size int64 `json:"size"`
|
|
Data []byte `json:"data"` // encrypted blob stored in SQLite
|
|
}
|
|
```
|
|
|
|
The `type` field is just a UI hint — it never constrains the fields.
|
|
A "shoe size" entry is valid. A "custom API token" entry is valid.
|
|
|
|
### Example entries
|
|
|
|
**Credit card:**
|
|
```json
|
|
{
|
|
"type": "card",
|
|
"title": "Amex Platinum",
|
|
"fields": [
|
|
{"label":"Cardholder","value":"Johan Jongsma","kind":"text"},
|
|
{"label":"Number","value":"3782 8224 6310 005","kind":"password","l2":true},
|
|
{"label":"CVV","value":"1234","kind":"password","l2":true},
|
|
{"label":"Expiry","value":"09/28","kind":"text"},
|
|
{"label":"Bank","value":"American Express","kind":"text"}
|
|
]
|
|
}
|
|
```
|
|
AI sees: Cardholder, Expiry, Bank. Never Number or CVV.
|
|
|
|
**Identity:**
|
|
```json
|
|
{
|
|
"type": "identity",
|
|
"title": "Johan Jongsma",
|
|
"fields": [
|
|
{"label":"First Name","value":"Johan","section":"Personal"},
|
|
{"label":"Last Name","value":"Jongsma","section":"Personal"},
|
|
{"label":"Email","value":"johan@jongsma.me","section":"Personal"},
|
|
{"label":"Phone","value":"+17272252475","section":"Personal"},
|
|
{"label":"Address","value":"851 Brightwaters Blvd","section":"Address"},
|
|
{"label":"City","value":"St. Petersburg","section":"Address"},
|
|
{"label":"State","value":"FL","section":"Address"},
|
|
{"label":"ZIP","value":"33704","section":"Address"},
|
|
{"label":"Passport","value":"NL12345678","kind":"password","l2":true,"section":"Documents"}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Two-Tier Encryption
|
|
|
|
### L1 — Server Key (AI-readable)
|
|
|
|
- `VAULT_KEY` env var (hex-encoded 32 bytes)
|
|
- Per-entry key: HKDF-SHA256(VAULT_KEY, entry_id)
|
|
- Encryption: zstd compress → AES-256-GCM encrypt → store
|
|
- AI (MCP) can read all L1 fields
|
|
- L2 fields within an L1 entry: individual field values are further encrypted
|
|
with L2 key before being packed into VaultData — server stores ciphertext,
|
|
returns `{"label":"CVV","value":null,"l2":true}` to AI
|
|
|
|
### L2 — Client Key (WebAuthn PRF, never leaves device)
|
|
|
|
- Derived entirely in browser via WebAuthn PRF extension
|
|
- `navigator.credentials.get({extensions:{prf:{eval:{first:salt}}}})` → 32-byte secret
|
|
- Secret → HKDF → L2 key → decrypt L2 field values in browser (Web Crypto API)
|
|
- Server NEVER sees L2 key or L2 plaintext
|
|
- L2 key lives in `sessionStorage` only — gone on tab close
|
|
- Auto-lock after 15 minutes idle (configurable)
|
|
|
|
### WebAuthn authenticators supported
|
|
|
|
- Touch ID (Mac) — native browser prompt
|
|
- Face ID (iPhone, via web UI on iOS Safari)
|
|
- Windows Hello
|
|
- Google Titan Key (USB-A/NFC, FIDO2)
|
|
- YubiKey (FIDO2)
|
|
|
|
Register multiple authenticators for redundancy. One unlock = all L2 accessible.
|
|
|
|
### Recovery
|
|
|
|
- Printed 12-word BIP39 mnemonic generated at vault setup
|
|
- Mnemonic derives L2 key as last resort (entropy → HKDF)
|
|
- Store physically (safe, safety deposit box)
|
|
- NO email/SMS/server fallback — would break the security model
|
|
|
|
---
|
|
|
|
## Import (LLM-powered)
|
|
|
|
`POST /api/import` — multipart, accepts any file format
|
|
|
|
Flow:
|
|
1. User uploads file (Bitwarden JSON, 1Password CSV, LastPass, Chrome, plain text)
|
|
2. Server sends content to Fireworks LLM (zero retention):
|
|
> "Parse this password export into ClawVault entries. Return JSON array of
|
|
> VaultData. Guess types. Mark l2:true on fields that appear sensitive:
|
|
> card numbers, CVV, SSN, passport numbers, private keys, TOTP seeds."
|
|
3. Server returns preview — user reviews before committing
|
|
4. **L2 fields highlighted in amber** — "These fields will NOT be readable by your AI"
|
|
5. User can toggle L2 per field in preview
|
|
6. Confirm → write to DB
|
|
|
|
Best practice enforced by UI: "Review L2 markings before importing."
|
|
Sensitive fields default to L2 — user explicitly unlocks them for AI if desired.
|
|
|
|
---
|
|
|
|
## Browser Extension
|
|
|
|
Manifest V3, Chrome first.
|
|
|
|
### Files
|
|
- `manifest.json`
|
|
- `background.js` — service worker, API key, vault calls
|
|
- `content.js` — form detection, field filling
|
|
- `popup.html/js` — credential picker
|
|
|
|
### Extension API key
|
|
Stored in `chrome.storage.local`. Full access (L1 + L2 via client-side decrypt).
|
|
Extension has its own token type: `ext` — distinguishable in audit log.
|
|
|
|
### LLM Field Mapping
|
|
|
|
1. Content script detects forms (any login/fill page)
|
|
2. Serializes visible inputs: label, name, placeholder, type, selector
|
|
3. User opens popup → picks credential
|
|
4. Background calls `POST /ext/map`:
|
|
```json
|
|
{"entry_id":"...", "fields":[{"selector":"#email","label":"Email","type":"email"},...]}
|
|
```
|
|
5. Server calls LLM: maps vault fields to form selectors
|
|
6. Content script fills mapped fields
|
|
7. TOTP field detected → live code generated → filled automatically
|
|
|
|
### L2 fill flow
|
|
- Extension popup shows 🔒 on L2 fields
|
|
- Click → WebAuthn prompt in browser (Touch ID etc.)
|
|
- L2 key derived → L2 fields decrypted in browser memory → filled
|
|
- Key discarded after fill
|
|
|
|
---
|
|
|
|
## TOTP
|
|
|
|
Seed stored as L2 field by default (user can change to L1 per entry).
|
|
|
|
**If L1 (AI-accessible):**
|
|
- `GET /api/ext/totp/:id` → `{"code":"123456","expires_in":23}`
|
|
- MCP tool `get_totp("GitHub")` → returns live code
|
|
- AI can complete 2FA flows autonomously — killer feature
|
|
|
|
**If L2 (client-only):**
|
|
- Seed only decryptable client-side
|
|
- Extension generates code locally after WebAuthn unlock
|
|
- AI cannot access
|
|
|
|
Default: **L2** for TOTP seeds. User explicitly marks L1 to enable AI 2FA automation.
|
|
This is a conscious, visible decision — not a silent default.
|
|
|
|
---
|
|
|
|
## MCP Tools (L1 only)
|
|
|
|
```
|
|
get_credential(query) → VaultData, L2 fields omitted
|
|
list_credentials(filter?) → [{entry_id, title, type, urls}]
|
|
get_totp(query) → {code, expires_in} (L1 TOTP only)
|
|
search_vault(query) → [{entry_id, title, type, matched_field}]
|
|
check_expiring(days?) → [{title, type, expires, days_remaining}]
|
|
create_entry(data) → entry_id (AI can save new credentials)
|
|
```
|
|
|
|
MCP token: read-only by default. Write token optional (for AI to save new credentials).
|
|
|
|
---
|
|
|
|
## Web UI
|
|
|
|
Vanilla JS, embedded in binary, no framework, no build step.
|
|
|
|
### Views
|
|
- `/` → entry list, folder tree, search
|
|
- `/entry/:id` → detail view
|
|
- `/entry/new` → create (dynamic field builder, type picker)
|
|
- `/import` → LLM import with preview
|
|
- `/settings` → WebAuthn setup, tokens, export, audit log
|
|
|
|
### L2 UX
|
|
- L2 fields render as: `🔒 Locked — Touch to unlock`
|
|
- Click → WebAuthn prompt → decrypt in browser → show value + copy button
|
|
- "Unlock all" button → single WebAuthn prompt → all L2 visible for session
|
|
- Lock icon in nav shows session state (locked/unlocked)
|
|
|
|
---
|
|
|
|
## Audit Log
|
|
|
|
```go
|
|
type AuditEvent struct {
|
|
EventID string
|
|
EntryID string
|
|
Title string // snapshot
|
|
Action string // read|fill|ai_read|create|update|delete|import|export
|
|
Actor string // web|extension|mcp
|
|
IPAddr string
|
|
Timestamp int64
|
|
}
|
|
```
|
|
|
|
`GET /api/audit` — paginated, filterable by actor/action/entry.
|
|
AI access clearly marked as `actor:"mcp"`.
|
|
|
|
---
|
|
|
|
## Password Generator
|
|
|
|
`GET /api/generate?length=20&symbols=true`
|
|
`GET /api/generate?words=4` → "correct-horse-battery-staple"
|
|
|
|
Crypto/rand throughout. Built into field editor.
|
|
|
|
---
|
|
|
|
## Config
|
|
|
|
```bash
|
|
VAULT_KEY=<hex-encoded 32 bytes> # required, L1 master key
|
|
PORT=8765
|
|
DB_PATH=./clawvault.db
|
|
FIREWORKS_API_KEY=... # for LLM import + field mapping
|
|
LLM_MODEL=accounts/fireworks/models/llama-v3p3-70b-instruct
|
|
SESSION_TTL=86400 # seconds
|
|
L2_LOCK_IDLE=900 # L2 auto-lock after 15min idle
|
|
```
|
|
|
|
---
|
|
|
|
## Build Plan
|
|
|
|
### Day 1 — Foundation
|
|
- [ ] Go module, entry model, L1 crypto, dbcore
|
|
- [ ] CRUD API (create/read/update/delete entries)
|
|
- [ ] Web UI: list, create, view, edit
|
|
- [ ] Password generator
|
|
- [ ] Session auth (token-based)
|
|
|
|
### Day 2 — L2 + Extension
|
|
- [ ] L2 field encryption (client-side Web Crypto in browser)
|
|
- [ ] WebAuthn PRF registration + unlock flow
|
|
- [ ] Chrome extension: popup, content script, LLM fill
|
|
- [ ] `/ext/map` LLM field mapping endpoint
|
|
|
|
### Day 3 — AI + Import
|
|
- [ ] MCP endpoint + 6 tools
|
|
- [ ] OpenClaw skill: ClawVault
|
|
- [ ] LLM import (`/api/import`) with preview UI
|
|
- [ ] TOTP generation
|
|
|
|
### Day 4 — Complete
|
|
- [ ] Audit log
|
|
- [ ] File attachments (BLOB in SQLite)
|
|
- [ ] Expiry alerts (heartbeat + MCP)
|
|
- [ ] Export (Bitwarden-compatible JSON)
|
|
- [ ] Systemd service + deploy script
|
|
|
|
---
|
|
|
|
## Out of Scope v0.1
|
|
|
|
- Mobile autofill (v2, OSS community)
|
|
- Firefox/Safari extension
|
|
- Multi-user/team sharing
|
|
- Browser extension for non-Chromium
|
|
|
|
---
|
|
|
|
## Competitive Landscape
|
|
|
|
| | ClawVault | Bitwarden MCP | mcp-secrets-vault | 1Password |
|
|
|--|--|--|--|--|
|
|
| Field-level AI visibility | ✅ | ❌ | ❌ | ❌ |
|
|
| Two-tier encryption | ✅ | ❌ | ❌ | ❌ |
|
|
| WebAuthn L2 unlock | ✅ | ❌ | ❌ | ✅ (auth only) |
|
|
| LLM field mapping | ✅ | ❌ | ❌ | ❌ |
|
|
| LLM import (any format) | ✅ | ❌ | ❌ | ❌ |
|
|
| One binary | ✅ | ❌ | ✅ | ❌ |
|
|
| Self-hosted | ✅ | ✅ | ✅ | ❌ |
|
|
| Open source | ✅ | ✅ | ✅ | ❌ |
|
|
|
|
---
|
|
|
|
*ClawVault — the vault that knows who it's talking to.*
|