summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2026-01-23 18:21:46 +0100
committerGitHub <noreply@github.com>2026-01-23 18:21:46 +0100
commit322a17dba0d44200f6c241b18a58761220050aba (patch)
treebeaa47d7b7c5fe30e9dea95798d31d8320f508ae
parent24b6948f0a1d63a28932b5306fa65bd09f668a97 (diff)
downloadm2usenet-and-mail2news-322a17dba0d44200f6c241b18a58761220050aba.tar.gz
m2usenet-and-mail2news-322a17dba0d44200f6c241b18a58761220050aba.tar.xz
m2usenet-and-mail2news-322a17dba0d44200f6c241b18a58761220050aba.zip
Update print statement from 'Hello' to 'Goodbye'
-rw-r--r--index1648
1 files changed, 1648 insertions, 0 deletions
diff --git a/index b/index
new file mode 100644
index 0000000..1a9663c
--- /dev/null
+++ b/index
@@ -0,0 +1,1648 @@
+<?php
+// /var/www/m2usenet/index.php
+// m2usenet v2.6.0 - Face Header + Identity Hash v2
+
+if (session_status() === PHP_SESSION_NONE) {
+ ini_set('session.cookie_httponly', 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();
+}
+
+if (!isset($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+}
+$csrfToken = $_SESSION['csrf_token'];
+?>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>m2usenet Gateway v2.6</title>
+ <style>
+ :root {
+ --background: #f9f9f9;
+ --card-bg: #fff;
+ --text: #333;
+ --border: #ccc;
+ --primary: #4caf50;
+ --primary-hover: #45a049;
+ --tab-bg: #eee;
+ --input-bg: #fff;
+ --input-readonly: #eee;
+ --progress-bg: #ddd;
+ --success: #28a745;
+ --warning: #ffc107;
+ --error: #dc3545;
+ }
+
+ .dark-theme {
+ --background: #1a1a1a;
+ --card-bg: #2c2c2c;
+ --text: #e0e0e0;
+ --border: #444;
+ --primary: #5cbb60;
+ --primary-hover: #4caf50;
+ --tab-bg: #333;
+ --input-bg: #3c3c3c;
+ --input-readonly: #2a2a2a;
+ --progress-bg: #444;
+ }
+
+ body {
+ background: var(--background);
+ color: var(--text);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ margin: 0;
+ padding: 0;
+ transition: background 0.3s ease;
+ }
+
+ .container {
+ max-width: 800px;
+ margin: auto;
+ padding: 20px;
+ }
+
+ header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ }
+
+ .theme-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .toggle {
+ position: relative;
+ width: 60px;
+ height: 30px;
+ background: #ccc;
+ border-radius: 30px;
+ padding: 4px;
+ cursor: pointer;
+ transition: 0.3s;
+ }
+
+ .toggle:before {
+ content: '';
+ position: absolute;
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ background: white;
+ top: 2px;
+ left: 2px;
+ transition: 0.3s;
+ }
+
+ input[type="checkbox"]:checked + .toggle {
+ background: #4caf50;
+ }
+
+ input[type="checkbox"]:checked + .toggle:before {
+ transform: translateX(30px);
+ }
+
+ input[type="checkbox"] {
+ display: none;
+ }
+
+ .notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ max-width: 400px;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ z-index: 10000;
+ animation: slideIn 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .notification.success { background: var(--success); color: white; }
+ .notification.error { background: var(--error); color: white; }
+ .notification.warning { background: var(--warning); color: #333; }
+ .notification.info { background: #007bff; color: white; }
+
+ @keyframes slideIn {
+ from { transform: translateX(400px); opacity: 0; }
+ 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; }
+
+ .section-info {
+ background: var(--card-bg);
+ border-left: 4px solid var(--primary);
+ padding: 10px 15px;
+ margin-bottom: 15px;
+ border-radius: 0 4px 4px 0;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ }
+
+ .tabs {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 15px;
+ }
+
+ .tabs button {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 12px 15px;
+ border: none;
+ background: var(--tab-bg);
+ color: var(--text);
+ cursor: pointer;
+ border-radius: 4px;
+ font-weight: bold;
+ transition: background 0.2s, transform 0.1s;
+ position: relative;
+ }
+
+ .tabs button:hover { background: var(--primary); color: white; }
+ .tabs button:active { transform: scale(0.98); }
+
+ .tabs button.completed::after {
+ content: '✓';
+ position: absolute;
+ right: 15px;
+ background: var(--success);
+ color: white;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.9em;
+ }
+
+ .tab-content {
+ display: none !important;
+ padding: 20px;
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ margin-bottom: 20px;
+ }
+
+ .tab-content.active {
+ display: block !important;
+ }
+
+ label {
+ display: block;
+ margin-top: 15px;
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+
+ input, textarea, select {
+ width: 100%;
+ padding: 10px;
+ margin-top: 5px;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--input-bg);
+ color: var(--text);
+ font-family: inherit;
+ }
+
+ input:focus, textarea:focus, select:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
+ }
+
+ input.error, textarea.error { border-color: var(--error); }
+
+ input[readonly], textarea[readonly] {
+ background: var(--input-readonly);
+ cursor: not-allowed;
+ border: 2px solid #999;
+ }
+
+ input.prefilled {
+ background: #e8f5e9;
+ border: 2px solid var(--primary);
+ }
+
+ .dark-theme input.prefilled {
+ background: #1b3d1b;
+ }
+
+ button {
+ margin-top: 15px;
+ padding: 12px 20px;
+ background: var(--primary);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: bold;
+ transition: background 0.2s, opacity 0.2s;
+ font-family: inherit;
+ }
+
+ 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);
+ border-radius: 5px;
+ overflow: hidden;
+ height: 20px;
+ margin-top: 10px;
+ }
+
+ .progress-bar-inner {
+ height: 100%;
+ width: 0;
+ background: var(--primary);
+ text-align: center;
+ color: white;
+ line-height: 20px;
+ transition: width 0.3s;
+ font-size: 0.85em;
+ }
+
+ .output-field {
+ margin-top: 15px;
+ padding: 10px;
+ background: var(--input-readonly);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ word-break: break-all;
+ min-height: 30px;
+ font-family: monospace;
+ font-size: 0.9em;
+ }
+
+ .output-field.empty { color: #999; font-style: italic; }
+
+ .gateway-info {
+ background: var(--card-bg);
+ border: 1px solid var(--border);
+ padding: 15px;
+ margin: 15px 0;
+ border-radius: 4px;
+ border-left: 4px solid #007bff;
+ }
+
+ .gateway-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .gateway-item:last-child { border-bottom: none; }
+
+ .gateway-priority {
+ background: #007bff;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.8em;
+ margin-right: 10px;
+ min-width: 70px;
+ text-align: center;
+ }
+
+ .gateway-address {
+ font-family: monospace;
+ background: var(--input-readonly);
+ padding: 4px 8px;
+ border-radius: 4px;
+ flex: 1;
+ font-size: 0.85em;
+ }
+
+ .prefill-banner {
+ background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
+ color: white;
+ padding: 12px 15px;
+ border-radius: 4px;
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
+ }
+
+ .prefill-banner.reply {
+ background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
+ }
+
+ .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;
+ }
+
+ .keypair-actions button {
+ flex: 1;
+ min-width: 150px;
+ }
+
+ .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;
+ }
+
+ .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 {
+ text-align: center;
+ margin-top: 40px;
+ padding-top: 20px;
+ border-top: 1px solid var(--border);
+ font-size: 0.9em;
+ color: #888;
+ }
+
+ .footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 10px;
+ }
+
+ .footer-links a {
+ color: var(--primary);
+ text-decoration: none;
+ transition: color 0.2s;
+ }
+
+ .footer-links a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+ }
+
+ @media (max-width: 600px) {
+ .container { padding: 10px; }
+ .tab-content { padding: 15px 10px; }
+ .footer-links { flex-direction: column; gap: 10px; }
+ .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>
+</head>
+<body>
+<div class="container">
+ <header>
+ <h1>m2usenet Gateway v2.6</h1>
+ <div class="theme-toggle">
+ <span>🌞</span>
+ <input type="checkbox" id="themeToggle">
+ <label for="themeToggle" class="toggle"></label>
+ <span>🌙</span>
+ </div>
+ </header>
+
+ <div id="prefillBanner" class="prefill-banner" style="display: none;">
+ <span class="prefill-banner-icon">📝</span>
+ <div class="prefill-banner-text">
+ <strong id="prefillTitle">New Post</strong>
+ <small id="prefillDetails">Fields pre-filled from Onion Newsreader</small>
+ </div>
+ </div>
+
+ <div class="tabs">
+ <button id="tabBtn1" onclick="showTab('pow')">
+ <span>1. Generate Hashcash Token</span>
+ </button>
+ <button id="tabBtn2" onclick="showTab('sign')">
+ <span>2. Sign Message (Identity)</span>
+ </button>
+ <button id="tabBtn3" onclick="showTab('send')">
+ <span>3. Send Message</span>
+ </button>
+ </div>
+
+ <div id="pow" class="tab-content active">
+ <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 Tor hidden services. No access logs are collected.</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>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>
+ <option value="20" selected>20 bits (fast, ~few seconds)</option>
+ <option value="24">24 bits (medium, ~30-60 seconds)</option>
+ <option value="28">28 bits (slow, ~several minutes)</option>
+ </select>
+ </label>
+ <button id="genTokenBtn">Generate Token</button>
+ <div class="progress-bar"><div id="tokenProgress" class="progress-bar-inner">Ready</div></div>
+ <label>Generated Token:</label>
+ <div id="tokenOutput" class="output-field empty">Token will appear here after generation</div>
+ </div>
+
+ <div id="sign" class="tab-content">
+ <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;"><small>Hash: <code id="identityHashDisplay"></code></small></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="/verify/" style="color: #9c27b0; text-decoration: none;">🔐 VFACE Verifier - Verify an identity</a>
+ </p>
+ </div>
+
+ <div class="section-info">
+ <p><strong>What is this?</strong> Your message will be signed with Ed25519. The signature proves you authored this post.</p>
+ <p><strong>Face Header:</strong> Your identicon is embedded as a <code>Face:</code> header (RFC 4021) visible in newsreaders like Newsgrouper.</p>
+ </div>
+
+ <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>
+
+ <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>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 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;">
+ <strong>SMTP Relays:</strong>
+ <p style="font-size: 0.85em; margin: 5px 0 0 0; color: #666;">Specialized SMTP relays operating over Tor hidden services</p>
+ </div>
+ </div>
+ <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><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.</p>
+ </div>
+ </div>
+
+ <div class="gateway-item">
+ <span class="gateway-priority">Step 3</span>
+ <div style="flex: 1;">
+ <strong>NNTP Server</strong>
+ <div class="gateway-address" style="margin-top: 5px;">nntp://peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</div>
+ <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 (SMTP → Mail2News → NNTP) operate as Tor hidden services, providing end-to-end anonymity.
+ </p>
+ </div>
+ </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):</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">
+ </label>
+ <label>Subject: <input type="text" name="subject" id="subject" required placeholder="Message Subject"></label>
+ <label>References (optional): <input type="text" name="references" id="references" placeholder="Message-ID of post you're replying to"></label>
+ <label>X-Hashcash Token: <input type="text" name="xhashcash" id="hcToken" required readonly></label>
+ <label>Message:</label>
+ <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 Message</button>
+ </form>
+ </div>
+
+ <footer>
+ <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="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%69%6E%66%6F%40%76%69%72%65%62%65%6E%74%2E%61%72%74">Contact</a>
+ <a href="https://github.com/gabrix73/m2usenet-go">Code</a>
+ </div>
+ </footer>
+</div>
+
+<script>
+// ============================================================================
+// 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');
+}
+
+function showNotification(message, type, duration) {
+ type = type || 'info';
+ duration = duration === undefined ? 5000 : duration;
+ const icons = { success: '✓', error: '✗', warning: '⚠', info: 'ℹ' };
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+ notification.innerHTML = `
+ <span class="notification-icon">${icons[type] || icons.info}</span>
+ <span class="notification-message">${message}</span>
+ <span class="notification-close" onclick="this.parentElement.remove()">×</span>
+ `;
+ document.body.appendChild(notification);
+ if (duration > 0) {
+ setTimeout(() => { if (notification.parentElement) notification.remove(); }, duration);
+ }
+}
+
+// ============================================================================
+// STATE MANAGEMENT
+// ============================================================================
+
+const appState = {
+ step1Complete: false,
+ step2Complete: false,
+ step3Complete: false,
+ identitySaved: false,
+ prefillMode: null
+};
+
+function updateTabIndicators() {
+ if (appState.step1Complete) document.getElementById('tabBtn1').classList.add('completed');
+ if (appState.step2Complete) document.getElementById('tabBtn2').classList.add('completed');
+ if (appState.step3Complete) document.getElementById('tabBtn3').classList.add('completed');
+}
+
+let keyPair = null;
+let workersSupported = true;
+
+try {
+ if (typeof Worker === 'undefined') {
+ workersSupported = false;
+ }
+} catch (e) {
+ workersSupported = false;
+}
+
+// ============================================================================
+// THEME TOGGLE
+// ============================================================================
+
+document.getElementById('themeToggle').addEventListener('change', function() {
+ document.body.classList.toggle('dark-theme', this.checked);
+ localStorage.setItem('darkTheme', this.checked);
+});
+
+// ============================================================================
+// IDENTICON GENERATION (client-side)
+// ============================================================================
+
+let currentIdenticonData = null;
+
+async function generateIdenticon(username, email, pubkeyBase64) {
+ const identityString = username + '|' + email + '|' + pubkeyBase64;
+ const encoder = new TextEncoder();
+ const data = encoder.encode(identityString);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
+ const hash = new Uint8Array(hashBuffer);
+ const hashHex = Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
+
+ const size = 128;
+ const gridSize = 5;
+ const padding = 0.08;
+
+ const hue = ((hash[0] << 8) | hash[1]) % 360;
+ const fgColor = `hsl(${hue}, 65%, 45%)`;
+ const bgHue = (hue + 180) % 360;
+ const bgColor = `hsl(${bgHue}, 15%, 92%)`;
+
+ const grid = [];
+ const halfWidth = Math.ceil(gridSize / 2);
+ let bitIndex = 16;
+
+ for (let row = 0; row < gridSize; row++) {
+ grid[row] = [];
+ for (let col = 0; col < halfWidth; col++) {
+ const byteIndex = Math.floor(bitIndex / 8);
+ const bitOffset = bitIndex % 8;
+ const bit = (hash[byteIndex] >> bitOffset) & 1;
+ grid[row][col] = bit === 1;
+ bitIndex++;
+ }
+ for (let col = halfWidth; col < gridSize; col++) {
+ grid[row][col] = grid[row][gridSize - 1 - col];
+ }
+ }
+
+ const canvas = document.createElement('canvas');
+ canvas.width = size;
+ canvas.height = size;
+ const ctx = canvas.getContext('2d');
+
+ const paddingPx = Math.floor(size * padding);
+ const cellSize = Math.floor((size - paddingPx * 2) / gridSize);
+ const offset = paddingPx + Math.floor((size - paddingPx * 2 - cellSize * gridSize) / 2);
+
+ ctx.fillStyle = bgColor;
+ ctx.fillRect(0, 0, size, size);
+
+ ctx.fillStyle = fgColor;
+ for (let row = 0; row < gridSize; row++) {
+ for (let col = 0; col < gridSize; col++) {
+ if (grid[row][col]) {
+ ctx.fillRect(offset + col * cellSize, offset + row * cellSize, cellSize, cellSize);
+ }
+ }
+ }
+
+ const dataUrl = canvas.toDataURL('image/png');
+
+ currentIdenticonData = {
+ dataUrl: dataUrl,
+ hash: hashHex,
+ username: username,
+ email: email,
+ pubkey: pubkeyBase64,
+ claim: JSON.stringify({
+ version: 2,
+ type: 'm2usenet-identity',
+ username: username,
+ email: email,
+ pubkey: pubkeyBase64,
+ identityHash: hashHex,
+ created: new Date().toISOString(),
+ algorithm: 'sha256(username|email|pubkey)'
+ }, null, 2)
+ };
+
+ return dataUrl;
+}
+
+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.substring(0, 16) + '...';
+ 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
+// ============================================================================
+
+document.addEventListener('DOMContentLoaded', function() {
+ const darkTheme = localStorage.getItem('darkTheme') === 'true';
+ document.getElementById('themeToggle').checked = darkTheme;
+ document.body.classList.toggle('dark-theme', darkTheme);
+
+ if (typeof nacl === 'undefined') {
+ showNotification('Cryptography library failed to load. Please refresh the page.', 'error', 0);
+ return;
+ }
+
+ if (typeof nacl.util === 'undefined') {
+ nacl.util = {
+ decodeUTF8: function(str) { return new TextEncoder().encode(str); },
+ encodeUTF8: function(arr) { return new TextDecoder().decode(arr); },
+ encodeBase64: function(arr) {
+ return btoa(Array.from(new Uint8Array(arr)).map(byte => String.fromCharCode(byte)).join(''));
+ },
+ decodeBase64: function(b64) {
+ const bin = atob(b64);
+ const arr = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
+ return arr;
+ }
+ };
+ }
+
+ showTab('pow');
+ showNotification('m2usenet Gateway v2.6.0 ready', 'success', 3000);
+});
+
+// ============================================================================
+// FORM SUBMISSION PROTECTION
+// ============================================================================
+
+document.addEventListener('DOMContentLoaded', function() {
+ const form = document.querySelector('form');
+ const submitButton = form.querySelector('button[type="submit"]');
+
+ if (!submitButton) return;
+
+ let isSubmitting = false;
+
+ form.addEventListener('submit', function(e) {
+ if (isSubmitting) {
+ e.preventDefault();
+ return false;
+ }
+
+ isSubmitting = true;
+ submitButton.disabled = true;
+ submitButton.textContent = '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 up to 60 seconds via Tor. Please wait.';
+ submitButton.parentNode.insertBefore(progressDiv, submitButton.nextSibling);
+
+ return true;
+ });
+});
+
+// ============================================================================
+// SHA-1 IMPLEMENTATION
+// ============================================================================
+
+function sha1(str) {
+ function rotate_left(n, s) { return (n << s) | (n >>> (32 - s)); }
+ function cvt_hex(val) {
+ let str = '';
+ for (let i = 7; i >= 0; i--) {
+ const v = (val >>> (i * 4)) & 0x0f;
+ str += v.toString(16);
+ }
+ return str;
+ }
+ function utf8Encode(str) { return unescape(encodeURIComponent(str)); }
+
+ let blockstart, i, j;
+ const W = new Array(80);
+ let H0 = 0x67452301, H1 = 0xEFCDAB89, H2 = 0x98BADCFE, H3 = 0x10325476, H4 = 0xC3D2E1F0;
+ let A, B, C, D, E, temp;
+
+ str = utf8Encode(str);
+ const str_len = str.length;
+ const word_array = [];
+
+ for (i = 0; i < str_len - 3; i += 4) {
+ j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3);
+ word_array.push(j);
+ }
+
+ switch (str_len % 4) {
+ case 0: i = 0x080000000; break;
+ case 1: i = str.charCodeAt(str_len - 1) << 24 | 0x0800000; break;
+ case 2: i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000; break;
+ case 3: i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) << 8 | 0x80; break;
+ }
+
+ word_array.push(i);
+ while ((word_array.length % 16) != 14) word_array.push(0);
+ word_array.push(str_len >>> 29);
+ word_array.push((str_len << 3) & 0x0ffffffff);
+
+ for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
+ for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
+ for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
+
+ A = H0; B = H1; C = H2; D = H3; E = H4;
+
+ for (i = 0; i <= 19; i++) {
+ temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+ for (i = 20; i <= 39; i++) {
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+ for (i = 40; i <= 59; i++) {
+ temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+ for (i = 60; i <= 79; i++) {
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
+ E = D; D = C; C = rotate_left(B, 30); B = A; A = temp;
+ }
+
+ H0 = (H0 + A) & 0x0ffffffff;
+ H1 = (H1 + B) & 0x0ffffffff;
+ H2 = (H2 + C) & 0x0ffffffff;
+ H3 = (H3 + D) & 0x0ffffffff;
+ H4 = (H4 + E) & 0x0ffffffff;
+ }
+
+ return cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4);
+}
+
+function mineSingleThread(prefix, targetZeros, progressCallback, foundCallback) {
+ const target = '0'.repeat(targetZeros);
+ let nonce = 0, checked = 0;
+ const batchSize = 100;
+
+ function mineNextBatch() {
+ const endNonce = nonce + batchSize;
+ while (nonce < endNonce) {
+ const token = prefix + nonce;
+ const hash = sha1(token);
+ checked++;
+ if (hash.startsWith(target)) {
+ foundCallback(nonce, checked);
+ return;
+ }
+ nonce++;
+ }
+ progressCallback(checked);
+ setTimeout(mineNextBatch, 0);
+ }
+ mineNextBatch();
+}
+
+// ============================================================================
+// STEP 1: HASHCASH TOKEN GENERATION
+// ============================================================================
+
+document.getElementById('genTokenBtn').onclick = () => {
+ const email = document.getElementById('hcEmail').value.trim();
+ const bits = parseInt(document.getElementById('hcBits').value);
+
+ if (!email) {
+ showNotification('⚠ Please enter your email address first!', 'warning');
+ document.getElementById('hcEmail').classList.add('error');
+ document.getElementById('hcEmail').focus();
+ return;
+ }
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ showNotification('⚠ Please enter a valid email address!', 'warning');
+ document.getElementById('hcEmail').classList.add('error');
+ document.getElementById('hcEmail').focus();
+ return;
+ }
+
+ document.getElementById('hcEmail').classList.remove('error');
+ showNotification(`🔨 Starting token generation (${bits} bits)...`, 'info');
+
+ const btn = document.getElementById('genTokenBtn');
+ btn.disabled = true;
+ btn.textContent = 'Mining Token...';
+
+ const now = new Date();
+ const timestamp = now.getUTCFullYear().toString().slice(2) +
+ ("0"+(now.getUTCMonth()+1)).slice(-2) +
+ ("0"+now.getUTCDate()).slice(-2) +
+ ("0"+now.getUTCHours()).slice(-2) +
+ ("0"+now.getUTCMinutes()).slice(-2) +
+ ("0"+now.getUTCSeconds()).slice(-2);
+ const ext = "";
+ const rand = Math.floor(Math.random()*1e6);
+ const prefix = `1:${bits}:${timestamp}:${email}:${ext}:${rand}:`;
+ const zeros = bits/4;
+
+ const progBar = document.getElementById('tokenProgress');
+ progBar.style.width = '0%';
+ progBar.innerText = 'Mining...';
+
+ let totalChecked = 0;
+ let startTime = Date.now();
+
+ function updateProgress(checked) {
+ totalChecked += checked;
+ const elapsed = (Date.now() - startTime) / 1000;
+ const hashRate = Math.round(totalChecked / elapsed);
+ progBar.style.width = Math.min(95, (totalChecked / 10000)) + '%';
+ progBar.innerText = `${totalChecked} hashes (${hashRate}/s)`;
+ }
+
+ function onFound(nonce, totalHashes) {
+ const token = prefix + nonce;
+ const outputField = document.getElementById('tokenOutput');
+ outputField.innerText = token;
+ outputField.classList.remove('empty');
+ document.getElementById('hcToken').value = token;
+
+ 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;
+
+ btn.disabled = false;
+ btn.textContent = 'Generate Token';
+ progBar.style.width = '100%';
+ progBar.innerText = `Complete! (${totalHashes} hashes)`;
+
+ appState.step1Complete = true;
+ updateTabIndicators();
+
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ showNotification(`✓ Token generated in ${elapsed}s!`, 'success', 8000);
+
+ setTimeout(() => showTab('sign'), 2000);
+ }
+
+ if (workersSupported) {
+ try {
+ const cores = navigator.hardwareConcurrency || 2;
+ let found = false;
+ let coreChecked = Array(cores).fill(0);
+ const workers = [];
+
+ function updateCombinedProgress() {
+ const sum = coreChecked.reduce((a,b)=>a+b,0);
+ updateProgress(sum - totalChecked);
+ }
+
+ for (let i = 0; i < cores; i++) {
+ try {
+ const w = new Worker('powWorker.js');
+ w.postMessage({ prefix, targetZeros: zeros, startNonce: i, step: cores });
+
+ w.onmessage = e => {
+ if (found) return;
+ if (e.data.type === 'found') {
+ found = true;
+ workers.forEach(x => x.terminate());
+ onFound(e.data.nonce, e.data.checked);
+ }
+ if (e.data.type === 'progress') {
+ coreChecked[i] = e.data.checked;
+ updateCombinedProgress();
+ }
+ };
+
+ w.onerror = (err) => {
+ if (i === 0) {
+ workers.forEach(x => x.terminate());
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ }
+ };
+
+ workers.push(w);
+ } catch (err) {
+ if (i === 0) {
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ return;
+ }
+ }
+ }
+
+ } catch (err) {
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ }
+ } else {
+ mineSingleThread(prefix, zeros, updateProgress, onFound);
+ }
+};
+
+// ============================================================================
+// STEP 2: ED25519 SIGNATURE + KEYPAIR MANAGEMENT
+// ============================================================================
+
+document.getElementById('genKeyBtn').onclick = async function() {
+ try {
+ showNotification('🔐 Generating Ed25519 key pair...', 'info');
+ keyPair = nacl.sign.keyPair();
+
+ const pubB64 = nacl.util.encodeBase64(keyPair.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;
+
+ 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();
+
+ showNotification('✓ Key pair generated! Write your message and sign it.', 'success', 6000);
+ } catch (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';
+
+ 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,
+ algorithm: 'sha256(username|email|pubkey)',
+ 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;
+
+ // 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 or load a key pair first!', 'warning');
+ return;
+ }
+
+ const msg = document.getElementById('messageToSign').value.trim();
+ if (!msg) {
+ showNotification('⚠ Please write a message!', 'warning');
+ document.getElementById('messageToSign').classList.add('error');
+ document.getElementById('messageToSign').focus();
+ return;
+ }
+
+ if (msg.length < 10) {
+ showNotification('⚠ Message too short (min 10 characters)', 'warning');
+ return;
+ }
+
+ document.getElementById('messageToSign').classList.remove('error');
+ showNotification('✍ Signing message...', 'info');
+
+ const msgBytes = nacl.util.decodeUTF8(msg);
+ const sig = nacl.sign.detached(msgBytes, keyPair.secretKey);
+ const sigB64 = nacl.util.encodeBase64(sig);
+
+ const sigOutput = document.getElementById('signatureOutput');
+ sigOutput.innerText = sigB64;
+ sigOutput.classList.remove('empty');
+ document.getElementById('x-ed25519-sig').value = sigB64;
+
+ document.getElementById('messageContent').value = msg + "\n\n--- Digital Signature ---\n" + sigB64;
+
+ appState.step2Complete = true;
+ updateTabIndicators();
+
+ // 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) {
+ 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: 'fromFull', name: 'From'},
+ {id: 'newsgroups', name: 'Newsgroups'},
+ {id: 'subject', name: 'Subject'},
+ {id: 'hcToken', name: 'Hashcash Token'},
+ {id: 'messageContent', name: 'Message'},
+ {id: 'x-ed25519-pub', name: 'Public Key'},
+ {id: 'x-ed25519-sig', name: 'Signature'}
+ ];
+
+ const missing = [];
+ requiredFields.forEach(field => {
+ const el = document.getElementById(field.id);
+ if (!el.value.trim()) {
+ missing.push(field.name);
+ el.classList.add('error');
+ } else {
+ el.classList.remove('error');
+ }
+ });
+
+ if (missing.length > 0) {
+ e.preventDefault();
+ showNotification(`⚠ Missing: ${missing.join(', ')}`, 'warning', 8000);
+ return false;
+ }
+
+ const messageLen = document.getElementById('messageContent').value.length;
+ if (messageLen < 10) {
+ e.preventDefault();
+ showNotification('⚠ Message too short', 'warning');
+ return false;
+ }
+
+ const newsgroups = document.getElementById('newsgroups').value.split(',').map(g => g.trim()).filter(g => g);
+ if (newsgroups.length === 0) {
+ e.preventDefault();
+ showNotification('⚠ Enter at least one newsgroup!', 'warning');
+ return false;
+ }
+
+ if (newsgroups.length > 3) {
+ e.preventDefault();
+ showNotification('⚠ Max 3 newsgroups!', 'warning');
+ return false;
+ }
+
+ 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 message...', 'info', 0);
+ document.getElementById('sendBtn').disabled = true;
+ document.getElementById('sendBtn').textContent = 'Sending...';
+
+ return true;
+});
+
+// ============================================================================
+// ONION-NEWSREADER INTEGRATION
+// ============================================================================
+
+(function() {
+ function decode(s) {
+ if (!s) return '';
+ try {
+ return decodeURIComponent(s.replace(/\+/g, ' '));
+ } catch(e) {
+ return s;
+ }
+ }
+
+ 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');
+ return true;
+ }
+ return false;
+ }
+
+ function showPrefillBanner(isReply, details) {
+ const banner = document.getElementById('prefillBanner');
+ const title = document.getElementById('prefillTitle');
+ const detailsEl = document.getElementById('prefillDetails');
+
+ if (banner) {
+ banner.style.display = 'flex';
+ if (isReply) {
+ banner.classList.add('reply');
+ title.textContent = '💬 Reply Mode';
+ } else {
+ title.textContent = '📝 New Post';
+ }
+ detailsEl.textContent = details;
+ }
+ }
+
+ let prefillData = null;
+ let prefillSource = null;
+
+ try {
+ const storedData = localStorage.getItem('m2usenet_prefill');
+ if (storedData) {
+ prefillData = JSON.parse(storedData);
+ prefillSource = 'localStorage';
+ localStorage.removeItem('m2usenet_prefill');
+
+ if (prefillData.newsgroups) prefillData.newsgroups = decode(prefillData.newsgroups);
+ if (prefillData.subject) prefillData.subject = decode(prefillData.subject);
+ if (prefillData.references) prefillData.references = decode(prefillData.references);
+ }
+ } catch (e) {
+ localStorage.removeItem('m2usenet_prefill');
+ }
+
+ if (!prefillData) {
+ const params = new URLSearchParams(window.location.search);
+ const action = params.get('action');
+
+ if (action === 'new' || action === 'reply' || params.has('newsgroups')) {
+ prefillData = {
+ newsgroups: decode(params.get('newsgroups') || ''),
+ subject: decode(params.get('subject') || ''),
+ references: decode(params.get('references') || ''),
+ body: decode(params.get('body') || '')
+ };
+ prefillSource = 'URL';
+ }
+ }
+
+ if (prefillData) {
+ let filledFields = [];
+
+ if (prefillData.newsgroups) {
+ fillField('newsgroups', prefillData.newsgroups);
+ filledFields.push('Newsgroups');
+ }
+
+ if (prefillData.subject) {
+ fillField('subject', prefillData.subject);
+ filledFields.push('Subject');
+ }
+
+ if (prefillData.references) {
+ fillField('references', prefillData.references);
+ filledFields.push('References');
+ }
+
+ if (prefillData.body) {
+ fillField('messageToSign', prefillData.body);
+ filledFields.push('Quoted message');
+ }
+
+ const isReply = !!(prefillData.references ||
+ (prefillData.subject && prefillData.subject.toLowerCase().startsWith('re:')));
+
+ if (filledFields.length > 0) {
+ const details = `Pre-filled: ${filledFields.join(', ')} (from ${prefillSource})`;
+ showPrefillBanner(isReply, details);
+
+ if (isReply) {
+ showNotification(`💬 Reply mode: ${prefillData.newsgroups}`, 'info', 5000);
+ } else {
+ showNotification(`📝 New post to: ${prefillData.newsgroups}`, 'success', 5000);
+ }
+ }
+
+ setTimeout(() => showTab('pow'), 300);
+ }
+})();
+</script>
+
+</body>
+</html>