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>
118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package stmf
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/Snider/Enchantrix/pkg/enchantrix"
|
|
"github.com/Snider/Enchantrix/pkg/trix"
|
|
)
|
|
|
|
// Encrypt encrypts form data using the server's public key.
|
|
// It performs X25519 ECDH key exchange with an ephemeral keypair,
|
|
// derives a symmetric key, and encrypts with ChaCha20-Poly1305.
|
|
//
|
|
// The result is a STMF container that can be base64-encoded for transmission.
|
|
func Encrypt(data *FormData, serverPublicKey []byte) ([]byte, error) {
|
|
// Load server's public key
|
|
serverPub, err := LoadPublicKey(serverPublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return EncryptWithKey(data, serverPub)
|
|
}
|
|
|
|
// EncryptBase64 encrypts form data and returns a base64-encoded string
|
|
func EncryptBase64(data *FormData, serverPublicKey []byte) (string, error) {
|
|
encrypted, err := Encrypt(data, serverPublicKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
}
|
|
|
|
// EncryptWithKey encrypts form data using a pre-loaded public key
|
|
func EncryptWithKey(data *FormData, serverPublicKey *ecdh.PublicKey) ([]byte, error) {
|
|
// Generate ephemeral keypair for this encryption
|
|
ephemeralPrivate, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate ephemeral key: %w", err)
|
|
}
|
|
ephemeralPublic := ephemeralPrivate.PublicKey()
|
|
|
|
// Perform ECDH key exchange
|
|
sharedSecret, err := ephemeralPrivate.ECDH(serverPublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ECDH failed: %w", err)
|
|
}
|
|
|
|
// Derive symmetric key using SHA-256 (same pattern as pkg/trix)
|
|
symmetricKey := sha256.Sum256(sharedSecret)
|
|
|
|
// Create ChaCha20-Poly1305 sigil
|
|
sigil, err := enchantrix.NewChaChaPolySigil(symmetricKey[:])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create sigil: %w", err)
|
|
}
|
|
|
|
// Serialize form data to JSON
|
|
payload, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal form data: %w", err)
|
|
}
|
|
|
|
// Encrypt the payload
|
|
encrypted, err := sigil.In(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encryption failed: %w", err)
|
|
}
|
|
|
|
// Build STMF container
|
|
// The nonce is included in the encrypted data by ChaChaPolySigil,
|
|
// but we include the ephemeral public key in the header
|
|
header := Header{
|
|
Version: Version,
|
|
Algorithm: "x25519-chacha20poly1305",
|
|
EphemeralPK: base64.StdEncoding.EncodeToString(ephemeralPublic.Bytes()),
|
|
Nonce: "", // Nonce is embedded in ciphertext by Enchantrix
|
|
}
|
|
|
|
// Convert header to map for trix
|
|
headerMap := map[string]interface{}{
|
|
"version": header.Version,
|
|
"algorithm": header.Algorithm,
|
|
"ephemeral_pk": header.EphemeralPK,
|
|
}
|
|
|
|
// Create trix container
|
|
t := &trix.Trix{
|
|
Header: headerMap,
|
|
Payload: encrypted,
|
|
}
|
|
|
|
// Encode with STMF magic
|
|
return trix.Encode(t, Magic, nil)
|
|
}
|
|
|
|
// EncryptMap is a convenience function to encrypt a simple key-value map
|
|
func EncryptMap(fields map[string]string, serverPublicKey []byte) ([]byte, error) {
|
|
data := NewFormData()
|
|
for name, value := range fields {
|
|
data.AddField(name, value)
|
|
}
|
|
return Encrypt(data, serverPublicKey)
|
|
}
|
|
|
|
// EncryptMapBase64 encrypts a map and returns base64
|
|
func EncryptMapBase64(fields map[string]string, serverPublicKey []byte) (string, error) {
|
|
encrypted, err := EncryptMap(fields, serverPublicKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(encrypted), nil
|
|
}
|