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>
554 lines
18 KiB
HTML
554 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>STMF - Sovereign Form Encryption</title>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||
min-height: 100vh;
|
||
padding: 2rem;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 1.8rem;
|
||
background: linear-gradient(90deg, #00d9ff, #00ff94);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.subtitle {
|
||
text-align: center;
|
||
color: #888;
|
||
margin-bottom: 2rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.card {
|
||
background: rgba(255,255,255,0.05);
|
||
border-radius: 16px;
|
||
padding: 2rem;
|
||
margin-bottom: 1.5rem;
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.card h2 {
|
||
font-size: 1.1rem;
|
||
margin-bottom: 0.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.card h2 .icon {
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.card p.description {
|
||
font-size: 0.85rem;
|
||
color: #888;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.input-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
color: #aaa;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
textarea, input[type="text"], input[type="email"], input[type="password"] {
|
||
width: 100%;
|
||
padding: 0.8rem 1rem;
|
||
border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 8px;
|
||
background: rgba(0,0,0,0.3);
|
||
color: #fff;
|
||
font-family: 'Monaco', 'Menlo', monospace;
|
||
font-size: 0.85rem;
|
||
resize: vertical;
|
||
}
|
||
|
||
textarea:focus, input:focus {
|
||
outline: none;
|
||
border-color: #00d9ff;
|
||
box-shadow: 0 0 0 3px rgba(0, 217, 255, 0.1);
|
||
}
|
||
|
||
input[readonly] {
|
||
background: rgba(0,0,0,0.5);
|
||
color: #00ff94;
|
||
cursor: default;
|
||
}
|
||
|
||
button {
|
||
padding: 0.8rem 2rem;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
button.primary {
|
||
background: linear-gradient(135deg, #00d9ff 0%, #00ff94 100%);
|
||
color: #000;
|
||
}
|
||
|
||
button.primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(0, 217, 255, 0.4);
|
||
}
|
||
|
||
button.primary:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
button.secondary {
|
||
background: rgba(255,255,255,0.1);
|
||
color: #fff;
|
||
border: 1px solid rgba(255,255,255,0.2);
|
||
}
|
||
|
||
button.secondary:hover {
|
||
background: rgba(255,255,255,0.15);
|
||
}
|
||
|
||
button.full-width {
|
||
width: 100%;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.key-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.key-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.status-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.85rem;
|
||
padding: 0.5rem 0;
|
||
}
|
||
|
||
.status-indicator .dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.status-indicator.loading .dot {
|
||
background: #ffc107;
|
||
animation: pulse 1s infinite;
|
||
}
|
||
|
||
.status-indicator.ready .dot {
|
||
background: #00ff94;
|
||
}
|
||
|
||
.status-indicator.error .dot {
|
||
background: #ff5252;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
pre {
|
||
background: rgba(0,0,0,0.4);
|
||
padding: 1rem;
|
||
border-radius: 8px;
|
||
overflow-x: auto;
|
||
font-size: 0.75rem;
|
||
word-break: break-all;
|
||
white-space: pre-wrap;
|
||
color: #00ff94;
|
||
font-family: 'Monaco', 'Menlo', monospace;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 1rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.info-item {
|
||
background: rgba(0,0,0,0.2);
|
||
padding: 1rem;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.info-item .value {
|
||
font-size: 1.2rem;
|
||
font-weight: 600;
|
||
color: #00d9ff;
|
||
}
|
||
|
||
.info-item .label {
|
||
font-size: 0.75rem;
|
||
color: #888;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.nav-links {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.nav-links a {
|
||
color: #00d9ff;
|
||
text-decoration: none;
|
||
font-size: 0.85rem;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 20px;
|
||
background: rgba(0, 217, 255, 0.1);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.nav-links a:hover {
|
||
background: rgba(0, 217, 255, 0.2);
|
||
}
|
||
|
||
.nav-links a.active {
|
||
background: rgba(0, 217, 255, 0.3);
|
||
}
|
||
|
||
.warning-banner {
|
||
background: rgba(255, 193, 7, 0.1);
|
||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||
border-radius: 8px;
|
||
padding: 0.8rem 1rem;
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.85rem;
|
||
color: #ffc107;
|
||
}
|
||
|
||
.success-banner {
|
||
background: rgba(0, 255, 148, 0.1);
|
||
border: 1px solid rgba(0, 255, 148, 0.3);
|
||
border-radius: 8px;
|
||
padding: 0.8rem 1rem;
|
||
margin-top: 1rem;
|
||
display: none;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.85rem;
|
||
color: #00ff94;
|
||
}
|
||
|
||
.success-banner.visible {
|
||
display: flex;
|
||
}
|
||
|
||
.copy-btn {
|
||
padding: 0.4rem 0.8rem;
|
||
font-size: 0.8rem;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
@media (max-width: 500px) {
|
||
.form-row {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>Sovereign Form Encryption</h1>
|
||
<p class="subtitle">X25519 ECDH + ChaCha20-Poly1305 client-side encryption</p>
|
||
|
||
<nav class="nav-links">
|
||
<a href="index.html" class="active">Form Encryption</a>
|
||
<a href="support-reply.html">Decrypt Messages</a>
|
||
</nav>
|
||
|
||
<div id="wasm-status" class="status-indicator loading">
|
||
<span class="dot"></span>
|
||
<span>Loading encryption module...</span>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2><span class="icon">🔑</span> Server Keypair</h2>
|
||
<p class="description">In production, generate this server-side and keep the private key secret. Only the public key is shared with clients.</p>
|
||
|
||
<button id="generate-btn" class="secondary" disabled>Generate New Keypair</button>
|
||
|
||
<div class="key-row" style="margin-top: 1rem;">
|
||
<div class="input-group">
|
||
<label>Public Key (share with clients)</label>
|
||
<input type="text" id="publicKey" readonly placeholder="Click generate...">
|
||
</div>
|
||
<div class="input-group">
|
||
<label>Private Key (keep secret!)</label>
|
||
<input type="text" id="privateKey" readonly placeholder="Click generate...">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2><span class="icon">📝</span> Encrypt Form Data</h2>
|
||
<p class="description">Enter form fields to encrypt. Data is encrypted client-side before transmission.</p>
|
||
|
||
<div id="no-key-warning" class="warning-banner">
|
||
<span>⚠️</span>
|
||
<span>Generate a keypair first to enable encryption</span>
|
||
</div>
|
||
|
||
<form id="demoForm">
|
||
<div class="form-row">
|
||
<div class="input-group">
|
||
<label for="email">Email</label>
|
||
<input type="email" id="email" name="email" value="user@example.com" required>
|
||
</div>
|
||
<div class="input-group">
|
||
<label for="password">Password</label>
|
||
<input type="password" id="form-password" name="password" value="supersecret123" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="input-group">
|
||
<label for="message">Message</label>
|
||
<input type="text" id="message" name="message" value="Hello, encrypted world!">
|
||
</div>
|
||
|
||
<button type="submit" id="encrypt-btn" class="primary full-width" disabled>Encrypt Form Data</button>
|
||
</form>
|
||
|
||
<div id="success-banner" class="success-banner">
|
||
<span>✅</span>
|
||
<span>Form encrypted successfully!</span>
|
||
<button class="secondary copy-btn" id="copy-btn">Copy</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="output-card" style="display: none;">
|
||
<h2><span class="icon">🔒</span> Encrypted Output</h2>
|
||
<p class="description">This base64 payload can be safely transmitted. Only the server with the private key can decrypt it.</p>
|
||
|
||
<pre id="encrypted"></pre>
|
||
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<div class="value" id="payload-size">-</div>
|
||
<div class="label">Payload Size</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="value" id="fields-count">-</div>
|
||
<div class="label">Fields Encrypted</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="value" id="algo-type">X25519</div>
|
||
<div class="label">Key Exchange</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="value" id="cipher-type">ChaCha20</div>
|
||
<div class="label">Cipher</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2><span class="icon">ℹ️</span> How It Works</h2>
|
||
<p class="description" style="margin-bottom: 0; line-height: 1.7;">
|
||
<strong>1. Key Exchange:</strong> An ephemeral X25519 keypair is generated for each encryption.<br>
|
||
<strong>2. Shared Secret:</strong> ECDH derives a shared secret using the ephemeral private key and server's public key.<br>
|
||
<strong>3. Encryption:</strong> Form data is encrypted with ChaCha20-Poly1305 using the derived key.<br>
|
||
<strong>4. Payload:</strong> The ephemeral public key is included in the header so the server can decrypt.<br><br>
|
||
Each encryption produces a unique output even for the same data, ensuring forward secrecy.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="wasm_exec.js"></script>
|
||
<script>
|
||
let wasmReady = false;
|
||
|
||
// Update status indicator safely
|
||
function updateStatus(el, status, message) {
|
||
el.className = 'status-indicator ' + status;
|
||
while (el.firstChild) el.removeChild(el.firstChild);
|
||
const dot = document.createElement('span');
|
||
dot.className = 'dot';
|
||
const text = document.createElement('span');
|
||
text.textContent = message;
|
||
el.appendChild(dot);
|
||
el.appendChild(text);
|
||
}
|
||
|
||
// Initialize WASM
|
||
async function initWasm() {
|
||
const statusEl = document.getElementById('wasm-status');
|
||
|
||
try {
|
||
const go = new Go();
|
||
const result = await WebAssembly.instantiateStreaming(
|
||
fetch('stmf.wasm'),
|
||
go.importObject
|
||
);
|
||
go.run(result.instance);
|
||
|
||
// Wait for BorgSTMF to be ready
|
||
await new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => reject(new Error('WASM init timeout')), 5000);
|
||
|
||
if (typeof BorgSTMF !== 'undefined' && BorgSTMF.ready) {
|
||
clearTimeout(timeout);
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
document.addEventListener('borgstmf:ready', () => {
|
||
clearTimeout(timeout);
|
||
resolve();
|
||
});
|
||
});
|
||
|
||
wasmReady = true;
|
||
updateStatus(statusEl, 'ready', 'Encryption module ready (v' + BorgSTMF.version + ')');
|
||
document.getElementById('generate-btn').disabled = false;
|
||
|
||
} catch (err) {
|
||
updateStatus(statusEl, 'error', 'Failed to load: ' + err.message);
|
||
console.error('WASM init error:', err);
|
||
}
|
||
}
|
||
|
||
// Generate keypair
|
||
async function generateKeys() {
|
||
if (!wasmReady) return;
|
||
|
||
try {
|
||
const keypair = await BorgSTMF.generateKeyPair();
|
||
document.getElementById('publicKey').value = keypair.publicKey;
|
||
document.getElementById('privateKey').value = keypair.privateKey;
|
||
|
||
// Enable encryption
|
||
document.getElementById('encrypt-btn').disabled = false;
|
||
document.getElementById('no-key-warning').style.display = 'none';
|
||
} catch (err) {
|
||
alert('Error generating keys: ' + err.message);
|
||
}
|
||
}
|
||
|
||
// Handle form submission
|
||
async function handleFormSubmit(e) {
|
||
e.preventDefault();
|
||
|
||
if (!wasmReady) {
|
||
alert('WASM not loaded yet');
|
||
return;
|
||
}
|
||
|
||
const publicKey = document.getElementById('publicKey').value;
|
||
if (!publicKey) {
|
||
alert('Generate a keypair first!');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Get form data
|
||
const formData = new FormData(e.target);
|
||
const fields = {};
|
||
formData.forEach((value, key) => {
|
||
fields[key] = value;
|
||
});
|
||
|
||
// Encrypt
|
||
const encrypted = await BorgSTMF.encryptFields(
|
||
fields,
|
||
publicKey,
|
||
{ origin: window.location.origin, timestamp: Date.now().toString() }
|
||
);
|
||
|
||
// Show output
|
||
document.getElementById('encrypted').textContent = encrypted;
|
||
document.getElementById('output-card').style.display = 'block';
|
||
document.getElementById('success-banner').classList.add('visible');
|
||
|
||
// Update stats
|
||
const sizeKB = (encrypted.length * 0.75 / 1024).toFixed(2);
|
||
document.getElementById('payload-size').textContent = sizeKB + ' KB';
|
||
document.getElementById('fields-count').textContent = Object.keys(fields).length;
|
||
|
||
// Scroll to output
|
||
document.getElementById('output-card').scrollIntoView({ behavior: 'smooth' });
|
||
|
||
} catch (err) {
|
||
alert('Encryption error: ' + err.message);
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
// Copy to clipboard
|
||
async function copyToClipboard() {
|
||
const encrypted = document.getElementById('encrypted').textContent;
|
||
try {
|
||
await navigator.clipboard.writeText(encrypted);
|
||
const btn = document.getElementById('copy-btn');
|
||
btn.textContent = 'Copied!';
|
||
setTimeout(() => btn.textContent = 'Copy', 2000);
|
||
} catch (err) {
|
||
alert('Failed to copy: ' + err.message);
|
||
}
|
||
}
|
||
|
||
// Event listeners
|
||
document.getElementById('generate-btn').addEventListener('click', generateKeys);
|
||
document.getElementById('demoForm').addEventListener('submit', handleFormSubmit);
|
||
document.getElementById('copy-btn').addEventListener('click', copyToClipboard);
|
||
|
||
// Initialize
|
||
initWasm();
|
||
</script>
|
||
</body>
|
||
</html>
|