summaryrefslogtreecommitdiffstats
path: root/m2usenet.go
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2026-06-29 00:13:23 +0200
committerGab <24553253+gabrix73@users.noreply.github.com>2026-06-29 00:13:23 +0200
commit1d1d06c025be8b16a3535ecceffc26fe303977db (patch)
treecc814b265bcaa2711f092f53349cc66068a6256a /m2usenet.go
parent322a17dba0d44200f6c241b18a58761220050aba (diff)
downloadm2usenet-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
Diffstat (limited to 'm2usenet.go')
-rw-r--r--m2usenet.go476
1 files changed, 370 insertions, 106 deletions
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
-}