From 16854e8b24a3b00d9d5dbd285bbe3b93ee47ce1c Mon Sep 17 00:00:00 2001 From: Gab Virebent Date: Mon, 6 Jul 2026 16:34:47 +0200 Subject: Guard key material in RAM with memguard, add Markdown preview for message field Reader now keeps the private key bytes and the per-submission ECDH/HKDF secrets in memguard.LockedBuffer (mlock + guaranteed wipe) instead of a manual zero loop. Added a regression test for the decrypt path. Message field gets a self-hosted Markdown toggle preview (no external library): headers, bold/italic, inline code, code blocks, blockquotes, lists, links. Output is HTML-escaped and link schemes are whitelisted before rendering. Submitted content is still the raw Markdown source, unchanged on the wire. --- cmd/nymdrop-reader/main.go | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) (limited to 'cmd/nymdrop-reader/main.go') diff --git a/cmd/nymdrop-reader/main.go b/cmd/nymdrop-reader/main.go index 248972e..e2632cb 100644 --- a/cmd/nymdrop-reader/main.go +++ b/cmd/nymdrop-reader/main.go @@ -25,6 +25,7 @@ import ( "syscall" "time" + "github.com/awnumar/memguard" "golang.org/x/net/websocket" ) @@ -73,6 +74,10 @@ func main() { signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig fmt.Println("\nShutting down.") + // Safety net: wipes any locked buffer still alive (e.g. one in flight in + // receiveLoop when the signal arrived) on top of the deferred Destroy + // calls that cover the normal path. + memguard.Purge() } // ── key management ─────────────────────────────────────────────────────────── @@ -87,7 +92,12 @@ func loadOrGenKey(keyFile, homeDir string) *ecdh.PrivateKey { if err != nil { fatalf("privkey decode: %v", err) } - priv, err := ecdh.X25519().NewPrivateKey(raw) + // raw is wiped as soon as it's copied into locked, non-swappable memory; + // ecdh.NewPrivateKey takes its own copy, so the guarded buffer can be + // destroyed right after construction. + keyBuf := memguard.NewBufferFromBytes(raw) + priv, err := ecdh.X25519().NewPrivateKey(keyBuf.Bytes()) + keyBuf.Destroy() if err != nil { fatalf("privkey load: %v", err) } @@ -102,16 +112,21 @@ func loadOrGenKey(keyFile, homeDir string) *ecdh.PrivateKey { if _, err := rand.Read(rawPriv); err != nil { fatalf("keygen rand: %v", err) } - priv, err := ecdh.X25519().NewPrivateKey(rawPriv) + keyBuf := memguard.NewBufferFromBytes(rawPriv) // wipes rawPriv on copy + priv, err := ecdh.X25519().NewPrivateKey(keyBuf.Bytes()) if err != nil { + keyBuf.Destroy() fatalf("keygen: %v", err) } if err := os.MkdirAll(filepath.Dir(keyFile), 0700); err != nil { + keyBuf.Destroy() fatalf("mkdir keydir: %v", err) } - if err := os.WriteFile(keyFile, []byte(hex.EncodeToString(rawPriv)+"\n"), 0600); err != nil { + if err := os.WriteFile(keyFile, []byte(hex.EncodeToString(keyBuf.Bytes())+"\n"), 0600); err != nil { + keyBuf.Destroy() fatalf("write privkey: %v", err) } + keyBuf.Destroy() pubHex := hex.EncodeToString(priv.PublicKey().Bytes()) fmt.Printf("Generated new private key → %s\n", keyFile) fmt.Printf("Public key (deploy in nymdrop-server): %s\n\n", pubHex) @@ -344,18 +359,22 @@ func decryptMessage(dataB64 string, privKey *ecdh.PrivateKey) (string, error) { if err != nil { return "", fmt.Errorf("ecdh: %w", err) } - defer zeroBytes(sharedSecret) + // Locked, non-swappable memory for the two secrets derived per submission; + // Destroy zeroes and unlocks instead of relying on a plain overwrite loop. + sharedBuf := memguard.NewBufferFromBytes(sharedSecret) + defer sharedBuf.Destroy() // HKDF-SHA-256 - aesKey := make([]byte, 32) - hkdfR := hkdf.New(sha256.New, sharedSecret, make([]byte, 32), []byte("nymdrop-v1")) - if _, err := io.ReadFull(hkdfR, aesKey); err != nil { + aesKeyRaw := make([]byte, 32) + hkdfR := hkdf.New(sha256.New, sharedBuf.Bytes(), make([]byte, 32), []byte("nymdrop-v1")) + if _, err := io.ReadFull(hkdfR, aesKeyRaw); err != nil { return "", fmt.Errorf("hkdf: %w", err) } - defer zeroBytes(aesKey) + aesBuf := memguard.NewBufferFromBytes(aesKeyRaw) + defer aesBuf.Destroy() // AES-GCM-256 decrypt - block, err := aes.NewCipher(aesKey) + block, err := aes.NewCipher(aesBuf.Bytes()) if err != nil { return "", fmt.Errorf("aes: %w", err) } @@ -448,12 +467,6 @@ func saveAttachments(outDir, ts, fileSection string) { // ── helpers ─────────────────────────────────────────────────────────────────── -func zeroBytes(b []byte) { - for i := range b { - b[i] = 0 - } -} - func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "nymdrop-reader: "+format+"\n", args...) os.Exit(1) -- cgit v1.2.3