From aa459e3b84cc4c5d908f3781c9253848c18640c3 Mon Sep 17 00:00:00 2001 From: Gab Virebent Date: Sat, 20 Jun 2026 01:07:45 +0000 Subject: Initial public release: Nym-native anonymous submission system End-to-end verified pipeline (browser -> HTTP relay -> Nym mixnet -> reader). Client-side X25519+HKDF+AES-GCM-256, no-log blind relay, AGPL-3.0. --- cmd/nymdrop-source/main.go | 257 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 cmd/nymdrop-source/main.go (limited to 'cmd/nymdrop-source') diff --git a/cmd/nymdrop-source/main.go b/cmd/nymdrop-source/main.go new file mode 100644 index 0000000..a9ff0f5 --- /dev/null +++ b/cmd/nymdrop-source/main.go @@ -0,0 +1,257 @@ +package main + +import ( + "embed" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "syscall" + "time" + + "golang.org/x/net/proxy" +) + +//go:embed bin/nym-socks5-client-linux-amd64 +var nymBin embed.FS + +// nymdropProviderAddr is the Nym network requester — set at build time via -ldflags +var nymdropProviderAddr = "NYMDROP_PROVIDER_ADDR_PLACEHOLDER" + +// nymdropInboxAddr is the Nym address of the nymdrop-server SP — set at build time via -ldflags +var nymdropInboxAddr = "NYMDROP_INBOX_ADDR_PLACEHOLDER" + +func main() { + // Extract nym-socks5-client binary to temp dir + tmpDir, err := os.MkdirTemp("", "nymdrop-*") + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tmpDir) + + nymBinName := "nym-socks5-client" + if runtime.GOOS == "windows" { + nymBinName += ".exe" + } + nymBinPath := filepath.Join(tmpDir, nymBinName) + + srcName := "bin/nym-socks5-client-linux-amd64" + data, err := fs.ReadFile(nymBin, srcName) + if err != nil { + fmt.Fprintf(os.Stderr, "embedded binary not found: %v\n", err) + os.Exit(1) + } + if err := os.WriteFile(nymBinPath, data, 0700); err != nil { + fmt.Fprintf(os.Stderr, "extract binary: %v\n", err) + os.Exit(1) + } + + // Init nym client config if not already done + homeDir, _ := os.UserHomeDir() + configDir := filepath.Join(homeDir, ".nymdrop-source") + initCmd := exec.Command(nymBinPath, "init", "--id", "nymdrop-source", + "--provider", nymdropProviderAddr, + "--home", configDir) + initCmd.Stdout = os.Stdout + initCmd.Stderr = os.Stderr + _ = initCmd.Run() // ignore error if already initialised + + // Start nym-socks5-client + nymCmd := exec.Command(nymBinPath, "run", "--id", "nymdrop-source", + "--home", configDir, + "--port", "11080") + nymCmd.Stdout = os.Stdout + nymCmd.Stderr = os.Stderr + if err := nymCmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "nym start: %v\n", err) + os.Exit(1) + } + defer nymCmd.Process.Kill() + + // Wait for SOCKS5 to be ready + fmt.Println("Connecting to Nym mixnet...") + for i := 0; i < 30; i++ { + conn, err := net.DialTimeout("tcp", "127.0.0.1:11080", 300*time.Millisecond) + if err == nil { + conn.Close() + break + } + time.Sleep(500 * time.Millisecond) + } + + // Serve submission form locally + mux := http.NewServeMux() + mux.HandleFunc("/", serveForm) + mux.HandleFunc("/submit", handleSubmit) + + srv := &http.Server{Addr: "127.0.0.1:18080", Handler: mux} + go srv.ListenAndServe() + + // Open browser + openBrowser("http://127.0.0.1:18080") + fmt.Println("NymDrop ready — http://127.0.0.1:18080") + + // Wait for interrupt + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + fmt.Println("\nShutting down.") +} + +func openBrowser(url string) { + switch runtime.GOOS { + case "linux": + exec.Command("xdg-open", url).Start() + case "darwin": + exec.Command("open", url).Start() + case "windows": + exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + } +} + +func serveForm(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + fmt.Fprint(w, submissionForm) +} + +func handleSubmit(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024) + defer r.Body.Close() + + payload, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read error", http.StatusBadRequest) + return + } + defer func() { + for i := range payload { + payload[i] = 0 + } + }() + + if len(payload) == 0 { + http.Error(w, "empty payload", http.StatusBadRequest) + return + } + + dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:11080", nil, proxy.Direct) + if err != nil { + fmt.Fprintf(os.Stderr, "socks5 dialer: %v\n", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + conn, err := dialer.Dial("tcp", nymdropInboxAddr) + if err != nil { + fmt.Fprintf(os.Stderr, "nym dial: %v\n", err) + http.Error(w, "delivery failed", http.StatusBadGateway) + return + } + defer conn.Close() + + if _, err := conn.Write(payload); err != nil { + fmt.Fprintf(os.Stderr, "nym send: %v\n", err) + http.Error(w, "delivery failed", http.StatusBadGateway) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} + +const submissionForm = ` + + + + +NymDrop — Secure Submission + + + +
+

⬡ NYMDROP

+

Anonymous submission — encrypted in your browser, delivered over Nym mixnet.

+
+END-TO-END ENCRYPTED +NYM MIXNET +NO LOGS +NO METADATA +
+
+ + +
+ + + + +
+ +
+
+
+

Your submission is encrypted before leaving your device. The server keeps no logs. Once delivered over Nym, no record exists on this server.

+
+ + +` -- cgit v1.2.3