Files
mcias/internal/ui/csrf.go
Kyle Isom a80242ae3e Add HTMX-based UI templates and handlers for account and audit management
- Introduced `web/templates/` for HTMX-fragmented pages (`dashboard`, `accounts`, `account_detail`, `error_fragment`, etc.).
- Implemented UI routes for account CRUD, audit log display, and login/logout with CSRF protection.
- Added `internal/ui/` package for handlers, CSRF manager, session validation, and token issuance.
- Updated documentation to include new UI features and templates directory structure.
- Security: Double-submit CSRF cookies, constant-time HMAC validation, login password/Argon2id re-verification at all steps to prevent bypass.
2026-03-11 18:02:53 -07:00

66 lines
2.3 KiB
Go

// 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
}