8.8 KiB
8.8 KiB
RFC-009: STMF Secure To-Me Form
Status: Draft Author: Snider Created: 2026-01-13 License: EUPL-1.2
Abstract
STMF (Secure To-Me Form) provides asymmetric encryption for web form submissions. It enables end-to-end encrypted form data where only the recipient can decrypt submissions, protecting sensitive data from server compromise.
1. Overview
STMF provides:
- Asymmetric encryption for form data
- X25519 key exchange
- ChaCha20-Poly1305 for payload encryption
- Browser-based encryption via WASM
- HTTP middleware for server-side decryption
2. Cryptographic Primitives
2.1 Key Exchange
X25519 (Curve25519 Diffie-Hellman)
| Parameter | Value |
|---|---|
| Private key | 32 bytes |
| Public key | 32 bytes |
| Shared secret | 32 bytes |
2.2 Encryption
ChaCha20-Poly1305
| Parameter | Value |
|---|---|
| Key | 32 bytes (SHA-256 of shared secret) |
| Nonce | 24 bytes (XChaCha variant) |
| Tag | 16 bytes |
3. Protocol
3.1 Setup (One-time)
Recipient (Server):
1. Generate X25519 keypair
2. Publish public key (embed in page or API)
3. Store private key securely
3.2 Encryption Flow (Browser)
1. Fetch recipient's public key
2. Generate ephemeral X25519 keypair
3. Compute shared secret: X25519(ephemeral_private, recipient_public)
4. Derive encryption key: SHA256(shared_secret)
5. Encrypt form data: ChaCha20-Poly1305(data, key, random_nonce)
6. Send: {ephemeral_public, nonce, ciphertext}
3.3 Decryption Flow (Server)
1. Receive {ephemeral_public, nonce, ciphertext}
2. Compute shared secret: X25519(recipient_private, ephemeral_public)
3. Derive encryption key: SHA256(shared_secret)
4. Decrypt: ChaCha20-Poly1305_Open(ciphertext, key, nonce)
4. Wire Format
4.1 Container (Trix-based)
[Magic: "STMF" (4 bytes)]
[Header: Gob-encoded JSON]
[Payload: ChaCha20-Poly1305 ciphertext]
4.2 Header Structure
{
"version": "1.0",
"algorithm": "x25519-chacha20poly1305",
"ephemeral_pk": "<base64 32-byte ephemeral public key>"
}
4.3 Transmission
- Default form field:
_stmf_payload - Encoding: Base64 string
- Content-Type:
application/x-www-form-urlencodedormultipart/form-data
5. Data Structures
5.1 FormField
type FormField struct {
Name string // Field name
Value string // Base64 for files, plaintext otherwise
Type string // "text", "password", "file"
Filename string // For file uploads
MimeType string // For file uploads
}
5.2 FormData
type FormData struct {
Fields []FormField // Array of form fields
Metadata map[string]string // Arbitrary key-value metadata
}
5.3 Builder Pattern
formData := NewFormData().
AddField("email", "user@example.com").
AddFieldWithType("password", "secret", "password").
AddFile("document", base64Content, "report.pdf", "application/pdf").
SetMetadata("timestamp", time.Now().String())
6. Key Management API
6.1 Key Generation
// pkg/stmf/keypair.go
func GenerateKeyPair() (*KeyPair, error)
type KeyPair struct {
privateKey *ecdh.PrivateKey
publicKey *ecdh.PublicKey
}
6.2 Key Loading
// From raw bytes
func LoadPublicKey(data []byte) (*ecdh.PublicKey, error)
func LoadPrivateKey(data []byte) (*ecdh.PrivateKey, error)
// From base64
func LoadPublicKeyBase64(encoded string) (*ecdh.PublicKey, error)
func LoadPrivateKeyBase64(encoded string) (*ecdh.PrivateKey, error)
// Reconstruct keypair from private key
func LoadKeyPair(privateKeyBytes []byte) (*KeyPair, error)
6.3 Key Export
func (kp *KeyPair) PublicKey() []byte // Raw 32 bytes
func (kp *KeyPair) PrivateKey() []byte // Raw 32 bytes
func (kp *KeyPair) PublicKeyBase64() string // Base64 encoded
func (kp *KeyPair) PrivateKeyBase64() string // Base64 encoded
7. WASM API
7.1 BorgSTMF Namespace
// Generate X25519 keypair
const keypair = await BorgSTMF.generateKeyPair();
// keypair.publicKey: base64 string
// keypair.privateKey: base64 string
// Encrypt form data
const encrypted = await BorgSTMF.encrypt(
JSON.stringify(formData),
serverPublicKeyBase64
);
// Encrypt with field-level control
const encrypted = await BorgSTMF.encryptFields(
{email: "user@example.com", password: "secret"},
serverPublicKeyBase64,
{timestamp: Date.now().toString()} // Optional metadata
);
8. HTTP Middleware
8.1 Simple Usage
import "github.com/Snider/Borg/pkg/stmf/middleware"
// Create middleware with private key
mw := middleware.Simple(privateKeyBytes)
// Or from base64
mw, err := middleware.SimpleBase64(privateKeyB64)
// Apply to handler
http.Handle("/submit", mw(myHandler))
8.2 Advanced Configuration
cfg := middleware.DefaultConfig(privateKeyBytes)
cfg.FieldName = "_custom_field" // Custom field name (default: _stmf_payload)
cfg.PopulateForm = &true // Auto-populate r.Form
cfg.OnError = customErrorHandler // Custom error handling
cfg.OnMissingPayload = customHandler // When field is absent
mw := middleware.Middleware(cfg)
8.3 Context Access
func myHandler(w http.ResponseWriter, r *http.Request) {
// Get decrypted form data
formData := middleware.GetFormData(r)
// Get metadata
metadata := middleware.GetMetadata(r)
// Access fields
email := formData.Get("email")
password := formData.Get("password")
}
8.4 Middleware Behavior
- Handles POST, PUT, PATCH requests only
- Parses multipart/form-data (32 MB limit) or application/x-www-form-urlencoded
- Looks for field
_stmf_payload(configurable) - Base64 decodes, then decrypts
- Populates
r.Formandr.PostFormwith decrypted fields - Returns 400 Bad Request on decryption failure
9. Integration Example
9.1 HTML Form
<form id="secure-form" data-stmf-pubkey="<base64-public-key>">
<input name="name" type="text">
<input name="email" type="email">
<input name="ssn" type="password">
<button type="submit">Send Securely</button>
</form>
<script>
document.getElementById('secure-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const pubkey = form.dataset.stmfPubkey;
const formData = new FormData(form);
const data = Object.fromEntries(formData);
const encrypted = await BorgSTMF.encrypt(JSON.stringify(data), pubkey);
await fetch('/api/submit', {
method: 'POST',
body: new URLSearchParams({_stmf_payload: encrypted}),
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
});
</script>
9.2 Server Handler
func main() {
privateKey, _ := os.ReadFile("private.key")
mw := middleware.Simple(privateKey)
http.Handle("/api/submit", mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
formData := middleware.GetFormData(r)
name := formData.Get("name")
email := formData.Get("email")
ssn := formData.Get("ssn")
// Process securely...
w.WriteHeader(http.StatusOK)
})))
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
}
10. Security Properties
10.1 Forward Secrecy
- Fresh ephemeral keypair per encryption
- Compromised private key doesn't decrypt past messages
- Each ciphertext has unique shared secret
10.2 Authenticity
- Poly1305 MAC prevents tampering
- Decryption fails if ciphertext modified
10.3 Confidentiality
- ChaCha20 provides 256-bit security
- Nonces are random (24 bytes), collision unlikely
- Data encrypted before leaving browser
10.4 Key Isolation
- Private key never exposed to browser/JavaScript
- Public key can be safely distributed
- Ephemeral keys discarded after encryption
11. Error Handling
var (
ErrInvalidMagic = errors.New("invalid STMF magic")
ErrInvalidPayload = errors.New("invalid STMF payload")
ErrDecryptionFailed = errors.New("decryption failed")
ErrInvalidPublicKey = errors.New("invalid public key")
ErrInvalidPrivateKey = errors.New("invalid private key")
ErrKeyGenerationFailed = errors.New("key generation failed")
)
12. Implementation Reference
- Types:
pkg/stmf/types.go - Key management:
pkg/stmf/keypair.go - Encryption:
pkg/stmf/encrypt.go - Decryption:
pkg/stmf/decrypt.go - Middleware:
pkg/stmf/middleware/http.go - WASM:
pkg/wasm/stmf/main.go
13. Security Considerations
- Public key authenticity: Verify public key source (HTTPS, pinning)
- Private key protection: Never expose to browser, store securely
- Nonce uniqueness: Random generation ensures uniqueness
- HTTPS required: Transport layer must be encrypted
14. Future Work
- Multiple recipients
- Key attestation
- Offline decryption app
- Hardware key support (WebAuthn)
- Key rotation support