vault

package
v0.2.3-dev Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 30, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

View Source
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

func InjectKey

func InjectKey(ctx context.Context, hex string) context.Context

InjectKey is a test helper that puts a vault_key_hex into the request context as if RequireUnlock had run. Lives here (rather than in test files) so handlers in vault and intake can exercise their unlock-required code paths without spinning up the full middleware chain.

func KeyFrom

func KeyFrom(ctx context.Context) (string, bool)

KeyFrom returns the vault_key hex injected by RequireUnlock.

Types

type CRUDHandlers

type CRUDHandlers struct {
	Store  *Store
	Bridge *cli.Bridge
}

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

type Handlers struct {
	Store  *Store
	Bridge *cli.Bridge
	// contains filtered or unexported fields
}

Handlers bundles the /api/user/vault/{unlock,lock,status,init} endpoints.

func NewHandlers

func NewHandlers(store *Store, bridge *cli.Bridge) *Handlers

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:

  1. If vault is already initialized, return 422 I_VAULT_ALREADY_INITIALIZED; the web layer refreshes /status and falls into the regular unlock flow.
  2. Spawn `aikey _internal init` with the password (stdin JSON).
  3. 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": "..."}

  1. call cli `vault-op metadata` to fetch salt + KDF params (no secret revealed)
  2. derive Argon2id(password, salt) locally -> 32-byte key -> hex
  3. call cli `vault-op verify` with the hex; on "ok", mint session
  4. 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

func NewStore(ttl time.Duration) *Store

NewStore returns a store with the given idle TTL. Use 10–15 minutes for Personal, shorter for high-security deployments.

func (*Store) Delete

func (s *Store) Delete(id string)

Delete drops a session id (used on explicit lock and after expiry).

func (*Store) Get

func (s *Store) Get(id string) (string, time.Duration, bool)

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

func (s *Store) Put(hex string) (id string, expiresAt time.Time)

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).

func (*Store) TTL

func (s *Store) TTL() time.Duration

TTL returns the configured session TTL.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL