- 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.
66 lines
2.3 KiB
Go
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
|
|
}
|