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>
191 lines
5.1 KiB
Go
191 lines
5.1 KiB
Go
// Example: Creating encrypted support reply messages
|
|
//
|
|
// This example demonstrates how to create password-protected secure messages
|
|
// that can be decrypted client-side using the BorgSMSG WASM module.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run main.go
|
|
// go run main.go -password "secret123" -body "Your message here"
|
|
// go run main.go -password "secret123" -body "Message" -hint "Your hint"
|
|
// go run main.go -password "secret123" -body "Message" -attach file.txt
|
|
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/Snider/Borg/pkg/smsg"
|
|
"github.com/Snider/Borg/pkg/stmf"
|
|
)
|
|
|
|
func main() {
|
|
// Command line flags
|
|
password := flag.String("password", "demo123", "Password for encryption")
|
|
hint := flag.String("hint", "", "Optional password hint")
|
|
body := flag.String("body", "", "Message body (if empty, uses demo content)")
|
|
subject := flag.String("subject", "", "Message subject")
|
|
from := flag.String("from", "support@example.com", "Sender address")
|
|
attachFile := flag.String("attach", "", "File to attach (optional)")
|
|
withReplyKey := flag.Bool("reply-key", false, "Include a reply public key")
|
|
outputFile := flag.String("out", "", "Output file (if empty, prints to stdout)")
|
|
rawBytes := flag.Bool("raw", false, "Output raw bytes instead of base64")
|
|
|
|
flag.Parse()
|
|
|
|
// Create the message
|
|
var msg *smsg.Message
|
|
if *body == "" {
|
|
msg = createDemoMessage()
|
|
} else {
|
|
msg = smsg.NewMessage(*body)
|
|
}
|
|
|
|
// Set optional fields
|
|
if *subject != "" {
|
|
msg.WithSubject(*subject)
|
|
}
|
|
if *from != "" {
|
|
msg.WithFrom(*from)
|
|
}
|
|
|
|
// Add attachment if specified
|
|
if *attachFile != "" {
|
|
if err := addAttachment(msg, *attachFile); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error adding attachment: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// Add reply key if requested
|
|
if *withReplyKey {
|
|
kp, err := stmf.GenerateKeyPair()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error generating reply key: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
msg.WithReplyKey(kp.PublicKeyBase64())
|
|
fmt.Fprintf(os.Stderr, "Reply private key (keep secret): %s\n", kp.PrivateKeyBase64())
|
|
}
|
|
|
|
// Encrypt the message
|
|
var encrypted []byte
|
|
var err error
|
|
if *hint != "" {
|
|
encrypted, err = smsg.EncryptWithHint(msg, *password, *hint)
|
|
} else {
|
|
encrypted, err = smsg.Encrypt(msg, *password)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Encryption failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Output the result
|
|
var output []byte
|
|
if *rawBytes {
|
|
output = encrypted
|
|
} else {
|
|
output = []byte(base64.StdEncoding.EncodeToString(encrypted))
|
|
}
|
|
|
|
if *outputFile != "" {
|
|
if err := os.WriteFile(*outputFile, output, 0644); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "Encrypted message written to: %s\n", *outputFile)
|
|
} else {
|
|
fmt.Println(string(output))
|
|
}
|
|
|
|
// Print info
|
|
fmt.Fprintln(os.Stderr)
|
|
fmt.Fprintln(os.Stderr, "--- Message Info ---")
|
|
fmt.Fprintf(os.Stderr, "Password: %s\n", *password)
|
|
if *hint != "" {
|
|
fmt.Fprintf(os.Stderr, "Hint: %s\n", *hint)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "From: %s\n", msg.From)
|
|
if msg.Subject != "" {
|
|
fmt.Fprintf(os.Stderr, "Subject: %s\n", msg.Subject)
|
|
}
|
|
if len(msg.Attachments) > 0 {
|
|
fmt.Fprintf(os.Stderr, "Attachments: %d\n", len(msg.Attachments))
|
|
}
|
|
if msg.ReplyKey != nil {
|
|
fmt.Fprintln(os.Stderr, "Reply Key: included")
|
|
}
|
|
}
|
|
|
|
// createDemoMessage creates a sample support reply message
|
|
func createDemoMessage() *smsg.Message {
|
|
return smsg.NewMessage(`Hello,
|
|
|
|
Thank you for contacting our support team. We have reviewed your request and are pleased to provide the following update.
|
|
|
|
Your account has been verified and all services are now active. If you have any further questions, please don't hesitate to reach out.
|
|
|
|
Best regards,
|
|
The Support Team`).
|
|
WithSubject("Re: Your Support Request #" + fmt.Sprintf("%d", time.Now().Unix()%100000)).
|
|
WithFrom("support@example.com").
|
|
SetMeta("ticket_id", fmt.Sprintf("%d", time.Now().Unix()%100000)).
|
|
SetMeta("priority", "normal")
|
|
}
|
|
|
|
// addAttachment reads a file and adds it as an attachment
|
|
func addAttachment(msg *smsg.Message, filePath string) error {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading file: %w", err)
|
|
}
|
|
|
|
name := filepath.Base(filePath)
|
|
content := base64.StdEncoding.EncodeToString(data)
|
|
mimeType := detectMimeType(filePath)
|
|
|
|
msg.AddAttachment(name, content, mimeType)
|
|
return nil
|
|
}
|
|
|
|
// detectMimeType returns a basic mime type based on file extension
|
|
func detectMimeType(path string) string {
|
|
ext := filepath.Ext(path)
|
|
switch ext {
|
|
case ".txt":
|
|
return "text/plain"
|
|
case ".html", ".htm":
|
|
return "text/html"
|
|
case ".css":
|
|
return "text/css"
|
|
case ".js":
|
|
return "application/javascript"
|
|
case ".json":
|
|
return "application/json"
|
|
case ".xml":
|
|
return "application/xml"
|
|
case ".pdf":
|
|
return "application/pdf"
|
|
case ".png":
|
|
return "image/png"
|
|
case ".jpg", ".jpeg":
|
|
return "image/jpeg"
|
|
case ".gif":
|
|
return "image/gif"
|
|
case ".svg":
|
|
return "image/svg+xml"
|
|
case ".zip":
|
|
return "application/zip"
|
|
case ".tar":
|
|
return "application/x-tar"
|
|
case ".gz":
|
|
return "application/gzip"
|
|
default:
|
|
return "application/octet-stream"
|
|
}
|
|
}
|