// /home/m2usenet/m2usenet.go // 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" "io" "log" "math/big" "net/mail" "os" "path/filepath" "strconv" "strings" "sync" "time" "golang.org/x/net/proxy" ) var ( // 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")) 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" ) func getEnv(key, def string) string { if v, ok := os.LookupEnv(key); ok { return v } return def } // 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...) } } // 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() db, err := loadTokenDB() if err != nil { return false } 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 registers token atomically with expiration func markTokenSpent(token string) error { tokenMutex.Lock() defer tokenMutex.Unlock() db, err := loadTokenDB() if err != nil { db = make(TokenDB) } // 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) } // 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) { return db, nil } data, err := os.ReadFile(dbPath) if err != nil { return nil, err } if len(data) == 0 { return db, nil } if err := json.Unmarshal(data, &db); err != nil { return nil, err } return db, nil } func saveTokenDB(db TokenDB) error { dir := filepath.Dir(dbPath) 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, 0600) } func parseHashcashDate(dateStr string) (time.Time, error) { layouts := []string{"0601021504", "060102150405"} var t time.Time var err error for _, layout := range layouts { t, err = time.Parse(layout, dateStr) if err == nil { return t, nil } } return t, errors.New("invalid date format") } // verifyHashcashToken with constant-time comparisons where possible func verifyHashcashToken(token, fromAddr string) bool { parts := strings.Split(token, ":") if len(parts) != 7 { 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] // 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 { secureLog("Invalid bits") return false } // 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 { secureLog("Invalid token date") return false } now := time.Now().UTC() timeDiff := now.Sub(tokenTime) if timeDiff < 0 { timeDiff = -timeDiff } if timeDiff > time.Duration(timeWindowSec)*time.Second { secureLog("Token outside time window") return false } // 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) { secureLog("Hash verification failed") return false } return true } // 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("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("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, fmt.Errorf("welcome read error: %v", err) } 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 } if !strings.HasPrefix(ihaveResp, "335") { 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 } 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() { // 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("Failed to read input") } msg, err := mail.ReadMessage(bytes.NewReader(input)) if err != nil { log.Fatalf("Failed to parse email") } header := msg.Header fromHeader := header.Get("From") fromAddr, err := safeParseFrom(fromHeader) if err != nil { log.Fatalf("Invalid From header") } newsgroups := header.Get("Newsgroups") if newsgroups == "" { log.Fatalf("Missing Newsgroups header") } subject := header.Get("Subject") if subject == "" { subject = "(No subject)" } xhashcash := header.Get("X-Hashcash") if xhashcash == "" { log.Fatalf("Missing X-Hashcash header") } // Read body bodyBuf := new(bytes.Buffer) if _, err = io.Copy(bodyBuf, msg.Body); err != nil { log.Fatalf("Failed to read body") } body := strings.TrimSpace(bodyBuf.String()) if body == "" { log.Fatalf("Empty message body") } // ATOMIC token check and mark tokenMutex.Lock() if tokenAlreadySpent(xhashcash) { tokenMutex.Unlock() log.Fatalf("Token already used") } if !verifyHashcashToken(xhashcash, fromAddr) { tokenMutex.Unlock() log.Fatalf("Invalid hashcash token") } if err := markTokenSpent(xhashcash); err != nil { tokenMutex.Unlock() log.Fatalf("Failed to register token") } 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] secureLog("Limited to 3 newsgroups") } limitedGroups := strings.Join(groups, ", ") // Randomized delay for cross-posting if len(groups) > 1 { randomDelay(delayCrossPost) } // Generate secure Message-ID messageID, err := generateSecureMessageID() if err != nil { log.Fatalf("Failed to generate Message-ID") } dateHeader := time.Now().UTC().Format(time.RFC1123Z) // Build message headers := []string{ fmt.Sprintf("Message-ID: %s", messageID), fmt.Sprintf("Date: %s", dateHeader), fmt.Sprintf("From: %s", fromHeader), fmt.Sprintf("Newsgroups: %s", limitedGroups), fmt.Sprintf("Subject: %s", subject), "Path: mail2usenet", "Organization: Victor Hostile Communication Center 1312", fmt.Sprintf("X-Hashcash: %s", xhashcash), "X-No-Archive: Yes", "Mime-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", "Content-Transfer-Encoding: 7bit", "User-Agent: m2usenet-go v1.0.0", } // 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 // 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("Send failed: %v", err) } if success { secureLog("Message sent successfully") os.Exit(0) } else { log.Fatalln("Message delivery failed") } }