// UI handlers for vault unseal page. package ui import ( "net/http" "git.wntrmute.dev/kyle/mcias/internal/audit" "git.wntrmute.dev/kyle/mcias/internal/middleware" "git.wntrmute.dev/kyle/mcias/internal/model" "git.wntrmute.dev/kyle/mcias/internal/vault" ) // UnsealData is the view model for the unseal page. type UnsealData struct { Error string } // handleUnsealPage renders the unseal form, or redirects to login if already unsealed. func (u *UIServer) handleUnsealPage(w http.ResponseWriter, r *http.Request) { if !u.vault.IsSealed() { http.Redirect(w, r, "/login", http.StatusFound) return } u.render(w, "unseal", UnsealData{}) } // handleUnsealPost processes the unseal form submission. // // Security: The passphrase is never logged. No CSRF protection is applied // because there is no session to protect (the vault is sealed), and CSRF // token generation depends on the master key (chicken-and-egg). func (u *UIServer) handleUnsealPost(w http.ResponseWriter, r *http.Request) { if !u.vault.IsSealed() { http.Redirect(w, r, "/login", http.StatusFound) return } r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes) if err := r.ParseForm(); err != nil { u.render(w, "unseal", UnsealData{Error: "invalid form data"}) return } passphrase := r.FormValue("passphrase") if passphrase == "" { u.render(w, "unseal", UnsealData{Error: "passphrase is required"}) return } // Derive master key from passphrase. masterKey, err := vault.DeriveFromPassphrase(passphrase, u.db) if err != nil { u.logger.Error("vault unseal (UI): derive key", "error", err) u.render(w, "unseal", UnsealData{Error: "unseal failed"}) return } // Decrypt the signing key. privKey, pubKey, err := vault.DecryptSigningKey(u.db, masterKey) if err != nil { // Zero derived master key on failure. for i := range masterKey { masterKey[i] = 0 } u.logger.Error("vault unseal (UI): decrypt signing key", "error", err) u.render(w, "unseal", UnsealData{Error: "unseal failed"}) return } if err := u.vault.Unseal(masterKey, privKey, pubKey); err != nil { u.logger.Error("vault unseal (UI): state transition", "error", err) http.Redirect(w, r, "/login", http.StatusFound) return } ip := middleware.ClientIP(r, nil) u.writeAudit(r, model.EventVaultUnsealed, nil, nil, audit.JSON("source", "ui", "ip", ip)) u.logger.Info("vault unsealed via UI", "ip", ip) http.Redirect(w, r, "/login", http.StatusFound) }