Borg/js/borg-stmf/index.html

557 lines
18 KiB
HTML
Raw Normal View History

<!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>
<a href="media-player.html">Media Player</a>
<a href="artist-portal.html">Artist Portal</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>