// Vault seal/unseal REST handlers for MCIAS. package server 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" ) // unsealRequest is the request body for POST /v1/vault/unseal. type unsealRequest struct { Passphrase string `json:"passphrase"` } // handleUnseal accepts a passphrase, derives the master key, decrypts the // signing key, and unseals the vault. Rate-limited to 3/s burst 5. // // Security: The passphrase is never logged. A generic error is returned on // any failure to prevent information leakage about the vault state. func (s *Server) handleUnseal(w http.ResponseWriter, r *http.Request) { if !s.vault.IsSealed() { writeJSON(w, http.StatusOK, map[string]string{"status": "already unsealed"}) return } var req unsealRequest if !decodeJSON(w, r, &req) { return } if req.Passphrase == "" { middleware.WriteError(w, http.StatusBadRequest, "passphrase is required", "bad_request") return } // Derive master key from passphrase. masterKey, err := vault.DeriveFromPassphrase(req.Passphrase, s.db) if err != nil { s.logger.Error("vault unseal: derive key", "error", err) middleware.WriteError(w, http.StatusUnauthorized, "unseal failed", "unauthorized") return } // Decrypt the signing key. privKey, pubKey, err := vault.DecryptSigningKey(s.db, masterKey) if err != nil { // Zero derived master key on failure. for i := range masterKey { masterKey[i] = 0 } s.logger.Error("vault unseal: decrypt signing key", "error", err) middleware.WriteError(w, http.StatusUnauthorized, "unseal failed", "unauthorized") return } if err := s.vault.Unseal(masterKey, privKey, pubKey); err != nil { s.logger.Error("vault unseal: state transition", "error", err) middleware.WriteError(w, http.StatusConflict, "vault is already unsealed", "conflict") return } ip := middleware.ClientIP(r, nil) s.writeAudit(r, model.EventVaultUnsealed, nil, nil, audit.JSON("source", "api", "ip", ip)) s.logger.Info("vault unsealed via API", "ip", ip) writeJSON(w, http.StatusOK, map[string]string{"status": "unsealed"}) } // handleSeal seals the vault, zeroing all key material. Admin-only. // // Security: The caller's token becomes invalid after sealing because the // public key needed to validate it is no longer available. func (s *Server) handleSeal(w http.ResponseWriter, r *http.Request) { if s.vault.IsSealed() { writeJSON(w, http.StatusOK, map[string]string{"status": "already sealed"}) return } claims := middleware.ClaimsFromContext(r.Context()) var actorID *int64 if claims != nil { acct, err := s.db.GetAccountByUUID(claims.Subject) if err == nil { actorID = &acct.ID } } s.vault.Seal() ip := middleware.ClientIP(r, nil) s.writeAudit(r, model.EventVaultSealed, actorID, nil, audit.JSON("source", "api", "ip", ip)) s.logger.Info("vault sealed via API", "ip", ip) writeJSON(w, http.StatusOK, map[string]string{"status": "sealed"}) } // handleVaultStatus returns the current seal state of the vault. func (s *Server) handleVaultStatus(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]bool{"sealed": s.vault.IsSealed()}) }