diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2026-06-29 00:13:23 +0200 |
|---|---|---|
| committer | Gab <24553253+gabrix73@users.noreply.github.com> | 2026-06-29 00:13:23 +0200 |
| commit | 1d1d06c025be8b16a3535ecceffc26fe303977db (patch) | |
| tree | cc814b265bcaa2711f092f53349cc66068a6256a | |
| parent | 322a17dba0d44200f6c241b18a58761220050aba (diff) | |
| download | m2usenet-and-mail2news-1d1d06c025be8b16a3535ecceffc26fe303977db.tar.gz m2usenet-and-mail2news-1d1d06c025be8b16a3535ecceffc26fe303977db.tar.xz m2usenet-and-mail2news-1d1d06c025be8b16a3535ecceffc26fe303977db.zip | |
Update m2usenet UI and backend; add identicon renderer and nacl libs
- index.php, send.php: updated client UI
- m2usenet.go: backend changes
- identicon.php: new identicon renderer
- nacl.min.js, nacl-util.min.js: vendored client crypto libs
| -rw-r--r-- | identicon.php | 59 | ||||
| -rw-r--r-- | index.php | 720 | ||||
| -rw-r--r-- | m2usenet.go | 476 | ||||
| -rw-r--r-- | nacl-util.min.js | 1 | ||||
| -rw-r--r-- | nacl.min.js | 1 | ||||
| -rw-r--r-- | send.php | 24 |
6 files changed, 1008 insertions, 273 deletions
diff --git a/identicon.php b/identicon.php new file mode 100644 index 0000000..742cdf0 --- /dev/null +++ b/identicon.php @@ -0,0 +1,59 @@ +<?php +// /var/www/m2usenet/identicon.php +// VFACE identicon generator for m2usenet β wraps the ORIGINAL identicons-cli +// engine (Ch1ffr3punk algorithm), the same binary used by identicons.virebent.art. +// Input is PUBLIC only (username|email|ed25519-pubkey); no secret key involved. +// No logging (privacy). + +ini_set('display_errors', 0); +error_reporting(0); +header('Content-Type: application/json'); +header('Referrer-Policy: no-referrer'); +header('X-Content-Type-Options: nosniff'); + +define('IDENTICONS_CLI', '/usr/local/bin/identicons-cli'); + +function fail(string $msg, int $code = 400): void { + http_response_code($code); + echo json_encode(['error' => $msg]); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') fail('POST only', 405); +if (!is_executable(IDENTICONS_CLI)) fail('engine unavailable', 503); + +$username = trim($_POST['username'] ?? ''); +$email = trim($_POST['email'] ?? ''); +$pubkey = trim($_POST['pubkey'] ?? ''); + +// Validation: public fields only. No '|' (identity-string separator) allowed inside. +if ($username === '' || mb_strlen($username) > 64 || strpos($username, '|') !== false) fail('bad username'); +if ($email === '' || mb_strlen($email) > 128 || strpos($email, '|') !== false) fail('bad email'); +if (!preg_match('#^[A-Za-z0-9+/=]{1,128}$#', $pubkey)) fail('bad pubkey'); + +// Identity string = exact format consumed by identicons-cli / identicons.virebent.art +$input = $username . '|' . $email . '|' . $pubkey; + +function identiconB64(string $input, int $size): ?string { + $cmd = IDENTICONS_CLI . ' -input ' . escapeshellarg($input) . + ' -size ' . intval($size) . ' -transparent -format base64 2>/dev/null'; + $out = trim(shell_exec($cmd) ?? ''); + return ($out !== '' && preg_match('#^[A-Za-z0-9+/=]+$#', $out)) ? $out : null; +} + +// Single-line Face: header (no folding) so the base64 stays clean in the +// saved identity JSON and after downstream RFC unfolding. ~222 octets < 998. +function foldFaceHeader(string $base64): string { + return 'Face: ' . $base64; +} + +$face48 = identiconB64($input, 48); +$face256 = identiconB64($input, 256); +if ($face48 === null || $face256 === null) fail('generation failed', 500); + +echo json_encode([ + 'hash' => hash('sha256', $input), + 'face48' => $face48, // raw base64 PNG (48x48) -> Face: header + 'faceHeader' => foldFaceHeader($face48), // ready-to-paste folded Face: header + 'preview' => 'data:image/png;base64,' . $face256, // 256x256 dataurl for display +]); @@ -1,16 +1,15 @@ <?php // /var/www/m2usenet/index.php -// m2usenet v2.1.0 - Hardened Gateway Interface +// m2usenet v2.6.0 - Face Header + Identity Hash v2 -// Start session for CSRF token if (session_status() === PHP_SESSION_NONE) { ini_set('session.cookie_httponly', 1); - ini_set('session.cookie_secure', 1); + ini_set('session.cookie_secure', 0); // .onion non ha TLS ini_set('session.use_strict_mode', 1); + ini_set('session.cookie_samesite', 'Lax'); // Strict puΓ² bloccare su .onion session_start(); } -// Generate CSRF token if (!isset($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } @@ -21,7 +20,7 @@ $csrfToken = $_SESSION['csrf_token']; <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>m2usenet Gateway v2.1</title> + <title>m2usenet Gateway v2.6</title> <style> :root { --background: #f9f9f9; @@ -140,6 +139,12 @@ $csrfToken = $_SESSION['csrf_token']; to { transform: translateX(0); opacity: 1; } } + @keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(156, 39, 176, 0.7); } + 50% { box-shadow: 0 0 0 15px rgba(156, 39, 176, 0); } + 100% { box-shadow: 0 0 0 0 rgba(156, 39, 176, 0); } + } + .notification-icon { font-size: 1.5em; } .notification-close { margin-left: auto; cursor: pointer; font-size: 1.2em; opacity: 0.8; } .notification-close:hover { opacity: 1; } @@ -195,7 +200,7 @@ $csrfToken = $_SESSION['csrf_token']; } .tab-content { - display: none; + display: none !important; padding: 20px; background: var(--card-bg); border: 1px solid var(--border); @@ -205,7 +210,7 @@ $csrfToken = $_SESSION['csrf_token']; } .tab-content.active { - display: block; + display: block !important; } label { @@ -266,6 +271,11 @@ $csrfToken = $_SESSION['csrf_token']; button:hover:not(:disabled) { background: var(--primary-hover); } button:disabled { opacity: 0.6; cursor: not-allowed; } + .btn-secondary { + background: #6c757d; + } + .btn-secondary:hover:not(:disabled) { background: #5a6268; } + .progress-bar { width: 100%; background: var(--progress-bg); @@ -329,9 +339,6 @@ $csrfToken = $_SESSION['csrf_token']; text-align: center; } - .gateway-priority.secondary { background: #6c757d; } - .gateway-priority.fallback { background: #ffc107; color: #333; } - .gateway-address { font-family: monospace; background: var(--input-readonly); @@ -357,21 +364,123 @@ $csrfToken = $_SESSION['csrf_token']; background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); } - .prefill-banner-icon { - font-size: 1.5em; + .prefill-banner-icon { font-size: 1.5em; } + .prefill-banner-text { flex: 1; } + .prefill-banner-text strong { display: block; margin-bottom: 2px; } + .prefill-banner-text small { opacity: 0.9; } + + .keypair-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 15px; } - .prefill-banner-text { + .keypair-actions button { flex: 1; + min-width: 150px; } - .prefill-banner-text strong { - display: block; - margin-bottom: 2px; + .identity-loaded { + background: #d4edda; + border: 1px solid #c3e6cb; + padding: 10px 15px; + border-radius: 4px; + margin-top: 15px; + display: flex; + align-items: center; + gap: 10px; + } + + .dark-theme .identity-loaded { + background: #1e3d1e; + border-color: #2e5d2e; + } + + .identity-loaded-icon { font-size: 1.5em; } + .identity-loaded-text { flex: 1; } + + .identicon-preview { + display: inline-block; + vertical-align: middle; + margin-left: 10px; + } + + .identicon-preview img { + width: 32px; + height: 32px; + image-rendering: pixelated; + border: 1px solid var(--border); + border-radius: 4px; + } + + .identicon-section { + background: var(--card-bg); + border: 1px solid var(--border); + border-left: 4px solid #9c27b0; + padding: 15px; + margin: 15px 0; + border-radius: 0 4px 4px 0; + text-align: center; + } + + .identicon-section h4 { + margin: 0 0 15px 0; + color: #9c27b0; + } + + .identicon-large-preview { + margin: 15px 0; + } + + .identicon-hash { + font-size: 0.8em; + color: #666; + word-break: break-all; + margin: 10px 0; + } + + .identicon-hash code { + background: var(--input-readonly); + padding: 4px 8px; + border-radius: 4px; + display: inline-block; + max-width: 100%; + overflow-x: auto; + } + + .identicon-actions { + display: flex; + gap: 10px; + justify-content: center; + flex-wrap: wrap; + margin: 15px 0; + } + + .identicon-actions button { + margin: 0; + padding: 8px 15px; + font-size: 0.9em; + } + + .identicon-tip { + background: #f3e5f5; + padding: 10px; + border-radius: 4px; + margin-top: 10px; } - .prefill-banner-text small { - opacity: 0.9; + .dark-theme .identicon-tip { + background: #2d1f30; + } + + .dark-theme #saveIdentityWarning { + background: linear-gradient(135deg, #3d3510 0%, #4a4012 100%); + border-color: #856404; + } + + .dark-theme #saveIdentityWarning p { + color: #ffc107; } footer { @@ -408,15 +517,17 @@ $csrfToken = $_SESSION['csrf_token']; .gateway-item { flex-direction: column; align-items: flex-start; gap: 5px; } .gateway-address { width: 100%; } .notification { right: 10px; left: 10px; max-width: none; } + .keypair-actions { flex-direction: column; } + .keypair-actions button { min-width: 100%; } } </style> - <script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js"></script> + <script src="nacl.min.js"></script> + <script src="nacl-util.min.js"></script> </head> <body> <div class="container"> <header> - <h1>m2usenet Gateway v2.1</h1> + <h1>m2usenet Gateway v2.6</h1> <div class="theme-toggle"> <span>π</span> <input type="checkbox" id="themeToggle"> @@ -425,7 +536,6 @@ $csrfToken = $_SESSION['csrf_token']; </div> </header> - <!-- Prefill banner - shown when coming from onion-newsreader --> <div id="prefillBanner" class="prefill-banner" style="display: none;"> <span class="prefill-banner-icon">π</span> <div class="prefill-banner-text"> @@ -439,7 +549,7 @@ $csrfToken = $_SESSION['csrf_token']; <span>1. Generate Hashcash Token</span> </button> <button id="tabBtn2" onclick="showTab('sign')"> - <span>2. Sign Message</span> + <span>2. Sign Message (Identity)</span> </button> <button id="tabBtn3" onclick="showTab('send')"> <span>3. Send Message</span> @@ -447,13 +557,33 @@ $csrfToken = $_SESSION['csrf_token']; </div> <div id="pow" class="tab-content active"> - <h2>Proof-of-Work Token</h2> + <h2>1. Load Identity or Start Fresh</h2> + + <div class="identity-loader" style="background: var(--card-bg); border: 2px dashed var(--primary); border-radius: 8px; padding: 20px; margin-bottom: 20px; text-align: center;"> + <p style="margin: 0 0 15px 0;"><strong>π Have a saved identity?</strong> Load it to auto-fill all fields.</p> + <button id="loadIdentityBtn" class="btn-primary" style="font-size: 1.1em; padding: 12px 30px;">π Load Identity File</button> + <input type="file" id="identityFileInput" accept=".json" style="display:none;"> + <p style="margin: 15px 0 0 0; font-size: 0.9em; color: var(--text); opacity: 0.7;">Or fill the fields below to create a new identity.</p> + </div> + + <div id="identityLoadedPow" class="identity-loaded" style="display:none; margin-bottom: 20px;"> + <span class="identity-loaded-icon">β
</span> + <div class="identity-loaded-text"> + <strong id="loadedIdentityName">Identity Loaded</strong> + <small id="loadedIdentityEmail">email@example.com</small> + </div> + <div class="identicon-preview"> + <img id="identiconPreviewPow" alt="Your identicon" style="width: 48px; height: 48px; image-rendering: pixelated; border-radius: 4px;"> + </div> + </div> + <div class="section-info"> - <p><strong>About m2usenet:</strong> This application is a privacy-focused gateway that sends your messages to Usenet newsgroups via our mail2news gateways on the onion network. No access logs are collected, and your messages are routed through secure gateways.</p> - <p><strong>What is this?</strong> This step generates a "proof-of-work" token (hashcash) that prevents spam by requiring your computer to perform some calculations. This is similar to how cryptocurrencies work - you need to "mine" a valid token before sending a message.</p> - <p><strong>How to use:</strong> Enter your email address, select the difficulty level (higher bits = longer processing time), and click "Generate Token". Your browser will mine a valid token that will be required in the next steps.</p> + <p><strong>About m2usenet:</strong> A privacy-focused gateway that posts to Usenet newsgroups over Tor hidden services. No access logs are kept. To post you must first create — or load — a <strong>VFACE pseudonymous identity</strong>: an Ed25519 keypair whose public key deterministically generates your identicon. Same key → same identicon → same person over time.</p> + <p><strong>What is Proof-of-Work?</strong> This step generates a hashcash token that prevents spam by requiring your computer to perform some calculations.</p> </div> - <label>Email (resource): <input type="email" id="hcEmail" placeholder="your@email.com" required></label> + + <label>Username (your pseudonym): <input type="text" id="fromName" placeholder="YourPseudonym" required></label> + <label>Email (for identity hash): <input type="email" id="hcEmail" placeholder="your@email.onion" required></label> <label>Difficulty (bits): <select id="hcBits"> <option value="16">16 bits (very fast, ~instant - recommended for Tor Browser)</option> @@ -469,30 +599,65 @@ $csrfToken = $_SESSION['csrf_token']; </div> <div id="sign" class="tab-content"> - <h2>Ed25519 Digital Signature</h2> + <h2>2. Sign Your Message</h2> + + <div id="identiconSection" class="identicon-section" style="display:none;"> + <h4>π¨ Your Visual Identity</h4> + <div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;"> + <div class="identicon-large-preview"> + <img id="identiconPreviewLarge" alt="Your identicon" style="width:96px;height:96px;image-rendering:pixelated;border:2px solid var(--border);border-radius:8px;"> + </div> + <div style="flex: 1; min-width: 200px;"> + <p style="margin: 0 0 5px 0;"><strong id="identityUsername">Username</strong></p> + <p style="margin: 0 0 10px 0; font-size: 0.9em; color: var(--text); opacity: 0.8;" id="identityEmail">email@example.com</p> + <p class="identicon-hash" style="margin: 0;">Hash: <code id="identityHashDisplay"></code></p> + </div> + </div> + <div class="identicon-actions" style="margin-top: 15px;"> + <button type="button" id="downloadIdenticonBtn" class="btn-secondary">π₯ Identicon</button> + <button type="button" id="saveIdentityBtn" class="btn-secondary">πΎ Save Identity</button> + </div> + <div id="saveIdentityWarning" style="margin-top: 15px; padding: 12px; background: linear-gradient(135deg, #fff3cd 0%, #ffeeba 100%); border: 1px solid #ffc107; border-radius: 6px; text-align: center;"> + <p style="margin: 0; color: #856404; font-weight: bold;">β οΈ You must save your identity to continue!</p> + <p style="margin: 5px 0 0 0; color: #856404; font-size: 0.9em;">Click "Save Identity" above to download your identity file. This is required before signing and sending messages.</p> + </div> + <p style="margin-top: 15px; text-align: center; font-size: 0.9em;"> + <a href="https://identicons.virebent.art" target="_blank" rel="noopener noreferrer" style="color: #9c27b0; text-decoration: none;">π VFACE Verifier - Verify an identity</a> + </p> + </div> + <div class="section-info"> - <p><strong>What is this?</strong> This step creates a digital signature for your message using the Ed25519 cryptographic algorithm. This signature helps verify that the message was sent by you and hasn't been tampered with.</p> - <p><strong>How to use:</strong> First, generate a key pair (this creates a public and private key). Then, write your message and click "Sign Message". This will create a digital signature that will be attached to your post.</p> + <p><strong>What is this?</strong> Your keypair is generated and your message is signed <strong>locally in your browser</strong> with the self-hosted TweetNaCl (<code>nacl</code>) library — your Ed25519 secret key never leaves your device. Only the public identity (username, email, public key) is sent, to render the identicon. The signature proves you authored this post.</p> + <p><strong>VFACE / Face Header:</strong> Your identicon is produced by the original <code>identicons-cli</code> engine — the same backend as <code>identicons.virebent.art</code> — so it is fully deterministic: the same key always yields the same identicon. It is embedded as a folded <code>Face:</code> header (RFC 4021/2822) alongside <code>X-Ed25519-Pub</code> and <code>X-Ed25519-Sig</code>, visible in newsreaders like Newsgrouper and re-verifiable by any reader from your public key.</p> </div> - <label>Email used for PoW:</label> + + <label>Email (from PoW):</label> <input type="text" id="readonlyEmailSign" readonly> + <label>Message to Sign:</label> <textarea id="messageToSign" rows="6" placeholder="Write your message here..." required></textarea> - <button id="genKeyBtn">Generate Key Pair</button> - <button id="signMsgBtn" disabled>Sign Message</button> - <label>Generated Public Key:</label> + + <div class="keypair-actions"> + <button id="genKeyBtn">π Generate New Keypair</button> + </div> + <input type="file" id="keyFileInput" accept=".json" style="display:none;"> + + <button id="signMsgBtn" disabled>βοΈ Sign Message</button> + + <label>Public Key:</label> <div id="pubKeyOutput" class="output-field empty">Public key will appear here</div> - <label>Generated Signature:</label> + <label>Signature:</label> <div id="signatureOutput" class="output-field empty">Signature will appear here</div> </div> <div id="send" class="tab-content"> <h2>Send Message</h2> <div class="section-info"> - <p><strong>What is this?</strong> This final step sends your signed message to the Usenet network via mail2news gateways. m2usenet v2.1.0 uses Tor for all connections with automatic fallback.</p> + <p><strong>What is this?</strong> This final step sends your signed message to the Usenet network via Tor hidden services.</p> <div class="gateway-info"> <h4>π Full Onion Network Path</h4> <p style="margin-bottom: 15px; font-size: 0.95em;">Your message travels entirely within the Tor network:</p> + <div class="gateway-item"> <span class="gateway-priority">Step 1</span> <div style="flex: 1;"> @@ -503,18 +668,20 @@ $csrfToken = $_SESSION['csrf_token']; <details style="margin-top: 8px; font-size: 0.8em;"> <summary style="cursor: pointer; color: #007bff;">Show relay nodes</summary> <ul style="margin: 8px 0; padding-left: 20px; font-family: monospace; color: #555;"> - <li>4uwpi53u524xdphjw2dv5kywsxmyjxtk4facb76jgl3sc3nda3sz4fqd.onion:25</li> - <li>xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion:25</li> + <li><strong>PRIMARY:</strong> 4uwpi53u524xdphjw2dv5kywsxmyjxtk4facb76jgl3sc3nda3sz4fqd.onion:25</li> + <li><strong>FALLBACK:</strong> xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion:25</li> </ul> </details> + <div class="gateway-item"> <span class="gateway-priority">Step 2</span> <div style="flex: 1;"> <strong>Mail2News Gateway</strong> <div class="gateway-address" style="margin-top: 5px;">mail2news@xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion</div> - <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Converts email to Usenet posts while maintaining full anonymity within the onion network.</p> + <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Converts email to Usenet posts while maintaining full anonymity.</p> </div> </div> + <div class="gateway-item"> <span class="gateway-priority">Step 3</span> <div style="flex: 1;"> @@ -523,17 +690,19 @@ $csrfToken = $_SESSION['csrf_token']; <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Final destination for Usenet Tor hidden service.</p> </div> </div> + <p style="margin-top: 15px; padding: 10px; background: rgba(0,123,255,0.1); border-radius: 4px; font-size: 0.9em;"> - <strong>π Privacy Guarantee:</strong> Your message never leaves the Tor network until it reaches the NNTP server. All three components (Pluto2 β Mail2News β NNTP) operate as Tor hidden services, providing end-to-end anonymity with protection against traffic analysis, timing attacks, and metadata correlation. + <strong>π Privacy Guarantee:</strong> Your message never leaves the Tor network until it reaches the NNTP server. All three components (SMTP β Mail2News β NNTP) operate as Tor hidden services, providing end-to-end anonymity. </p> </div> - <p><strong>How to use:</strong> Complete the form below with your name, newsgroups (max 3), subject, and verify that your message and authentication details are correct. Then click "Send" to post your message via the gateway system.</p> </div> + <label>Email used for PoW:</label> <input type="text" id="readonlyEmailSend" readonly> <form id="sendForm" method="POST" action="send.php"> <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>"> - <label>From (Name): <input type="text" id="fromName" required placeholder="Your Name"></label> + <label>From (Name):</label> + <input type="text" id="readonlyFromName" readonly> <input type="hidden" name="from" id="fromFull"> <label>Newsgroups (max 3, comma separated): <input type="text" name="newsgroups" id="newsgroups" required placeholder="e.g. alt.privacy, comp.security"> @@ -545,12 +714,12 @@ $csrfToken = $_SESSION['csrf_token']; <textarea name="message" id="messageContent" rows="8" required placeholder="Your message will appear here after signing..."></textarea> <input type="hidden" name="x-ed25519-pub" id="x-ed25519-pub"> <input type="hidden" name="x-ed25519-sig" id="x-ed25519-sig"> - <button type="submit" id="sendBtn">Send via m2usenet Gateway</button> + <button type="submit" id="sendBtn">Send Message</button> </form> </div> <footer> - <div>m2usenet Gateway v2.1.0 Β© 2025 - Privacy-focused Usenet posting tool</div> + <div>m2usenet Gateway v2.6.0 Β© 2025 - Privacy-focused Usenet posting via Tor</div> <div class="footer-links"> <a href="https://yamn.virebent.art">Home</a> <a href="mailto:%69%6E%66%6F%40%76%69%72%65%62%65%6E%74%2E%61%72%74">Contact</a> @@ -561,10 +730,15 @@ $csrfToken = $_SESSION['csrf_token']; <script> // ============================================================================ -// DEFINIZIONI FUNZIONI GLOBALI - DEVONO ESSERE PRIMA DI TUTTO +// GLOBAL FUNCTIONS - MUST BE DEFINED FIRST // ============================================================================ function showTab(id) { + // Block access to send tab if identity not saved + if (id === 'send' && !appState.identitySaved) { + showNotification('β Please save your identity first before sending!', 'warning'); + return; + } document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.getElementById(id).classList.add('active'); } @@ -594,6 +768,7 @@ const appState = { step1Complete: false, step2Complete: false, step3Complete: false, + identitySaved: false, prefillMode: null }; @@ -604,16 +779,62 @@ function updateTabIndicators() { } let keyPair = null; + +// --- VFACE identity persistence (session-scoped: secret lives in RAM/sessionStorage, +// auto-cleared when the tab closes; the .json stays the portable on-disk form). +// Lets reply->/compose reuse the active identity without re-loading the file. --- +function persistIdentitySession() { + try { + if (!keyPair) return; + sessionStorage.setItem('m2usenet_identity', JSON.stringify({ + username: document.getElementById('fromName').value || 'Anonymous', + email: document.getElementById('hcEmail').value || '', + publicKey: nacl.util.encodeBase64(keyPair.publicKey), + secretKey: nacl.util.encodeBase64(keyPair.secretKey) + })); + } catch (e) {} +} +function forgetIdentitySession() { + try { sessionStorage.removeItem('m2usenet_identity'); } catch (e) {} +} +async function restoreIdentityFromSession() { + let sd; + try { sd = JSON.parse(sessionStorage.getItem('m2usenet_identity') || 'null'); } catch (e) { return; } + if (!sd || !sd.publicKey || !sd.secretKey || keyPair) return; + try { + const pubKey = nacl.util.decodeBase64(sd.publicKey); + const secKey = nacl.util.decodeBase64(sd.secretKey); + if (pubKey.length !== 32 || secKey.length !== 64) return; + keyPair = { publicKey: pubKey, secretKey: secKey }; + const uname = sd.username || 'Anonymous'; + const mail = sd.email || ''; + const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; }; + set('fromName', uname); set('hcEmail', mail); + const pubOutput = document.getElementById('pubKeyOutput'); + if (pubOutput) { pubOutput.innerText = sd.publicKey; pubOutput.classList.remove('empty'); } + set('x-ed25519-pub', sd.publicKey); + const signBtn = document.getElementById('signMsgBtn'); if (signBtn) signBtn.disabled = false; + set('readonlyFromName', uname); + if (mail) { set('fromFull', uname + ' <' + mail + '>'); set('readonlyEmailSign', mail); set('readonlyEmailSend', mail); } + const il = document.getElementById('identityLoadedPow'); if (il) il.style.display = 'flex'; + const ln = document.getElementById('loadedIdentityName'); if (ln) ln.textContent = uname; + const le = document.getElementById('loadedIdentityEmail'); if (le) le.textContent = mail; + await updateIdenticonPreview(); + const gk = document.getElementById('genKeyBtn'); if (gk) { gk.disabled = true; gk.textContent = 'β Keypair Loaded'; } + appState.identitySaved = true; + const sb = document.getElementById('saveIdentityBtn'); if (sb) { sb.textContent = 'β Identity (session)'; sb.style.background = '#28a745'; } + const warning = document.getElementById('saveIdentityWarning'); if (warning) warning.style.display = 'none'; + showNotification('π Identity restored for this session', 'info', 4000); + } catch (e) { console.error('session restore failed', e); } +} let workersSupported = true; try { if (typeof Worker === 'undefined') { workersSupported = false; - console.warn('[WARN] Web Workers not supported'); } } catch (e) { workersSupported = false; - console.warn('[WARN] Web Workers check failed:', e); } // ============================================================================ @@ -626,6 +847,86 @@ document.getElementById('themeToggle').addEventListener('change', function() { }); // ============================================================================ +// IDENTICON GENERATION (client-side) +// ============================================================================ + +let currentIdenticonData = null; + +async function generateIdenticon(username, email, pubkeyBase64) { + // Generate the identicon with the ORIGINAL identicons-cli engine (server-side), + // the same backend used by identicons.virebent.art (Ch1ffr3punk algorithm). + // Only PUBLIC data leaves the browser (username|email|ed25519-pubkey). + const body = new URLSearchParams({ username: username, email: email, pubkey: pubkeyBase64 }); + const resp = await fetch('identicon.php', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString() + }); + if (!resp.ok) throw new Error('identicon engine error ' + resp.status); + const r = await resp.json(); + if (!r.face48 || !r.preview) throw new Error('identicon engine returned no data'); + + currentIdenticonData = { + dataUrl: r.preview, // 256x256 PNG dataurl (display/download) + hash: r.hash, // sha256(username|email|pubkey) + face48: r.face48, // raw base64 PNG (48x48) -> Face: header + faceHeader: r.faceHeader, // folded "Face: ..." header (RFC 2822) + username: username, + email: email, + pubkey: pubkeyBase64, + claim: JSON.stringify({ + version: 2, + type: 'm2usenet-identity', + username: username, + email: email, + pubkey: pubkeyBase64, + identityHash: r.hash, + created: new Date().toISOString(), + algorithm: 'identicons-cli sha256(username|email|pubkey) [VFACE]' + }, null, 2) + }; + return r.preview; +} + +async function updateIdenticonPreview() { + if (!keyPair) return; + + const pubB64 = nacl.util.encodeBase64(keyPair.publicKey); + const username = document.getElementById('fromName').value || 'Anonymous'; + const email = document.getElementById('hcEmail').value || 'anonymous@m2usenet.local'; + + try { + const identiconUrl = await generateIdenticon(username, email, pubB64); + + document.getElementById('identiconPreviewPow').src = identiconUrl; + document.getElementById('identiconPreviewLarge').src = identiconUrl; + document.getElementById('identityHashDisplay').textContent = currentIdenticonData.hash; + document.getElementById('identityUsername').textContent = username; + document.getElementById('identityEmail').textContent = email; + document.getElementById('identiconSection').style.display = 'block'; + + } catch (e) { + console.error('Identicon generation failed:', e); + } +} + +document.getElementById('downloadIdenticonBtn').onclick = function() { + if (!currentIdenticonData) { + showNotification('β Generate a keypair first!', 'warning'); + return; + } + + const a = document.createElement('a'); + a.href = currentIdenticonData.dataUrl; + a.download = 'identicon-' + currentIdenticonData.hash.substring(0, 8) + '.png'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + showNotification('β Identicon downloaded!', 'success', 3000); +}; + +// ============================================================================ // INITIALIZATION // ============================================================================ @@ -655,8 +956,9 @@ document.addEventListener('DOMContentLoaded', function() { }; } - showNotification('m2usenet Gateway v2.1 ready', 'success', 3000); + restoreIdentityFromSession(); showTab('pow'); + showNotification('m2usenet Gateway v2.6.0 ready', 'success', 3000); }); // ============================================================================ @@ -665,57 +967,32 @@ document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() { const form = document.querySelector('form'); - const submitButton = form.querySelector('button[type="submit"], input[type="submit"]'); + const submitButton = form.querySelector('button[type="submit"]'); - if (!submitButton) { - console.error('Submit button not found'); - return; - } + if (!submitButton) return; let isSubmitting = false; form.addEventListener('submit', function(e) { if (isSubmitting) { e.preventDefault(); - console.log('Form already submitting, preventing duplicate'); return false; } isSubmitting = true; submitButton.disabled = true; - const originalText = submitButton.textContent || submitButton.value; submitButton.textContent = 'Sending...'; - submitButton.value = 'Sending...'; submitButton.style.opacity = '0.6'; submitButton.style.cursor = 'not-allowed'; const progressDiv = document.createElement('div'); progressDiv.id = 'sending-progress'; progressDiv.style.cssText = 'margin-top: 10px; padding: 10px; background: #fff3cd; border-left: 4px solid #ffc107; color: #856404;'; - progressDiv.innerHTML = '<strong>β³ Sending message...</strong><br>This may take 30-60 seconds. Please wait.'; + progressDiv.innerHTML = '<strong>β³ Sending message...</strong><br>This may take up to 60 seconds via Tor. Please wait.'; submitButton.parentNode.insertBefore(progressDiv, submitButton.nextSibling); return true; }); - - form.addEventListener('keypress', function(e) { - if (e.key === 'Enter' && isSubmitting) { - e.preventDefault(); - return false; - } - }); - - window.addEventListener('pageshow', function(event) { - if (event.persisted) { - isSubmitting = false; - submitButton.disabled = false; - submitButton.textContent = originalText; - submitButton.style.opacity = '1'; - submitButton.style.cursor = 'pointer'; - const progressDiv = document.getElementById('sending-progress'); - if (progressDiv) progressDiv.remove(); - } - }); }); // ============================================================================ @@ -794,7 +1071,6 @@ function sha1(str) { } function mineSingleThread(prefix, targetZeros, progressCallback, foundCallback) { - console.log('[INFO] Using single-threaded mining'); const target = '0'.repeat(targetZeros); let nonce = 0, checked = 0; const batchSize = 100; @@ -883,6 +1159,7 @@ document.getElementById('genTokenBtn').onclick = () => { const fromName = document.getElementById('fromName').value || 'Anonymous'; document.getElementById('fromFull').value = `${fromName} <${email}>`; + document.getElementById('readonlyFromName').value = fromName; document.getElementById('readonlyEmailSign').value = email; document.getElementById('readonlyEmailSend').value = email; @@ -896,17 +1173,13 @@ document.getElementById('genTokenBtn').onclick = () => { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); showNotification(`β Token generated in ${elapsed}s!`, 'success', 8000); - console.log(`[INFO] Token mined: ${totalHashes} hashes in ${elapsed}s`); setTimeout(() => showTab('sign'), 2000); } if (workersSupported) { - console.log('[INFO] Attempting multi-threaded mining'); try { const cores = navigator.hardwareConcurrency || 2; - console.log(`[INFO] Using ${cores} workers`); - let found = false; let coreChecked = Array(cores).fill(0); const workers = []; @@ -935,9 +1208,7 @@ document.getElementById('genTokenBtn').onclick = () => { }; w.onerror = (err) => { - console.error(`[ERROR] Worker ${i} error:`, err); if (i === 0) { - console.warn('[WARN] Falling back to single-thread'); workers.forEach(x => x.terminate()); mineSingleThread(prefix, zeros, updateProgress, onFound); } @@ -945,7 +1216,6 @@ document.getElementById('genTokenBtn').onclick = () => { workers.push(w); } catch (err) { - console.error(`[ERROR] Failed to create worker ${i}:`, err); if (i === 0) { mineSingleThread(prefix, zeros, updateProgress, onFound); return; @@ -953,16 +1223,7 @@ document.getElementById('genTokenBtn').onclick = () => { } } - setTimeout(() => { - if (!found) { - console.warn('[WARN] Mining timeout'); - showNotification('β Mining is taking longer than expected.', 'warning', 10000); - } - }, 600000); - } catch (err) { - console.error('[ERROR] Worker mining failed:', err); - showNotification('β Multi-threaded mining failed, using fallback', 'warning'); mineSingleThread(prefix, zeros, updateProgress, onFound); } } else { @@ -971,22 +1232,15 @@ document.getElementById('genTokenBtn').onclick = () => { }; // ============================================================================ -// STEP 2: ED25519 SIGNATURE +// STEP 2: ED25519 SIGNATURE + KEYPAIR MANAGEMENT // ============================================================================ -document.getElementById('genKeyBtn').onclick = function() { +document.getElementById('genKeyBtn').onclick = async function() { try { - if (typeof nacl === 'undefined' || typeof nacl.sign === 'undefined') { - throw new Error("Cryptography library not loaded"); - } - showNotification('π Generating Ed25519 key pair...', 'info'); keyPair = nacl.sign.keyPair(); - const pubKey = keyPair.publicKey; - const pubB64 = typeof nacl.util.encodeBase64 === 'function' - ? nacl.util.encodeBase64(pubKey) - : btoa(Array.from(new Uint8Array(pubKey)).map(byte => String.fromCharCode(byte)).join('')); + const pubB64 = nacl.util.encodeBase64(keyPair.publicKey); const pubOutput = document.getElementById('pubKeyOutput'); pubOutput.innerText = pubB64; @@ -994,17 +1248,168 @@ document.getElementById('genKeyBtn').onclick = function() { document.getElementById('x-ed25519-pub').value = pubB64; document.getElementById('signMsgBtn').disabled = false; - showNotification('β Key pair generated!', 'success', 6000); + const username = document.getElementById('fromName').value || 'Anonymous'; + const email = document.getElementById('hcEmail').value || ''; + document.getElementById('identityLoadedPow').style.display = 'flex'; + document.getElementById('loadedIdentityName').textContent = username; + document.getElementById('loadedIdentityEmail').textContent = email || 'New keypair - save identity!'; + + await updateIdenticonPreview(); + + // Auto-open the save dialog at the end of identity generation: + // saving the .json is mandatory to keep (and later reload) this pseudonym. + document.getElementById('saveIdentityBtn').click(); + + showNotification('β Identity generated & saving β keep the .json to reuse this pseudonym. Now write and sign your message.', 'success', 7000); } catch (error) { - console.error("Error generating key pair:", error); showNotification('β Error: ' + error.message, 'error'); } }; +document.getElementById('loadIdentityBtn').onclick = function() { + document.getElementById('identityFileInput').click(); +}; + +document.getElementById('identityFileInput').onchange = async function(e) { + const file = e.target.files[0]; + if (!file) return; + + try { + const text = await file.text(); + const data = JSON.parse(text); + + if (!data.publicKey || !data.secretKey) { + throw new Error('Invalid identity file format'); + } + + const pubKey = nacl.util.decodeBase64(data.publicKey); + const secKey = nacl.util.decodeBase64(data.secretKey); + + if (pubKey.length !== 32 || secKey.length !== 64) { + throw new Error('Invalid key lengths'); + } + + keyPair = { publicKey: pubKey, secretKey: secKey }; + + const pubB64 = data.publicKey; + const pubOutput = document.getElementById('pubKeyOutput'); + pubOutput.innerText = pubB64; + pubOutput.classList.remove('empty'); + document.getElementById('x-ed25519-pub').value = pubB64; + document.getElementById('signMsgBtn').disabled = false; + + if (data.username) { + document.getElementById('fromName').value = data.username; + document.getElementById('readonlyFromName').value = data.username; + } + if (data.email) { + document.getElementById('hcEmail').value = data.email; + const fromName = document.getElementById('fromName').value || 'Anonymous'; + document.getElementById('fromFull').value = `${fromName} <${data.email}>`; + document.getElementById('readonlyEmailSign').value = data.email; + document.getElementById('readonlyEmailSend').value = data.email; + } + + const username = data.username || 'Anonymous'; + const email = data.email || ''; + document.getElementById('identityLoadedPow').style.display = 'flex'; + document.getElementById('loadedIdentityName').textContent = username; + document.getElementById('loadedIdentityEmail').textContent = email; + + await updateIdenticonPreview(); + + if (currentIdenticonData && currentIdenticonData.dataUrl) { + document.getElementById('identiconPreviewPow').src = currentIdenticonData.dataUrl; + } + + document.getElementById('genKeyBtn').disabled = true; + document.getElementById('genKeyBtn').textContent = 'β Keypair Loaded'; + + // Identity already saved (loaded from file) - allow access to send tab + appState.identitySaved = true; + document.getElementById('saveIdentityBtn').textContent = 'β Identity Loaded'; + document.getElementById('saveIdentityBtn').style.background = '#28a745'; + + // Hide the warning message + const warning = document.getElementById('saveIdentityWarning'); + if (warning) warning.style.display = 'none'; + + persistIdentitySession(); + showNotification('β Identity loaded! Username and email populated. Now generate PoW token.', 'success', 6000); + } catch (error) { + showNotification('β Failed to load identity: ' + error.message, 'error'); + } + + e.target.value = ''; +}; + +document.getElementById('saveIdentityBtn').onclick = async function() { + if (!keyPair) { + showNotification('β Generate a keypair first!', 'warning'); + return; + } + + const username = document.getElementById('fromName').value || 'Anonymous'; + const email = document.getElementById('hcEmail').value || 'anonymous@m2usenet.local'; + const pubB64 = nacl.util.encodeBase64(keyPair.publicKey); + const secB64 = nacl.util.encodeBase64(keyPair.secretKey); + + const identityString = username + '|' + email + '|' + pubB64; + const encoder = new TextEncoder(); + const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(identityString)); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const identityHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + const data = { + version: 2, + type: 'm2usenet-identity', + username: username, + email: email, + publicKey: pubB64, + secretKey: secB64, + identityHash: identityHash, + vface: (currentIdenticonData ? currentIdenticonData.faceHeader : ''), + face: (currentIdenticonData ? currentIdenticonData.face48 : ''), + algorithm: 'identicons-cli sha256(username|email|pubkey) [VFACE]', + created: new Date().toISOString(), + warning: 'Keep this file secure! Anyone with this file can sign messages as you.' + }; + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'm2usenet-identity-' + username + '.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + // Mark identity as saved - allows access to send tab + appState.identitySaved = true; + persistIdentitySession(); + + // Update button to show saved state + this.textContent = 'β Identity Saved'; + this.style.background = '#28a745'; + + // Hide the warning message + const warning = document.getElementById('saveIdentityWarning'); + if (warning) warning.style.display = 'none'; + + // If message already signed, go to send tab + if (appState.step2Complete) { + showNotification('β Identity saved! Proceeding to send...', 'success', 4000); + setTimeout(() => showTab('send'), 1500); + } else { + showNotification('β Identity saved! Now sign your message to continue.', 'success', 6000); + } +}; + document.getElementById('signMsgBtn').onclick = function() { try { if (!keyPair) { - showNotification('β Please generate a key pair first!', 'warning'); + showNotification('β Please generate or load a key pair first!', 'warning'); return; } @@ -1024,15 +1429,9 @@ document.getElementById('signMsgBtn').onclick = function() { document.getElementById('messageToSign').classList.remove('error'); showNotification('β Signing message...', 'info'); - const msgBytes = typeof nacl.util.decodeUTF8 === 'function' - ? nacl.util.decodeUTF8(msg) - : new TextEncoder().encode(msg); - + const msgBytes = nacl.util.decodeUTF8(msg); const sig = nacl.sign.detached(msgBytes, keyPair.secretKey); - - const sigB64 = typeof nacl.util.encodeBase64 === 'function' - ? nacl.util.encodeBase64(sig) - : btoa(Array.from(new Uint8Array(sig)).map(byte => String.fromCharCode(byte)).join('')); + const sigB64 = nacl.util.encodeBase64(sig); const sigOutput = document.getElementById('signatureOutput'); sigOutput.innerText = sigB64; @@ -1044,21 +1443,45 @@ document.getElementById('signMsgBtn').onclick = function() { appState.step2Complete = true; updateTabIndicators(); - showNotification('β Message signed!', 'success', 8000); - setTimeout(() => showTab('send'), 2000); + // After signing, check if identity is saved + if (!appState.identitySaved) { + // Scroll to identicon section and prompt to save + showNotification('β Message signed! Now save your identity to continue.', 'warning', 10000); + setTimeout(() => { + const identiconSection = document.getElementById('identiconSection'); + if (identiconSection) { + identiconSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + identiconSection.style.animation = 'pulse 1s ease-in-out 3'; + } + }, 500); + } else { + // Identity already saved, proceed to send + showNotification('β Message signed with your identity!', 'success', 8000); + setTimeout(() => showTab('send'), 2000); + } } catch (error) { - console.error("Error signing:", error); showNotification('β Error: ' + error.message, 'error'); } }; +document.getElementById('fromName').addEventListener('input', function() { + const email = document.getElementById('hcEmail').value; + document.getElementById('readonlyFromName').value = this.value; + if (email) { + document.getElementById('fromFull').value = `${this.value} <${email}>`; + } + if (keyPair) { + updateIdenticonPreview(); + } +}); + // ============================================================================ // STEP 3: SEND FORM // ============================================================================ document.getElementById('sendForm').addEventListener('submit', function(e) { const requiredFields = [ - {id: 'fromName', name: 'Name'}, + {id: 'fromFull', name: 'From'}, {id: 'newsgroups', name: 'Newsgroups'}, {id: 'subject', name: 'Subject'}, {id: 'hcToken', name: 'Hashcash Token'}, @@ -1104,59 +1527,45 @@ document.getElementById('sendForm').addEventListener('submit', function(e) { return false; } - const confirmMsg = `Send to Usenet?\n\nNewsgroups: ${newsgroups.join(', ')}\nSubject: ${document.getElementById('subject').value}\nLength: ${messageLen} chars\n\nThis cannot be undone.`; + const confirmMsg = `Send message?\n\nNewsgroups: ${newsgroups.join(', ')}\nSubject: ${document.getElementById('subject').value}\nLength: ${messageLen} chars\n\nDelivery via Tor may take up to 60 seconds.`; if (!confirm(confirmMsg)) { e.preventDefault(); return false; } - showNotification('π€ Sending...', 'info', 0); + showNotification('π€ Sending message...', 'info', 0); document.getElementById('sendBtn').disabled = true; document.getElementById('sendBtn').textContent = 'Sending...'; return true; }); -document.getElementById('fromName').addEventListener('input', function() { - const email = document.getElementById('hcEmail').value; - if (email) { - document.getElementById('fromFull').value = `${this.value} <${email}>`; - } -}); - // ============================================================================ -// ONION-NEWSREADER INTEGRATION (localStorage + URL params) - FIXED v2 +// ONION-NEWSREADER INTEGRATION // ============================================================================ (function() { - // Helper function to decode URL-encoded values function decode(s) { if (!s) return ''; try { - // Replace + with space, then decode URI components return decodeURIComponent(s.replace(/\+/g, ' ')); } catch(e) { - console.error('[PREFILL] Decode error:', e); return s; } } - // Helper function to fill and optionally style a field function fillField(id, value, highlight) { highlight = highlight !== false; const field = document.getElementById(id); if (field && value) { field.value = value; - if (highlight) { - field.classList.add('prefilled'); - } + if (highlight) field.classList.add('prefilled'); return true; } return false; } - // Show the prefill banner function showPrefillBanner(isReply, details) { const banner = document.getElementById('prefillBanner'); const title = document.getElementById('prefillTitle'); @@ -1177,78 +1586,62 @@ document.getElementById('fromName').addEventListener('input', function() { let prefillData = null; let prefillSource = null; - // 1. Try localStorage first (from onion-newsreader) try { const storedData = localStorage.getItem('m2usenet_prefill'); if (storedData) { prefillData = JSON.parse(storedData); prefillSource = 'localStorage'; - // Clear after reading localStorage.removeItem('m2usenet_prefill'); - console.log('[PREFILL] Raw data from localStorage:', prefillData); - // Decode values in case they were stored encoded if (prefillData.newsgroups) prefillData.newsgroups = decode(prefillData.newsgroups); if (prefillData.subject) prefillData.subject = decode(prefillData.subject); if (prefillData.references) prefillData.references = decode(prefillData.references); - - console.log('[PREFILL] Decoded data from localStorage:', prefillData); } } catch (e) { - console.error('[PREFILL] localStorage error:', e); localStorage.removeItem('m2usenet_prefill'); } - // 2. Fallback to URL query parameters if (!prefillData) { const params = new URLSearchParams(window.location.search); const action = params.get('action'); if (action === 'new' || action === 'reply' || params.has('newsgroups')) { - // Get raw values and decode them explicitly - const rawNG = params.get('newsgroups') || ''; - const rawSubj = params.get('subject') || ''; - const rawRefs = params.get('references') || ''; - - console.log('[PREFILL] Raw URL params:', { rawNG, rawSubj, rawRefs }); - prefillData = { - newsgroups: decode(rawNG), - subject: decode(rawSubj), - references: decode(rawRefs) + newsgroups: decode(params.get('newsgroups') || ''), + subject: decode(params.get('subject') || ''), + references: decode(params.get('references') || ''), + body: decode(params.get('body') || '') }; prefillSource = 'URL'; - console.log('[PREFILL] Decoded data from URL:', prefillData); } } - // 3. Apply prefill data if available if (prefillData) { let filledFields = []; - // Fill newsgroups if (prefillData.newsgroups) { fillField('newsgroups', prefillData.newsgroups); filledFields.push('Newsgroups'); } - // Fill subject if (prefillData.subject) { fillField('subject', prefillData.subject); filledFields.push('Subject'); } - // Fill references (for replies) if (prefillData.references) { fillField('references', prefillData.references); filledFields.push('References'); } + + if (prefillData.body) { + fillField('messageToSign', prefillData.body); + filledFields.push('Quoted message'); + } - // Determine if this is a reply or new post const isReply = !!(prefillData.references || (prefillData.subject && prefillData.subject.toLowerCase().startsWith('re:'))); - // Show banner and notification if (filledFields.length > 0) { const details = `Pre-filled: ${filledFields.join(', ')} (from ${prefillSource})`; showPrefillBanner(isReply, details); @@ -1258,11 +1651,8 @@ document.getElementById('fromName').addEventListener('input', function() { } else { showNotification(`π New post to: ${prefillData.newsgroups}`, 'success', 5000); } - - console.log(`[PREFILL] Applied ${filledFields.length} fields from ${prefillSource}`); } - // Start at first tab (PoW generation) setTimeout(() => showTab('pow'), 300); } })(); diff --git a/m2usenet.go b/m2usenet.go index 011d16a..2de71b5 100644 --- a/m2usenet.go +++ b/m2usenet.go @@ -1,21 +1,22 @@ // /home/m2usenet/m2usenet.go -// Versione production-ready di Mail2Usenet in Go. -// Legge un'email da STDIN, verifica il token hashcash (inclusi controlli di formattazione, data, resource e hash), -// registra il token usato in un database JSON e invia il messaggio a un server NNTP tramite Tor (utilizzando un dialer SOCKS5). +// Production-ready Mail2Usenet with enhanced security, privacy and anti-analysis features. +// Reads email from STDIN, verifies hashcash token, registers it in JSON DB, sends to NNTP via Tor. package main import ( "bufio" "bytes" + "crypto/rand" "crypto/sha1" + "crypto/subtle" + "encoding/hex" "encoding/json" "errors" "fmt" - "golang.org/x/net/proxy" "io" "log" - "math/rand" + "math/big" "net/mail" "os" "path/filepath" @@ -23,24 +24,33 @@ import ( "strings" "sync" "time" + + "golang.org/x/net/proxy" ) var ( - // Configurazioni prelevate dalle variabili di ambiente (con valori di default per produzione) + // Configuration from environment variables NNTPServer = getEnv("NNTP_SERVER", "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion") nntpPort, _ = strconv.Atoi(getEnv("NNTP_PORT", "119")) torProxyHost = getEnv("TOR_PROXY_HOST", "127.0.0.1") torProxyPort, _ = strconv.Atoi(getEnv("TOR_PROXY_PORT", "9050")) maxPostSize, _ = strconv.Atoi(getEnv("MAX_POST_SIZE", "10240")) + minPostSize, _ = strconv.Atoi(getEnv("MIN_POST_SIZE", "512")) delayCrossPost, _ = strconv.Atoi(getEnv("DELAY_CROSSPOST", "2")) - timeWindowSec, _ = strconv.Atoi(getEnv("TIME_WINDOW_SEC", "1800")) // 30 minuti + timeWindowSec, _ = strconv.Atoi(getEnv("TIME_WINDOW_SEC", "1800")) hashcashMinBits, _ = strconv.Atoi(getEnv("HASHCASH_MIN_BITS", "24")) dbPath = getEnv("DB_PATH", "/home/m2usenet/hashcash.json") + tokenTTL, _ = strconv.Atoi(getEnv("TOKEN_TTL_HOURS", "48")) + nntpTimeout, _ = strconv.Atoi(getEnv("NNTP_TIMEOUT_SEC", "30")) + maxRetries, _ = strconv.Atoi(getEnv("MAX_RETRIES", "3")) + paddingEnabled = getEnv("ENABLE_PADDING", "false") == "true" // Disabled for Usenet text tokenMutex sync.Mutex + + // Suppress detailed logging in production + quietMode = getEnv("QUIET_MODE", "false") == "true" ) -// getEnv restituisce il valore della variabile d'ambiente o il default se non impostata. func getEnv(key, def string) string { if v, ok := os.LookupEnv(key); ok { return v @@ -48,10 +58,48 @@ func getEnv(key, def string) string { return def } -// TokenDB rappresenta la struttura JSON per la memorizzazione dei token usati. -type TokenDB map[string]string +// TokenEntry stores token with expiration +type TokenEntry struct { + UsedAt string `json:"used_at"` + Expires string `json:"expires"` +} + +type TokenDB map[string]TokenEntry + +// secureLog logs only if not in quiet mode, avoiding detailed error leakage +func secureLog(format string, v ...interface{}) { + if !quietMode { + log.Printf(format, v...) + } +} -// tokenAlreadySpent verifica se il token Γ¨ giΓ stato registrato. +// cryptoRandInt generates cryptographically secure random integer in [min, max] +func cryptoRandInt(min, max int64) (int64, error) { + if max <= min { + return 0, errors.New("invalid range") + } + diff := max - min + 1 + n, err := rand.Int(rand.Reader, big.NewInt(diff)) + if err != nil { + return 0, err + } + return n.Int64() + min, nil +} + +// randomDelay adds jitter to prevent timing analysis +func randomDelay(baseSeconds int) { + if baseSeconds <= 0 { + return + } + jitter, err := cryptoRandInt(0, int64(baseSeconds)*500) + if err != nil { + jitter = 0 + } + delay := time.Duration(baseSeconds)*time.Second + time.Duration(jitter)*time.Millisecond + time.Sleep(delay) +} + +// tokenAlreadySpent checks if token exists (now with atomic operation) func tokenAlreadySpent(token string) bool { tokenMutex.Lock() defer tokenMutex.Unlock() @@ -60,11 +108,25 @@ func tokenAlreadySpent(token string) bool { if err != nil { return false } - _, exists := db[token] - return exists + + entry, exists := db[token] + if !exists { + return false + } + + // Check if token is expired + expires, err := time.Parse(time.RFC3339, entry.Expires) + if err == nil && time.Now().UTC().After(expires) { + // Token expired, can be reused + delete(db, token) + saveTokenDB(db) + return false + } + + return true } -// markTokenSpent registra il token nel file JSON, salvando anche il timestamp. +// markTokenSpent registers token atomically with expiration func markTokenSpent(token string) error { tokenMutex.Lock() defer tokenMutex.Unlock() @@ -73,11 +135,50 @@ func markTokenSpent(token string) error { if err != nil { db = make(TokenDB) } - db[token] = time.Now().UTC().Format(time.RFC3339) + + // Double-check within lock + if _, exists := db[token]; exists { + return errors.New("token already spent") + } + + now := time.Now().UTC() + db[token] = TokenEntry{ + UsedAt: now.Format(time.RFC3339), + Expires: now.Add(time.Duration(tokenTTL) * time.Hour).Format(time.RFC3339), + } + return saveTokenDB(db) } -// loadTokenDB carica il database dei token, se esistente. +// cleanupExpiredTokens removes expired entries (no persistent metadata retention) +func cleanupExpiredTokens() error { + tokenMutex.Lock() + defer tokenMutex.Unlock() + + db, err := loadTokenDB() + if err != nil { + return err + } + + now := time.Now().UTC() + cleaned := 0 + + for token, entry := range db { + expires, err := time.Parse(time.RFC3339, entry.Expires) + if err != nil || now.After(expires) { + delete(db, token) + cleaned++ + } + } + + if cleaned > 0 { + secureLog("Cleaned %d expired tokens", cleaned) + return saveTokenDB(db) + } + + return nil +} + func loadTokenDB() (TokenDB, error) { db := make(TokenDB) if _, err := os.Stat(dbPath); os.IsNotExist(err) { @@ -96,20 +197,18 @@ func loadTokenDB() (TokenDB, error) { return db, nil } -// saveTokenDB salva il database dei token sul file system. func saveTokenDB(db TokenDB) error { dir := filepath.Dir(dbPath) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := os.MkdirAll(dir, 0700); err != nil { return err } data, err := json.MarshalIndent(db, "", " ") if err != nil { return err } - return os.WriteFile(dbPath, data, 0644) + return os.WriteFile(dbPath, data, 0600) } -// parseHashcashDate interpreta la data dal token, accettando formati a 10 o 12 cifre. func parseHashcashDate(dateStr string) (time.Time, error) { layouts := []string{"0601021504", "060102150405"} var t time.Time @@ -120,180 +219,342 @@ func parseHashcashDate(dateStr string) (time.Time, error) { return t, nil } } - return t, errors.New("formato data non valido") + return t, errors.New("invalid date format") } -// verifyHashcashToken effettua i controlli sul token: -// - Formato (7 campi) -// - Versione "1" -// - Numero di bit almeno hashcashMinBits e multiplo di 4 -// - La resource deve corrispondere all'indirizzo del mittente (case-insensitive) -// - La data deve essere entro la finestra temporale Β±timeWindowSec -// - Il digest SHA-1 deve avere i necessari zero iniziali +// verifyHashcashToken with constant-time comparisons where possible func verifyHashcashToken(token, fromAddr string) bool { parts := strings.Split(token, ":") if len(parts) != 7 { - log.Printf("Formato token non valido: %s", token) + secureLog("Invalid token format") return false } - version, bitsStr, dateStr, resource, ext, randPart, counter := parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6] - if version != "1" { - log.Printf("Versione token non valida: %s", version) + + version, bitsStr, dateStr, resource, ext, randPart, counter := + parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6] + + // Use constant-time comparison for version + if subtle.ConstantTimeCompare([]byte(version), []byte("1")) != 1 { + secureLog("Invalid token version") return false } + bits, err := strconv.Atoi(bitsStr) if err != nil || bits < hashcashMinBits || bits%4 != 0 { - log.Printf("Bits non validi: %s", bitsStr) + secureLog("Invalid bits") return false } - if strings.TrimSpace(strings.ToLower(resource)) != strings.TrimSpace(strings.ToLower(fromAddr)) { - log.Printf("Resource mismacth: %s vs %s", resource, fromAddr) + + // Normalize addresses for comparison + normalizedResource := strings.TrimSpace(strings.ToLower(resource)) + normalizedFrom := strings.TrimSpace(strings.ToLower(fromAddr)) + + if subtle.ConstantTimeCompare([]byte(normalizedResource), []byte(normalizedFrom)) != 1 { + secureLog("Resource mismatch") return false } + tokenTime, err := parseHashcashDate(dateStr) if err != nil { - log.Printf("Errore nella data del token: %v", err) + secureLog("Invalid token date") return false } + now := time.Now().UTC() - if now.Sub(tokenTime) > time.Duration(timeWindowSec)*time.Second || - tokenTime.Sub(now) > time.Duration(timeWindowSec)*time.Second { - log.Printf("Token fuori finestra temporale: %v", now.Sub(tokenTime)) + timeDiff := now.Sub(tokenTime) + if timeDiff < 0 { + timeDiff = -timeDiff + } + + if timeDiff > time.Duration(timeWindowSec)*time.Second { + secureLog("Token outside time window") return false } - assembled := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", version, bitsStr, dateStr, resource, ext, randPart, counter) + + // Verify hash + assembled := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", + version, bitsStr, dateStr, resource, ext, randPart, counter) hash := sha1.Sum([]byte(assembled)) shaHex := fmt.Sprintf("%x", hash) target := strings.Repeat("0", bits/4) + if !strings.HasPrefix(shaHex, target) { - log.Printf("Verifica hash fallita: %s non inizia con %s", shaHex, target) + secureLog("Hash verification failed") return false } + return true } -// sendViaTor stabilisce una connessione al server NNTP tramite Tor usando un dialer SOCKS5, invia il comando IHAVE e il messaggio. +// validateOnionAddress ensures NNTP server is a valid .onion address +func validateOnionAddress(addr string) bool { + if !strings.HasSuffix(addr, ".onion") { + return false + } + parts := strings.Split(addr, ".") + if len(parts) != 2 { + return false + } + // v3 onion addresses are 56 chars + return len(parts[0]) == 56 +} + +// addAdaptivePadding adds random padding to prevent size correlation +func addAdaptivePadding(message string) string { + if !paddingEnabled { + return message + } + + currentSize := len([]byte(message)) + + // Pad to next 1KB boundary + targetSize := ((currentSize / 1024) + 1) * 1024 + if targetSize > maxPostSize { + targetSize = maxPostSize + } + + paddingNeeded := targetSize - currentSize + if paddingNeeded <= 0 { + return message + } + + // Add random padding as comments + padding := make([]byte, paddingNeeded) + rand.Read(padding) + paddingHex := hex.EncodeToString(padding) + + // Truncate to needed length + if len(paddingHex) > paddingNeeded-20 { + paddingHex = paddingHex[:paddingNeeded-20] + } + + return message + "\r\n\r\n[padding:" + paddingHex + "]" +} + +// sendViaTor with timeout, retry logic and exponential backoff func sendViaTor(server string, port int, message, messageID string) (bool, error) { + if !validateOnionAddress(server) { + return false, errors.New("invalid onion address") + } + + var lastErr error + + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + // Exponential backoff with jitter + backoff := (1 << attempt) // 2^attempt seconds + randomDelay(backoff) + secureLog("Retry attempt %d/%d", attempt+1, maxRetries) + } + + success, err := attemptSend(server, port, message, messageID) + if err == nil && success { + return true, nil + } + lastErr = err + + // Don't retry on certain errors + if err != nil && strings.Contains(err.Error(), "IHAVE not accepted") { + return false, err + } + } + + return false, fmt.Errorf("failed after %d attempts: %v", maxRetries, lastErr) +} + +func attemptSend(server string, port int, message, messageID string) (bool, error) { torAddr := fmt.Sprintf("%s:%d", torProxyHost, torProxyPort) dialer, err := proxy.SOCKS5("tcp", torAddr, nil, proxy.Direct) if err != nil { - return false, fmt.Errorf("Errore nel dialer SOCKS5: %v", err) + return false, fmt.Errorf("SOCKS5 dialer error: %v", err) } + + // Add timeout conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%d", server, port)) if err != nil { - return false, fmt.Errorf("Connessione NNTP fallita: %v", err) + return false, fmt.Errorf("NNTP connection failed: %v", err) } defer conn.Close() - + + // Set deadline + deadline := time.Now().Add(time.Duration(nntpTimeout) * time.Second) + conn.SetDeadline(deadline) + reader := bufio.NewReader(conn) + + // Read welcome welcome, err := reader.ReadString('\n') if err != nil { - return false, err + return false, fmt.Errorf("welcome read error: %v", err) } - log.Printf("Connesso a NNTP: %s", strings.TrimSpace(welcome)) - + secureLog("Connected to NNTP") + + if !strings.HasPrefix(welcome, "200") && !strings.HasPrefix(welcome, "201") { + return false, errors.New("unexpected welcome response") + } + + // Send IHAVE ihaveCmd := fmt.Sprintf("IHAVE %s\r\n", messageID) if _, err := conn.Write([]byte(ihaveCmd)); err != nil { return false, err } + ihaveResp, err := reader.ReadString('\n') if err != nil { return false, err } - log.Printf("Risposta IHAVE: %s", strings.TrimSpace(ihaveResp)) + if !strings.HasPrefix(ihaveResp, "335") { - return false, errors.New("IHAVE non accettato") + return false, errors.New("IHAVE not accepted") } + + // Send message fullMessage := message + "\r\n.\r\n" if _, err := conn.Write([]byte(fullMessage)); err != nil { return false, err } + postResp, err := reader.ReadString('\n') if err != nil { return false, err } - log.Printf("Risposta posting: %s", strings.TrimSpace(postResp)) + + secureLog("Post response received") + conn.Write([]byte("QUIT\r\n")) + return strings.HasPrefix(postResp, "235"), nil } +// safeParseFrom safely extracts email address from From header +func safeParseFrom(fromHeader string) (string, error) { + if fromHeader == "" { + return "", errors.New("empty From header") + } + + // Try parsing as RFC 5322 address + addr, err := mail.ParseAddress(fromHeader) + if err == nil { + return addr.Address, nil + } + + // Fallback to manual parsing + if strings.Contains(fromHeader, "<") && strings.Contains(fromHeader, ">") { + start := strings.Index(fromHeader, "<") + end := strings.Index(fromHeader, ">") + if end > start { + return strings.TrimSpace(fromHeader[start+1 : end]), nil + } + } + + return strings.TrimSpace(fromHeader), nil +} + +// generateSecureMessageID creates unpredictable Message-ID +func generateSecureMessageID() (string, error) { + timestamp := time.Now().Unix() + + // 16 bytes of crypto random data + randomBytes := make([]byte, 16) + if _, err := rand.Read(randomBytes); err != nil { + return "", err + } + randomHex := hex.EncodeToString(randomBytes) + + return fmt.Sprintf("<%d.%s@mail2usenet.local>", timestamp, randomHex), nil +} + func main() { - // Lettura dell'intera email da STDIN + // Cleanup expired tokens on startup + if err := cleanupExpiredTokens(); err != nil { + secureLog("Token cleanup warning: %v", err) + } + + // Read email from STDIN input, err := io.ReadAll(os.Stdin) if err != nil { - log.Fatalf("Errore lettura STDIN: %v", err) + log.Fatalf("Failed to read input") } + msg, err := mail.ReadMessage(bytes.NewReader(input)) if err != nil { - log.Fatalf("Errore nel parsing dell'email: %v", err) + log.Fatalf("Failed to parse email") } + header := msg.Header - + fromHeader := header.Get("From") - if fromHeader == "" { - log.Fatalf("Header From mancante") - } - fromAddr := fromHeader - if strings.Contains(fromHeader, "<") && strings.Contains(fromHeader, ">") { - fromAddr = strings.TrimSpace(strings.Split(strings.Split(fromHeader, "<")[1], ">")[0]) + fromAddr, err := safeParseFrom(fromHeader) + if err != nil { + log.Fatalf("Invalid From header") } + newsgroups := header.Get("Newsgroups") if newsgroups == "" { - log.Fatalf("Header Newsgroups mancante") + log.Fatalf("Missing Newsgroups header") } + subject := header.Get("Subject") if subject == "" { subject = "(No subject)" } + xhashcash := header.Get("X-Hashcash") if xhashcash == "" { - log.Fatalf("Header X-Hashcash mancante") + log.Fatalf("Missing X-Hashcash header") } - - // Log degli header Ed25519 per debugging - log.Printf("X-Ed25519-Pub header: %s", header.Get("X-Ed25519-Pub")) - log.Printf("X-Ed25519-Sig header: %s", header.Get("X-Ed25519-Sig")) - + + // Read body bodyBuf := new(bytes.Buffer) - _, err = io.Copy(bodyBuf, msg.Body) - if err != nil { - log.Fatalf("Errore lettura corpo email: %v", err) + if _, err = io.Copy(bodyBuf, msg.Body); err != nil { + log.Fatalf("Failed to read body") } body := strings.TrimSpace(bodyBuf.String()) if body == "" { - log.Fatalf("Corpo del messaggio vuoto") + log.Fatalf("Empty message body") } - - // Prevenzione riutilizzo token + + // ATOMIC token check and mark + tokenMutex.Lock() if tokenAlreadySpent(xhashcash) { - log.Fatalf("Token Hashcash giΓ utilizzato") + tokenMutex.Unlock() + log.Fatalf("Token already used") } - // Verifica del token if !verifyHashcashToken(xhashcash, fromAddr) { - log.Fatalf("Token Hashcash non valido") + tokenMutex.Unlock() + log.Fatalf("Invalid hashcash token") } if err := markTokenSpent(xhashcash); err != nil { - log.Printf("Errore nella registrazione del token: %v", err) + tokenMutex.Unlock() + log.Fatalf("Failed to register token") } - - // Gestione dei newsgroup: limita a 3 gruppi + tokenMutex.Unlock() + + // Limit newsgroups groups := strings.Split(newsgroups, ",") + for i := range groups { + groups[i] = strings.TrimSpace(groups[i]) + } if len(groups) > 3 { groups = groups[:3] - log.Printf("Limite newsgroups raggiunto, utilizzati: %v", groups) + secureLog("Limited to 3 newsgroups") } limitedGroups := strings.Join(groups, ", ") - - // Ritardo per cross-posting, se necessario + + // Randomized delay for cross-posting if len(groups) > 1 { - time.Sleep(time.Duration(delayCrossPost) * time.Second) + randomDelay(delayCrossPost) } - - // Generazione di un Message-ID univoco e formattazione data - messageID := fmt.Sprintf("<%d.%d@mail2usenet.local>", time.Now().Unix(), randInt(1000, 9999)) + + // Generate secure Message-ID + messageID, err := generateSecureMessageID() + if err != nil { + log.Fatalf("Failed to generate Message-ID") + } + dateHeader := time.Now().UTC().Format(time.RFC1123Z) - - // Composizione del messaggio Usenet in formato RFC-compliant + + // Build message headers := []string{ fmt.Sprintf("Message-ID: %s", messageID), fmt.Sprintf("Date: %s", dateHeader), @@ -307,39 +568,42 @@ func main() { "Mime-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", "Content-Transfer-Encoding: 7bit", - "User-Agent: m2usenet-go v0.1.0", + "User-Agent: m2usenet-go v1.0.0", } - - // Aggiunta degli header Ed25519 se presenti + + // Add Ed25519 headers if present if ed25519pub := header.Get("X-Ed25519-Pub"); ed25519pub != "" { headers = append(headers, fmt.Sprintf("X-Ed25519-Pub: %s", ed25519pub)) } if ed25519sig := header.Get("X-Ed25519-Sig"); ed25519sig != "" { headers = append(headers, fmt.Sprintf("X-Ed25519-Sig: %s", ed25519sig)) } - if ref := header.Get("References"); ref != "" { headers = append(headers, fmt.Sprintf("References: %s", ref)) } + usenetPost := strings.Join(headers, "\r\n") + "\r\n\r\n" + body - if len([]byte(usenetPost)) > maxPostSize { - log.Fatalf("Messaggio supera il limite di %d byte", maxPostSize) - } - + + // Add adaptive padding to prevent size correlation + usenetPost = addAdaptivePadding(usenetPost) + + postSize := len([]byte(usenetPost)) + if postSize > maxPostSize { + log.Fatalf("Message exceeds size limit") + } + if postSize < minPostSize && paddingEnabled { + log.Fatalf("Message too small even with padding") + } + success, err := sendViaTor(NNTPServer, nntpPort, usenetPost, messageID) if err != nil { - log.Fatalf("Errore nell'invio del messaggio: %v", err) + log.Fatalf("Send failed: %v", err) } + if success { - log.Println("Messaggio inviato correttamente") + secureLog("Message sent successfully") os.Exit(0) } else { - log.Fatalln("Invio del messaggio fallito") + log.Fatalln("Message delivery failed") } } - -// randInt genera un intero casuale nel range [min, max]. -func randInt(min, max int) int { - rand.Seed(time.Now().UnixNano()) - return rand.Intn(max-min+1) + min -} diff --git a/nacl-util.min.js b/nacl-util.min.js new file mode 100644 index 0000000..0426742 --- /dev/null +++ b/nacl-util.min.js @@ -0,0 +1 @@ +!function(e,n){"use strict";"undefined"!=typeof module&&module.exports?module.exports=n():(e.nacl||(e.nacl={}),e.nacl.util=n())}(this,function(){"use strict";var e={};function o(e){if(!/^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(e))throw new TypeError("invalid encoding")}return e.decodeUTF8=function(e){if("string"!=typeof e)throw new TypeError("expected string");var n,r=unescape(encodeURIComponent(e)),t=new Uint8Array(r.length);for(n=0;n<r.length;n++)t[n]=r.charCodeAt(n);return t},e.encodeUTF8=function(e){var n,r=[];for(n=0;n<e.length;n++)r.push(String.fromCharCode(e[n]));return decodeURIComponent(escape(r.join("")))},"undefined"==typeof atob?void 0!==Buffer.from?(e.encodeBase64=function(e){return Buffer.from(e).toString("base64")},e.decodeBase64=function(e){return o(e),new Uint8Array(Array.prototype.slice.call(Buffer.from(e,"base64"),0))}):(e.encodeBase64=function(e){return new Buffer(e).toString("base64")},e.decodeBase64=function(e){return o(e),new Uint8Array(Array.prototype.slice.call(new Buffer(e,"base64"),0))}):(e.encodeBase64=function(e){var n,r=[],t=e.length;for(n=0;n<t;n++)r.push(String.fromCharCode(e[n]));return btoa(r.join(""))},e.decodeBase64=function(e){o(e);var n,r=atob(e),t=new Uint8Array(r.length);for(n=0;n<r.length;n++)t[n]=r.charCodeAt(n);return t}),e});
\ No newline at end of file diff --git a/nacl.min.js b/nacl.min.js new file mode 100644 index 0000000..65340cc --- /dev/null +++ b/nacl.min.js @@ -0,0 +1 @@ +!function(i){"use strict";var m=function(r,n){this.hi=0|r,this.lo=0|n},v=function(r){var n,e=new Float64Array(16);if(r)for(n=0;n<r.length;n++)e[n]=r[n];return e},a=function(){throw new Error("no PRNG")},o=new Uint8Array(16),e=new Uint8Array(32);e[0]=9;var c=v(),w=v([1]),g=v([56129,1]),y=v([30883,4953,19914,30187,55467,16705,2637,112,59544,30585,16505,36039,65139,11119,27886,20995]),l=v([61785,9906,39828,60374,45398,33411,5274,224,53552,61171,33010,6542,64743,22239,55772,9222]),t=v([54554,36645,11616,51542,42930,38181,51040,26924,56412,64982,57905,49316,21502,52590,14035,8553]),f=v([26200,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214]),s=v([41136,18958,6951,50414,58488,44335,6150,12099,55207,15867,153,11085,57099,20417,9344,11139]);function h(r,n){return r<<n|r>>>32-n}function b(r,n){var e=255&r[n+3];return(e=(e=e<<8|255&r[n+2])<<8|255&r[n+1])<<8|255&r[n+0]}function B(r,n){var e=r[n]<<24|r[n+1]<<16|r[n+2]<<8|r[n+3],t=r[n+4]<<24|r[n+5]<<16|r[n+6]<<8|r[n+7];return new m(e,t)}function p(r,n,e){var t;for(t=0;t<4;t++)r[n+t]=255&e,e>>>=8}function S(r,n,e){r[n]=e.hi>>24&255,r[n+1]=e.hi>>16&255,r[n+2]=e.hi>>8&255,r[n+3]=255&e.hi,r[n+4]=e.lo>>24&255,r[n+5]=e.lo>>16&255,r[n+6]=e.lo>>8&255,r[n+7]=255&e.lo}function u(r,n,e,t,o){var i,a=0;for(i=0;i<o;i++)a|=r[n+i]^e[t+i];return(1&a-1>>>8)-1}function A(r,n,e,t){return u(r,n,e,t,16)}function _(r,n,e,t){return u(r,n,e,t,32)}function U(r,n,e,t,o){var i,a,f,u=new Uint32Array(16),c=new Uint32Array(16),w=new Uint32Array(16),y=new Uint32Array(4);for(i=0;i<4;i++)c[5*i]=b(t,4*i),c[1+i]=b(e,4*i),c[6+i]=b(n,4*i),c[11+i]=b(e,16+4*i);for(i=0;i<16;i++)w[i]=c[i];for(i=0;i<20;i++){for(a=0;a<4;a++){for(f=0;f<4;f++)y[f]=c[(5*a+4*f)%16];for(y[1]^=h(y[0]+y[3]|0,7),y[2]^=h(y[1]+y[0]|0,9),y[3]^=h(y[2]+y[1]|0,13),y[0]^=h(y[3]+y[2]|0,18),f=0;f<4;f++)u[4*a+(a+f)%4]=y[f]}for(f=0;f<16;f++)c[f]=u[f]}if(o){for(i=0;i<16;i++)c[i]=c[i]+w[i]|0;for(i=0;i<4;i++)c[5*i]=c[5*i]-b(t,4*i)|0,c[6+i]=c[6+i]-b(n,4*i)|0;for(i=0;i<4;i++)p(r,4*i,c[5*i]),p(r,16+4*i,c[6+i])}else for(i=0;i<16;i++)p(r,4*i,c[i]+w[i]|0)}function E(r,n,e,t){U(r,n,e,t,!1)}function x(r,n,e,t){return U(r,n,e,t,!0),0}var d=new Uint8Array([101,120,112,97,110,100,32,51,50,45,98,121,116,101,32,107]);function K(r,n,e,t,o,i,a){var f,u,c=new Uint8Array(16),w=new Uint8Array(64);if(!o)return 0;for(u=0;u<16;u++)c[u]=0;for(u=0;u<8;u++)c[u]=i[u];for(;64<=o;){for(E(w,c,a,d),u=0;u<64;u++)r[n+u]=(e?e[t+u]:0)^w[u];for(f=1,u=8;u<16;u++)f=f+(255&c[u])|0,c[u]=255&f,f>>>=8;o-=64,n+=64,e&&(t+=64)}if(0<o)for(E(w,c,a,d),u=0;u<o;u++)r[n+u]=(e?e[t+u]:0)^w[u];return 0}function Y(r,n,e,t,o){return K(r,n,null,0,e,t,o)}function L(r,n,e,t,o){var i=new Uint8Array(32);return x(i,t,o,d),Y(r,n,e,t.subarray(16),i)}function T(r,n,e,t,o,i,a){var f=new Uint8Array(32);return x(f,i,a,d),K(r,n,e,t,o,i.subarray(16),f)}function k(r,n){var e,t=0;for(e=0;e<17;e++)t=t+(r[e]+n[e]|0)|0,r[e]=255&t,t>>>=8}var z=new Uint32Array([5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,252]);function R(r,n,e,t,o,i){var a,f,u,c,w=new Uint32Array(17),y=new Uint32Array(17),l=new Uint32Array(17),s=new Uint32Array(17),h=new Uint32Array(17);for(u=0;u<17;u++)y[u]=l[u]=0;for(u=0;u<16;u++)y[u]=i[u];for(y[3]&=15,y[4]&=252,y[7]&=15,y[8]&=252,y[11]&=15,y[12]&=252,y[15]&=15;0<o;){for(u=0;u<17;u++)s[u]=0;for(u=0;u<16&&u<o;++u)s[u]=e[t+u];for(s[u]=1,t+=u,o-=u,k(l,s),f=0;f<17;f++)for(u=w[f]=0;u<17;u++)w[f]=w[f]+l[u]*(u<=f?y[f-u]:320*y[f+17-u]|0)|0;for(f=0;f<17;f++)l[f]=w[f];for(u=c=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;for(c=c+l[16]|0,l[16]=3&c,c=5*(c>>>2)|0,u=0;u<16;u++)c=c+l[u]|0,l[u]=255&c,c>>>=8;c=c+l[16]|0,l[16]=c}for(u=0;u<17;u++)h[u]=l[u];for(k(l,z),a=0|-(l[16]>>>7),u=0;u<17;u++)l[u]^=a&(h[u]^l[u]);for(u=0;u<16;u++)s[u]=i[u+16];for(s[16]=0,k(l,s),u=0;u<16;u++)r[n+u]=l[u];return 0}function P(r,n,e,t,o,i){var a=new Uint8Array(16);return R(a,0,e,t,o,i),A(r,n,a,0)}function M(r,n,e,t,o){var i;if(e<32)return-1;for(T(r,0,n,0,e,t,o),R(r,16,r,32,e-32,r),i=0;i<16;i++)r[i]=0;return 0}function N(r,n,e,t,o){var i,a=new Uint8Array(32);if(e<32)return-1;if(L(a,0,32,t,o),0!==P(n,16,n,32,e-32,a))return-1;for(T(r,0,n,0,e,t,o),i=0;i<32;i++)r[i]=0;return 0}function O(r,n){var e;for(e=0;e<16;e++)r[e]=0|n[e]}function C(r){var n,e;for(e=0;e<16;e++)r[e]+=65536,n=Math.floor(r[e]/65536),r[(e+1)*(e<15?1:0)]+=n-1+37*(n-1)*(15===e?1:0),r[e]-=65536*n}function F(r,n,e){for(var t,o=~(e-1),i=0;i<16;i++)t=o&(r[i]^n[i]),r[i]^=t,n[i]^=t}function Z(r,n){var e,t,o,i=v(),a=v();for(e=0;e<16;e++)a[e]=n[e];for(C(a),C(a),C(a),t=0;t<2;t++){for(i[0]=a[0]-65517,e=1;e<15;e++)i[e]=a[e]-65535-(i[e-1]>>16&1),i[e-1]&=65535;i[15]=a[15]-32767-(i[14]>>16&1),o=i[15]>>16&1,i[14]&=65535,F(a,i,1-o)}for(e=0;e<16;e++)r[2*e]=255&a[e],r[2*e+1]=a[e]>>8}function G(r,n){var e=new Uint8Array(32),t=new Uint8Array(32);return Z(e,r),Z(t,n),_(e,0,t,0)}function q(r){var n=new Uint8Array(32);return Z(n,r),1&n[0]}function D(r,n){var e;for(e=0;e<16;e++)r[e]=n[2*e]+(n[2*e+1]<<8);r[15]&=32767}function I(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]+e[t]|0}function V(r,n,e){var t;for(t=0;t<16;t++)r[t]=n[t]-e[t]|0}function X(r,n,e){var t,o,i=new Float64Array(31);for(t=0;t<31;t++)i[t]=0;for(t=0;t<16;t++)for(o=0;o<16;o++)i[t+o]+=n[t]*e[o];for(t=0;t<15;t++)i[t]+=38*i[t+16];for(t=0;t<16;t++)r[t]=i[t];C(r),C(r)}function j(r,n){X(r,n,n)}function H(r,n){var e,t=v();for(e=0;e<16;e++)t[e]=n[e];for(e=253;0<=e;e--)j(t,t),2!==e&&4!==e&&X(t,t,n);for(e=0;e<16;e++)r[e]=t[e]}function J(r,n){var e,t=v();for(e=0;e<16;e++)t[e]=n[e];for(e=250;0<=e;e--)j(t,t),1!==e&&X(t,t,n);for(e=0;e<16;e++)r[e]=t[e]}function Q(r,n,e){var t,o,i=new Uint8Array(32),a=new Float64Array(80),f=v(),u=v(),c=v(),w=v(),y=v(),l=v();for(o=0;o<31;o++)i[o]=n[o];for(i[31]=127&n[31]|64,i[0]&=248,D(a,e),o=0;o<16;o++)u[o]=a[o],w[o]=f[o]=c[o]=0;for(f[0]=w[0]=1,o=254;0<=o;--o)F(f,u,t=i[o>>>3]>>>(7&o)&1),F(c,w,t),I(y,f,c),V(f,f,c),I(c,u,w),V(u,u,w),j(w,y),j(l,f),X(f,c,f),X(c,u,y),I(y,f,c),V(f,f,c),j(u,f),V(c,w,l),X(f,c,g),I(f,f,w),X(c,c,f),X(f,w,l),X(w,u,a),j(u,y),F(f,u,t),F(c,w,t);for(o=0;o<16;o++)a[o+16]=f[o],a[o+32]=c[o],a[o+48]=u[o],a[o+64]=w[o];var s=a.subarray(32),h=a.subarray(16);return H(s,s),X(h,h,s),Z(r,h),0}function W(r,n){return Q(r,n,e)}function $(r,n){return a(n,32),W(r,n)}function rr(r,n,e){var t=new Uint8Array(32);return Q(t,e,n),x(r,o,t,d)}var nr=M,er=N;function tr(){var r,n,e,t=0,o=0,i=0,a=0,f=65535;for(e=0;e<arguments.length;e++)t+=(r=arguments[e].lo)&f,o+=r>>>16,i+=(n=arguments[e].hi)&f,a+=n>>>16;return new m((i+=(o+=t>>>16)>>>16)&f|(a+=i>>>16)<<16,t&f|o<<16)}function or(r,n){return new m(r.hi>>>n,r.lo>>>n|r.hi<<32-n)}function ir(){var r,n=0,e=0;for(r=0;r<arguments.length;r++)n^=arguments[r].lo,e^=arguments[r].hi;return new m(e,n)}function ar(r,n){var e,t,o=32-n;return n<32?(e=r.hi>>>n|r.lo<<o,t=r.lo>>>n|r.hi<<o):n<64&&(e=r.lo>>>n|r.hi<<o,t=r.hi>>>n|r.lo<<o),new m(e,t)}var fr=[new m(1116352408,3609767458),new m(1899447441,602891725),new m(3049323471,3964484399),new m(3921009573,2173295548),new m(961987163,4081628472),new m(1508970993,3053834265),new m(2453635748,2937671579),new m(2870763221,3664609560),new m(3624381080,2734883394),new m(310598401,1164996542),new m(607225278,1323610764),new m(1426881987,3590304994),new m(1925078388,4068182383),new m(2162078206,991336113),new m(2614888103,633803317),new m(3248222580,3479774868),new m(3835390401,2666613458),new m(4022224774,944711139),new m(264347078,2341262773),new m(604807628,2007800933),new m(770255983,1495990901),new m(1249150122,1856431235),new m(1555081692,3175218132),new m(1996064986,2198950837),new m(2554220882,3999719339),new m(2821834349,766784016),new m(2952996808,2566594879),new m(3210313671,3203337956),new m(3336571891,1034457026),new m(3584528711,2466948901),new m(113926993,3758326383),new m(338241895,168717936),new m(666307205,1188179964),new m(773529912,1546045734),new m(1294757372,1522805485),new m(1396182291,2643833823),new m(1695183700,2343527390),new m(1986661051,1014477480),new m(2177026350,1206759142),new m(2456956037,344077627),new m(2730485921,1290863460),new m(2820302411,3158454273),new m(3259730800,3505952657),new m(3345764771,106217008),new m(3516065817,3606008344),new m(3600352804,1432725776),new m(4094571909,1467031594),new m(275423344,851169720),new m(430227734,3100823752),new m(506948616,1363258195),new m(659060556,3750685593),new m(883997877,3785050280),new m(958139571,3318307427),new m(1322822218,3812723403),new m(1537002063,2003034995),new m(1747873779,3602036899),new m(1955562222,1575990012),new m(2024104815,1125592928),new m(2227730452,2716904306),new m(2361852424,442776044),new m(2428436474,593698344),new m(2756734187,3733110249),new m(3204031479,2999351573),new m(3329325298,3815920427),new m(3391569614,3928383900),new m(3515267271,566280711),new m(3940187606,3454069534),new m(4118630271,4000239992),new m(116418474,1914138554),new m(174292421,2731055270),new m(289380356,3203993006),new m(460393269,320620315),new m(685471733,587496836),new m(852142971,1086792851),new m(1017036298,365543100),new m(1126000580,2618297676),new m(1288033470,3409855158),new m(1501505948,4234509866),new m(1607167915,987167468),new m(1816402316,1246189591)];function ur(r,n,e){var t,o,i,a=[],f=[],u=[],c=[];for(o=0;o<8;o++)a[o]=u[o]=B(r,8*o);for(var w,y,l,s,h,v,g,b,p,A,_,U,E,x,d=0;128<=e;){for(o=0;o<16;o++)c[o]=B(n,8*o+d);for(o=0;o<80;o++){for(i=0;i<8;i++)f[i]=u[i];for(t=tr(u[7],ir(ar(x=u[4],14),ar(x,18),ar(x,41)),(p=u[4],A=u[5],_=u[6],0,U=p.hi&A.hi^~p.hi&_.hi,E=p.lo&A.lo^~p.lo&_.lo,new m(U,E)),fr[o],c[o%16]),f[7]=tr(t,ir(ar(b=u[0],28),ar(b,34),ar(b,39)),(l=u[0],s=u[1],h=u[2],0,v=l.hi&s.hi^l.hi&h.hi^s.hi&h.hi,g=l.lo&s.lo^l.lo&h.lo^s.lo&h.lo,new m(v,g))),f[3]=tr(f[3],t),i=0;i<8;i++)u[(i+1)%8]=f[i];if(o%16==15)for(i=0;i<16;i++)c[i]=tr(c[i],c[(i+9)%16],ir(ar(y=c[(i+1)%16],1),ar(y,8),or(y,7)),ir(ar(w=c[(i+14)%16],19),ar(w,61),or(w,6)))}for(o=0;o<8;o++)u[o]=tr(u[o],a[o]),a[o]=u[o];d+=128,e-=128}for(o=0;o<8;o++)S(r,8*o,a[o]);return e}var cr=new Uint8Array([106,9,230,103,243,188,201,8,187,103,174,133,132,202,167,59,60,110,243,114,254,148,248,43,165,79,245,58,95,29,54,241,81,14,82,127,173,230,130,209,155,5,104,140,43,62,108,31,31,131,217,171,251,65,189,107,91,224,205,25,19,126,33,121]);function wr(r,n,e){var t,o=new Uint8Array(64),i=new Uint8Array(256),a=e;for(t=0;t<64;t++)o[t]=cr[t];for(ur(o,n,e),e%=128,t=0;t<256;t++)i[t]=0;for(t=0;t<e;t++)i[t]=n[a-e+t];for(i[e]=128,i[(e=256-128*(e<112?1:0))-9]=0,S(i,e-8,new m(a/536870912|0,a<<3)),ur(o,i,e),t=0;t<64;t++)r[t]=o[t];return 0}function yr(r,n){var e=v(),t=v(),o=v(),i=v(),a=v(),f=v(),u=v(),c=v(),w=v();V(e,r[1],r[0]),V(w,n[1],n[0]),X(e,e,w),I(t,r[0],r[1]),I(w,n[0],n[1]),X(t,t,w),X(o,r[3],n[3]),X(o,o,l),X(i,r[2],n[2]),I(i,i,i),V(a,t,e),V(f,i,o),I(u,i,o),I(c,t,e),X(r[0],a,f),X(r[1],c,u),X(r[2],u,f),X(r[3],a,c)}function lr(r,n,e){var t;for(t=0;t<4;t++)F(r[t],n[t],e)}function sr(r,n){var e=v(),t=v(),o=v();H(o,n[2]),X(e,n[0],o),X(t,n[1],o),Z(r,t),r[31]^=q(e)<<7}function hr(r,n,e){var t,o;for(O(r[0],c),O(r[1],w),O(r[2],w),O(r[3],c),o=255;0<=o;--o)lr(r,n,t=e[o/8|0]>>(7&o)&1),yr(n,r),yr(r,r),lr(r,n,t)}function vr(r,n){var e=[v(),v(),v(),v()];O(e[0],t),O(e[1],f),O(e[2],w),X(e[3],t,f),hr(r,e,n)}function gr(r,n,e){var t,o=new Uint8Array(64),i=[v(),v(),v(),v()];for(e||a(n,32),wr(o,n,32),o[0]&=248,o[31]&=127,o[31]|=64,vr(i,o),sr(r,i),t=0;t<32;t++)n[t+32]=r[t];return 0}var br=new Float64Array([237,211,245,92,26,99,18,88,214,156,247,162,222,249,222,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16]);function pr(r,n){var e,t,o,i;for(t=63;32<=t;--t){for(e=0,o=t-32,i=t-12;o<i;++o)n[o]+=e-16*n[t]*br[o-(t-32)],e=Math.floor((n[o]+128)/256),n[o]-=256*e;n[o]+=e,n[t]=0}for(o=e=0;o<32;o++)n[o]+=e-(n[31]>>4)*br[o],e=n[o]>>8,n[o]&=255;for(o=0;o<32;o++)n[o]-=e*br[o];for(t=0;t<32;t++)n[t+1]+=n[t]>>8,r[t]=255&n[t]}function Ar(r){var n,e=new Float64Array(64);for(n=0;n<64;n++)e[n]=r[n];for(n=0;n<64;n++)r[n]=0;pr(r,e)}function _r(r,n,e,t){var o,i,a=new Uint8Array(64),f=new Uint8Array(64),u=new Uint8Array(64),c=new Float64Array(64),w=[v(),v(),v(),v()];wr(a,t,32),a[0]&=248,a[31]&=127,a[31]|=64;var y=e+64;for(o=0;o<e;o++)r[64+o]=n[o];for(o=0;o<32;o++)r[32+o]=a[32+o];for(wr(u,r.subarray(32),e+32),Ar(u),vr(w,u),sr(r,w),o=32;o<64;o++)r[o]=t[o];for(wr(f,r,e+64),Ar(f),o=0;o<64;o++)c[o]=0;for(o=0;o<32;o++)c[o]=u[o];for(o=0;o<32;o++)for(i=0;i<32;i++)c[o+i]+=f[o]*a[i];return pr(r.subarray(32),c),y}function Ur(r,n,e,t){var o,i=new Uint8Array(32),a=new Uint8Array(64),f=[v(),v(),v(),v()],u=[v(),v(),v(),v()];if(e<64)return-1;if(function(r,n){var e=v(),t=v(),o=v(),i=v(),a=v(),f=v(),u=v();if(O(r[2],w),D(r[1],n),j(o,r[1]),X(i,o,y),V(o,o,r[2]),I(i,r[2],i),j(a,i),j(f,a),X(u,f,a),X(e,u,o),X(e,e,i),J(e,e),X(e,e,o),X(e,e,i),X(e,e,i),X(r[0],e,i),j(t,r[0]),X(t,t,i),G(t,o)&&X(r[0],r[0],s),j(t,r[0]),X(t,t,i),G(t,o))return 1;q(r[0])===n[31]>>7&&V(r[0],c,r[0]),X(r[3],r[0],r[1])}(u,t))return-1;for(o=0;o<e;o++)r[o]=n[o];for(o=0;o<32;o++)r[o+32]=t[o];if(wr(a,r,e),Ar(a),hr(f,u,a),vr(u,n.subarray(32)),yr(f,u),sr(i,f),e-=64,_(n,0,i,0)){for(o=0;o<e;o++)r[o]=0;return-1}for(o=0;o<e;o++)r[o]=n[o+64];return e}function Er(r,n){if(32!==r.length)throw new Error("bad key size");if(24!==n.length)throw new Error("bad nonce size")}function xr(){for(var r=0;r<arguments.length;r++)if(!(arguments[r]instanceof Uint8Array))throw new TypeError("unexpected type, use Uint8Array")}function dr(r){for(var n=0;n<r.length;n++)r[n]=0}i.lowlevel={crypto_core_hsalsa20:x,crypto_stream_xor:T,crypto_stream:L,crypto_stream_salsa20_xor:K,crypto_stream_salsa20:Y,crypto_onetimeauth:R,crypto_onetimeauth_verify:P,crypto_verify_16:A,crypto_verify_32:_,crypto_secretbox:M,crypto_secretbox_open:N,crypto_scalarmult:Q,crypto_scalarmult_base:W,crypto_box_beforenm:rr,crypto_box_afternm:nr,crypto_box:function(r,n,e,t,o,i){var a=new Uint8Array(32);return rr(a,o,i),nr(r,n,e,t,a)},crypto_box_open:function(r,n,e,t,o,i){var a=new Uint8Array(32);return rr(a,o,i),er(r,n,e,t,a)},crypto_box_keypair:$,crypto_hash:wr,crypto_sign:_r,crypto_sign_keypair:gr,crypto_sign_open:Ur,crypto_secretbox_KEYBYTES:32,crypto_secretbox_NONCEBYTES:24,crypto_secretbox_ZEROBYTES:32,crypto_secretbox_BOXZEROBYTES:16,crypto_scalarmult_BYTES:32,crypto_scalarmult_SCALARBYTES:32,crypto_box_PUBLICKEYBYTES:32,crypto_box_SECRETKEYBYTES:32,crypto_box_BEFORENMBYTES:32,crypto_box_NONCEBYTES:24,crypto_box_ZEROBYTES:32,crypto_box_BOXZEROBYTES:16,crypto_sign_BYTES:64,crypto_sign_PUBLICKEYBYTES:32,crypto_sign_SECRETKEYBYTES:64,crypto_sign_SEEDBYTES:32,crypto_hash_BYTES:64,gf:v,D:y,L:br,pack25519:Z,unpack25519:D,M:X,A:I,S:j,Z:V,pow2523:J,add:yr,set25519:O,modL:pr,scalarmult:hr,scalarbase:vr},i.randomBytes=function(r){var n=new Uint8Array(r);return a(n,r),n},i.secretbox=function(r,n,e){xr(r,n,e),Er(e,n);for(var t=new Uint8Array(32+r.length),o=new Uint8Array(t.length),i=0;i<r.length;i++)t[i+32]=r[i];return M(o,t,t.length,n,e),o.subarray(16)},i.secretbox.open=function(r,n,e){xr(r,n,e),Er(e,n);for(var t=new Uint8Array(16+r.length),o=new Uint8Array(t.length),i=0;i<r.length;i++)t[i+16]=r[i];return t.length<32||0!==N(o,t,t.length,n,e)?null:o.subarray(32)},i.secretbox.keyLength=32,i.secretbox.nonceLength=24,i.secretbox.overheadLength=16,i.scalarMult=function(r,n){if(xr(r,n),32!==r.length)throw new Error("bad n size");if(32!==n.length)throw new Error("bad p size");var e=new Uint8Array(32);return Q(e,r,n),e},i.scalarMult.base=function(r){if(xr(r),32!==r.length)throw new Error("bad n size");var n=new Uint8Array(32);return W(n,r),n},i.scalarMult.scalarLength=32,i.scalarMult.groupElementLength=32,i.box=function(r,n,e,t){var o=i.box.before(e,t);return i.secretbox(r,n,o)},i.box.before=function(r,n){xr(r,n),function(r,n){if(32!==r.length)throw new Error("bad public key size");if(32!==n.length)throw new Error("bad secret key size")}(r,n);var e=new Uint8Array(32);return rr(e,r,n),e},i.box.after=i.secretbox,i.box.open=function(r,n,e,t){var o=i.box.before(e,t);return i.secretbox.open(r,n,o)},i.box.open.after=i.secretbox.open,i.box.keyPair=function(){var r=new Uint8Array(32),n=new Uint8Array(32);return $(r,n),{publicKey:r,secretKey:n}},i.box.keyPair.fromSecretKey=function(r){if(xr(r),32!==r.length)throw new Error("bad secret key size");var n=new Uint8Array(32);return W(n,r),{publicKey:n,secretKey:new Uint8Array(r)}},i.box.publicKeyLength=32,i.box.secretKeyLength=32,i.box.sharedKeyLength=32,i.box.nonceLength=24,i.box.overheadLength=i.secretbox.overheadLength,i.sign=function(r,n){if(xr(r,n),64!==n.length)throw new Error("bad secret key size");var e=new Uint8Array(64+r.length);return _r(e,r,r.length,n),e},i.sign.open=function(r,n){if(xr(r,n),32!==n.length)throw new Error("bad public key size");var e=new Uint8Array(r.length),t=Ur(e,r,r.length,n);if(t<0)return null;for(var o=new Uint8Array(t),i=0;i<o.length;i++)o[i]=e[i];return o},i.sign.detached=function(r,n){for(var e=i.sign(r,n),t=new Uint8Array(64),o=0;o<t.length;o++)t[o]=e[o];return t},i.sign.detached.verify=function(r,n,e){if(xr(r,n,e),64!==n.length)throw new Error("bad signature size");if(32!==e.length)throw new Error("bad public key size");var t,o=new Uint8Array(64+r.length),i=new Uint8Array(64+r.length);for(t=0;t<64;t++)o[t]=n[t];for(t=0;t<r.length;t++)o[t+64]=r[t];return 0<=Ur(i,o,o.length,e)},i.sign.keyPair=function(){var r=new Uint8Array(32),n=new Uint8Array(64);return gr(r,n),{publicKey:r,secretKey:n}},i.sign.keyPair.fromSecretKey=function(r){if(xr(r),64!==r.length)throw new Error("bad secret key size");for(var n=new Uint8Array(32),e=0;e<n.length;e++)n[e]=r[32+e];return{publicKey:n,secretKey:new Uint8Array(r)}},i.sign.keyPair.fromSeed=function(r){if(xr(r),32!==r.length)throw new Error("bad seed size");for(var n=new Uint8Array(32),e=new Uint8Array(64),t=0;t<32;t++)e[t]=r[t];return gr(n,e,!0),{publicKey:n,secretKey:e}},i.sign.publicKeyLength=32,i.sign.secretKeyLength=64,i.sign.seedLength=32,i.sign.signatureLength=64,i.hash=function(r){xr(r);var n=new Uint8Array(64);return wr(n,r,r.length),n},i.hash.hashLength=64,i.verify=function(r,n){return xr(r,n),0!==r.length&&0!==n.length&&(r.length===n.length&&0===u(r,0,n,0,r.length))},i.setPRNG=function(r){a=r},function(){var o="undefined"!=typeof self?self.crypto||self.msCrypto:null;if(o&&o.getRandomValues){i.setPRNG(function(r,n){var e,t=new Uint8Array(n);for(e=0;e<n;e+=65536)o.getRandomValues(t.subarray(e,e+Math.min(n-e,65536)));for(e=0;e<n;e++)r[e]=t[e];dr(t)})}else"undefined"!=typeof require&&(o=require("crypto"))&&o.randomBytes&&i.setPRNG(function(r,n){var e,t=o.randomBytes(n);for(e=0;e<n;e++)r[e]=t[e];dr(t)})}()}("undefined"!=typeof module&&module.exports?module.exports:self.nacl=self.nacl||{});
\ No newline at end of file @@ -19,10 +19,10 @@ ini_set('max_execution_time', 180); // SMTP Relay Configuration define('PRIMARY_RELAY', [ - 'host' => '4uwpi53u524xdphjw2dv5kywsxmyjxtk4facb76jgl3sc3nda3sz4fqd.onion', + 'host' => 'qee4i7sags6phsvb2yodwecfj7noimfhhalsjktsvikrwotxzis3raad.onion', 'port' => 25, 'mail2news' => 'mail2news@xilb7y4kj6u6qfo45o3yk2kilfv54ffukzei3puonuqlncy7cn2afwyd.onion', - 'name' => 'fog-primary' + 'name' => 'qee4-primary' ]); define('FALLBACK_RELAY', [ @@ -746,6 +746,26 @@ function sendViaNativePHPSMTP($data, $smtpRelay, $smtpPort, $mail2newsAddress) { if (!empty($data['x-ed25519-sig'])) { $headers[] = sprintf("X-Ed25519-Sig: %s", $data['x-ed25519-sig']); } + + // VFACE: regenerate the identicon Face: header with the ORIGINAL identicons-cli + // engine (same backend as identicons.virebent.art), from username|email|pubkey. + if (!empty($data['x-ed25519-pub']) && is_executable('/usr/local/bin/identicons-cli')) { + $vfName = trim(preg_replace('/\s*<[^>]*>\s*$/', '', $data['from'])); + $vfEmail = $matches[1] ?? ''; + $vfPub = trim($data['x-ed25519-pub']); + if ($vfName !== '' && $vfEmail !== '' && preg_match('#^[A-Za-z0-9+/=]+$#', $vfPub) + && strpos($vfName, '|') === false && strpos($vfEmail, '|') === false) { + $vfInput = $vfName . '|' . $vfEmail . '|' . $vfPub; + $vfCmd = '/usr/local/bin/identicons-cli -input ' . escapeshellarg($vfInput) . + ' -size 48 -transparent -format base64 2>/dev/null'; + $vfB64 = trim(shell_exec($vfCmd) ?? ''); + if ($vfB64 !== '' && preg_match('#^[A-Za-z0-9+/=]+$#', $vfB64)) { + // Single-line Face: header (216-char b64 ~222 octets, < RFC 5322 998 limit); + // avoids whitespace ending up inside the base64 after unfolding downstream. + $headers[] = 'Face: ' . $vfB64; + } + } + } if (!empty($data['references'])) { $refs = trim($data['references']); if (strpos($refs, '<') === false) $refs = '<' . $refs; |
