Borg/pkg/stmf/middleware/http.go
Snider b3755da69d feat: Add STMF form encryption and SMSG secure message packages
STMF (Sovereign Form Encryption):
- X25519 ECDH + ChaCha20-Poly1305 hybrid encryption
- Go library (pkg/stmf/) with encrypt/decrypt and HTTP middleware
- WASM module for client-side browser encryption
- JavaScript wrapper with TypeScript types (js/borg-stmf/)
- PHP library for server-side decryption (php/borg-stmf/)
- Full cross-platform interoperability (Go <-> PHP)

SMSG (Secure Message):
- Password-based ChaCha20-Poly1305 message encryption
- Support for attachments, metadata, and PKI reply keys
- WASM bindings for browser-based decryption

Demos:
- index.html: Form encryption demo with modern dark UI
- support-reply.html: Decrypt password-protected messages
- examples/smsg-reply/: CLI tool for creating encrypted replies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 00:49:07 +00:00

192 lines
5.1 KiB
Go

// Package middleware provides HTTP middleware for automatic STMF decryption.
package middleware
import (
"context"
"encoding/base64"
"net/http"
"net/url"
"github.com/Snider/Borg/pkg/stmf"
)
// contextKey is a custom type for context keys to avoid collisions
type contextKey string
const (
// FormDataKey is the context key for the decrypted FormData
FormDataKey contextKey = "stmf_form_data"
// MetadataKey is the context key for the form metadata
MetadataKey contextKey = "stmf_metadata"
)
// Config holds the middleware configuration
type Config struct {
// PrivateKey is the server's X25519 private key (32 bytes)
PrivateKey []byte
// FieldName is the form field name containing the STMF payload
// Defaults to "_stmf_payload" if empty
FieldName string
// OnError is called when decryption fails
// If nil, returns 400 Bad Request
OnError func(w http.ResponseWriter, r *http.Request, err error)
// OnMissingPayload is called when the STMF field is not present
// If nil, the request passes through unchanged
OnMissingPayload func(w http.ResponseWriter, r *http.Request)
// PopulateForm controls whether decrypted fields are added to r.Form
// Defaults to true
PopulateForm *bool
}
// DefaultConfig returns a Config with default values
func DefaultConfig(privateKey []byte) Config {
populateForm := true
return Config{
PrivateKey: privateKey,
FieldName: stmf.DefaultFieldName,
PopulateForm: &populateForm,
}
}
// Middleware creates an HTTP middleware that decrypts STMF payloads.
// It looks for the STMF payload in the configured field name,
// decrypts it, and populates r.Form with the decrypted fields.
func Middleware(cfg Config) func(http.Handler) http.Handler {
if cfg.FieldName == "" {
cfg.FieldName = stmf.DefaultFieldName
}
if cfg.PopulateForm == nil {
populateForm := true
cfg.PopulateForm = &populateForm
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only process POST/PUT/PATCH requests
if r.Method != http.MethodPost && r.Method != http.MethodPut && r.Method != http.MethodPatch {
next.ServeHTTP(w, r)
return
}
// Parse the form
if err := r.ParseMultipartForm(32 << 20); err != nil {
// Try regular form parsing
if err := r.ParseForm(); err != nil {
next.ServeHTTP(w, r)
return
}
}
// Look for STMF payload
payloadB64 := r.FormValue(cfg.FieldName)
if payloadB64 == "" {
if cfg.OnMissingPayload != nil {
cfg.OnMissingPayload(w, r)
return
}
next.ServeHTTP(w, r)
return
}
// Decode base64
payloadBytes, err := base64.StdEncoding.DecodeString(payloadB64)
if err != nil {
handleError(w, r, cfg, stmf.ErrInvalidPayload)
return
}
// Decrypt
formData, err := stmf.Decrypt(payloadBytes, cfg.PrivateKey)
if err != nil {
handleError(w, r, cfg, err)
return
}
// Store in context
ctx := r.Context()
ctx = context.WithValue(ctx, FormDataKey, formData)
if formData.Metadata != nil {
ctx = context.WithValue(ctx, MetadataKey, formData.Metadata)
}
// Populate r.Form with decrypted fields
if *cfg.PopulateForm {
if r.Form == nil {
r.Form = make(url.Values)
}
for _, field := range formData.Fields {
r.Form.Set(field.Name, field.Value)
}
// Also populate PostForm
if r.PostForm == nil {
r.PostForm = make(url.Values)
}
for _, field := range formData.Fields {
r.PostForm.Set(field.Name, field.Value)
}
}
// Remove the encrypted payload field
if r.Form != nil {
delete(r.Form, cfg.FieldName)
}
if r.PostForm != nil {
delete(r.PostForm, cfg.FieldName)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// handleError calls the error handler or returns 400
func handleError(w http.ResponseWriter, r *http.Request, cfg Config, err error) {
if cfg.OnError != nil {
cfg.OnError(w, r, err)
return
}
http.Error(w, "Invalid encrypted payload", http.StatusBadRequest)
}
// Simple creates a simple middleware with just a private key
func Simple(privateKey []byte) func(http.Handler) http.Handler {
return Middleware(DefaultConfig(privateKey))
}
// SimpleBase64 creates a simple middleware with a base64-encoded private key
func SimpleBase64(privateKeyB64 string) (func(http.Handler) http.Handler, error) {
keyBytes, err := base64.StdEncoding.DecodeString(privateKeyB64)
if err != nil {
return nil, err
}
return Simple(keyBytes), nil
}
// GetFormData retrieves the decrypted FormData from the request context
func GetFormData(r *http.Request) *stmf.FormData {
if fd, ok := r.Context().Value(FormDataKey).(*stmf.FormData); ok {
return fd
}
return nil
}
// GetMetadata retrieves the form metadata from the request context
func GetMetadata(r *http.Request) map[string]string {
if md, ok := r.Context().Value(MetadataKey).(map[string]string); ok {
return md
}
return nil
}
// HasSTMFPayload checks if the request contains a STMF payload
func HasSTMFPayload(r *http.Request, fieldName string) bool {
if fieldName == "" {
fieldName = stmf.DefaultFieldName
}
return r.FormValue(fieldName) != ""
}