// Package ui provides the HTMX-based management web interface for MCIAS. package ui import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" ) // CSRFManager implements HMAC-signed Double-Submit Cookie CSRF protection. // // Security design: // - The CSRF key is derived from the server master key via SHA-256 with a // domain-separation prefix, so it is unique to the UI CSRF function. // - The cookie value is 32 bytes of cryptographic random (non-HttpOnly so // HTMX can read it via JavaScript-free double-submit; SameSite=Strict // provides the primary CSRF defence for browser-initiated requests). // - The form/header value is HMAC-SHA256(key, cookieVal); this is what the // server verifies. An attacker cannot forge the HMAC without the key. // - Comparison uses crypto/subtle.ConstantTimeCompare to prevent timing attacks. type CSRFManager struct { key []byte } // newCSRFManager creates a CSRFManager whose key is derived from masterKey. // Key derivation: SHA-256("mcias-ui-csrf-v1" || masterKey) func newCSRFManager(masterKey []byte) *CSRFManager { h := sha256.New() h.Write([]byte("mcias-ui-csrf-v1")) h.Write(masterKey) return &CSRFManager{key: h.Sum(nil)} } // NewToken generates a fresh CSRF token pair. // // Returns: // - cookieVal: hex(32 random bytes) — stored in the mcias_csrf cookie // - headerVal: hex(HMAC-SHA256(key, cookieVal)) — embedded in forms / X-CSRF-Token header func (c *CSRFManager) NewToken() (cookieVal, headerVal string, err error) { raw := make([]byte, 32) if _, err = rand.Read(raw); err != nil { return "", "", fmt.Errorf("csrf: generate random bytes: %w", err) } cookieVal = hex.EncodeToString(raw) mac := hmac.New(sha256.New, c.key) mac.Write([]byte(cookieVal)) headerVal = hex.EncodeToString(mac.Sum(nil)) return cookieVal, headerVal, nil } // Validate verifies that headerVal is the correct HMAC of cookieVal. // Returns false on any mismatch or decoding error. func (c *CSRFManager) Validate(cookieVal, headerVal string) bool { if cookieVal == "" || headerVal == "" { return false } mac := hmac.New(sha256.New, c.key) mac.Write([]byte(cookieVal)) expected := hex.EncodeToString(mac.Sum(nil)) // Security: constant-time comparison prevents timing oracle attacks. return subtle.ConstantTimeCompare([]byte(expected), []byte(headerVal)) == 1 }