Documentation
¶
Overview ¶
Package vault hosts the personal-vault HTTP surface — unlock/lock/status/ init for the session lifecycle, plus the CRUD endpoints backing the User Vault Web page. All endpoints orchestrate the Rust aikey CLI through cli; Go never reads or writes vault.db and never performs AES-GCM.
The Argon2id key derivation runs on the Go side because the derived `vault_key_hex` must outlive the single unlock HTTP request and be reused by subsequent actions within the session.
Index ¶
- Constants
- func InjectKey(ctx context.Context, hex string) context.Context
- func KeyFrom(ctx context.Context) (string, bool)
- type CRUDHandlers
- func (h *CRUDHandlers) AliasPatchHandler(w http.ResponseWriter, r *http.Request)
- func (h *CRUDHandlers) EntryAddHandler(w http.ResponseWriter, r *http.Request)
- func (h *CRUDHandlers) EntryDeleteHandler(w http.ResponseWriter, r *http.Request)
- func (h *CRUDHandlers) ListHandler(w http.ResponseWriter, r *http.Request)
- func (h *CRUDHandlers) UseHandler(w http.ResponseWriter, r *http.Request)
- type Handlers
- type Store
Constants ¶
const SessionCookie = "aikey_vault_session"
SessionCookie is the HttpOnly session-id cookie name shared by all unlock / require-unlock paths.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type CRUDHandlers ¶
CRUDHandlers bundles the User-Vault-page endpoints. Depends on Store + cli.Bridge already built by the orchestrator.
func NewCRUDHandlers ¶
func NewCRUDHandlers(store *Store, bridge *cli.Bridge) *CRUDHandlers
NewCRUDHandlers wires a CRUDHandlers with shared deps.
func (*CRUDHandlers) AliasPatchHandler ¶
func (h *CRUDHandlers) AliasPatchHandler(w http.ResponseWriter, r *http.Request)
AliasPatchHandler: PATCH /api/user/vault/entry/alias.
func (*CRUDHandlers) EntryAddHandler ¶
func (h *CRUDHandlers) EntryAddHandler(w http.ResponseWriter, r *http.Request)
EntryAddHandler: POST /api/user/vault/entry.
func (*CRUDHandlers) EntryDeleteHandler ¶
func (h *CRUDHandlers) EntryDeleteHandler(w http.ResponseWriter, r *http.Request)
EntryDeleteHandler: DELETE /api/user/vault/entry.
func (*CRUDHandlers) ListHandler ¶
func (h *CRUDHandlers) ListHandler(w http.ResponseWriter, r *http.Request)
ListHandler: GET /api/user/vault/list.
Dispatches to one of two cli paths based on whether the caller has an unlocked vault session:
- Unlocked → spawns `query list_personal_with_masked` + `query list_oauth` in parallel and merges the two into a single `records[]` array.
- Locked → spawns the single `query list_metadata_locked` action.
Why this handler does its own session lookup instead of running under RequireUnlock middleware: we intentionally SHOULD serve this endpoint when locked, so the middleware would be wrong for it. See also `list_metadata_locked` in aikey-cli/src/commands_internal/query.rs for the security reasoning (2026-04-23 user decision A).
func (*CRUDHandlers) UseHandler ¶
func (h *CRUDHandlers) UseHandler(w http.ResponseWriter, r *http.Request)
UseHandler: POST /api/user/vault/use.
Switches the default-profile provider binding(s) for the given key. Multi-provider semantics match `aikey use` non-interactive mode.
Unlock required — the underlying vault-op verifies the vault_key against password_hash. The routing binding table isn't encrypted, but unlock is still required because the operation also refreshes `~/.aikey/active.env`.
type Handlers ¶
Handlers bundles the /api/user/vault/{unlock,lock,status,init} endpoints.
func NewHandlers ¶
NewHandlers wires a Handlers with shared deps.
func (*Handlers) InitHandler ¶
func (h *Handlers) InitHandler(w http.ResponseWriter, r *http.Request)
InitHandler: POST /api/user/vault/init (web-driven first-run vault initialization, per 20260430-个人vault-Web首次设置-方案A.md).
Body: {"password": "..."}
Behaviour:
- If vault is already initialized, return 422 I_VAULT_ALREADY_INITIALIZED; the web layer refreshes /status and falls into the regular unlock flow.
- Spawn `aikey _internal init` with the password (stdin JSON).
- On success, immediately derive the vault_key (Argon2id) and mint a session cookie — the user is now in the unlocked state without a redundant unlock prompt. Mirrors the UnlockHandler post-derive flow.
Distinct from UnlockHandler because there is no existing vault to verify against — initialization writes salt/KDF/password_hash, then we derive the same Argon2id key the cli just wrote.
func (*Handlers) LockHandler ¶
func (h *Handlers) LockHandler(w http.ResponseWriter, r *http.Request)
LockHandler: POST /api/user/vault/lock (explicit lock; session is dropped)
func (*Handlers) StatusHandler ¶
func (h *Handlers) StatusHandler(w http.ResponseWriter, r *http.Request)
StatusHandler: GET /api/user/vault/status (unauthenticated probe; used by the Web UI to render the locked vs unlocked banner; also gates the SetMasterPassword first-run CTA).
Per 20260430-个人vault-Web首次设置-方案A.md §1: the response carries an `initialized` field so a web-only user (who hasn't run any CLI command) can see "vault not yet set up — set master password" instead of being silently dumped on the unlock screen with no way forward.
Initialization is probed by calling `_internal vault-op metadata` with a placeholder vault key: the action returns `I_VAULT_NOT_INITIALIZED` when vault.db is missing or has no master_salt row, otherwise returns ok with salt/KDF parameters (no secrets revealed). Avoids needing a Go-side SQLite driver to peek at vault.db.
func (*Handlers) UnlockHandler ¶
func (h *Handlers) UnlockHandler(w http.ResponseWriter, r *http.Request)
UnlockHandler: POST /api/user/vault/unlock Body: {"password": "..."}
- call cli `vault-op metadata` to fetch salt + KDF params (no secret revealed)
- derive Argon2id(password, salt) locally -> 32-byte key -> hex
- call cli `vault-op verify` with the hex; on "ok", mint session
- zero password bytes asap
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is an in-memory session map keyed by session_id. Sessions survive process lifetime only; on restart users re-unlock.
func NewStore ¶
NewStore returns a store with the given idle TTL. Use 10–15 minutes for Personal, shorter for high-security deployments.
func (*Store) Get ¶
Get resolves a session id to its vault_key hex + remaining TTL. Returns ok=false when the id is unknown or has hard-expired (in which case the entry is evicted lazily).
Hard-expire: session TTL is fixed at unlock time and never extended. Why: the previous sliding TTL caused the 10s front-end status poll in index.tsx to defeat idle auto-lock — any open tab kept the session alive indefinitely. Idle policy requires password re-entry at expiry regardless of background activity.
func (*Store) Put ¶
Put records `hex` as a freshly-unlocked session and returns the new session id (random 32-byte base64url) along with its absolute expiry.
func (*Store) RequireUnlock ¶
func (s *Store) RequireUnlock(next http.HandlerFunc) http.HandlerFunc
RequireUnlock wraps a handler so it runs only when the request carries a valid session cookie. The handler can retrieve the hex via KeyFrom(ctx).