Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Project516/p2p-chat/llms.txt

Use this file to discover all available pages before exploring further.

Every message sent over p2p-chat is encrypted end-to-end using the NaCl box construction. Once a shared key is established at connection time, each message is independently encrypted with a fresh random nonce, so intercepting the ciphertext gives an attacker nothing useful. Messages are never written to disk — they exist only in memory for the duration of the session.

What NaCl box provides

NaCl box is a high-level authenticated encryption scheme that combines two primitives:
  • X25519 (Curve25519 Diffie-Hellman) — used during the key exchange phase to derive a shared secret from each peer’s key pair.
  • XSalsa20-Poly1305 — used to encrypt and authenticate each message. XSalsa20 provides the stream cipher; Poly1305 provides the MAC.
p2p-chat uses the Go implementation from golang.org/x/crypto/nacl/box (v0.50.0). The box.SealAfterPrecomputation and box.OpenAfterPrecomputation functions handle encryption and decryption once the shared key has been precomputed from the key exchange.
The nacl/box package is part of the Go extended cryptography library (golang.org/x/crypto). It wraps the original NaCl primitives and is considered production-ready for peer-to-peer encrypted communication.

Wire format

Every encrypted message is structured as:
nonce (24 bytes) || ciphertext
The 24-byte nonce is prepended directly to the ciphertext before transmission. On the receiving end, the first 24 bytes are extracted as the nonce and the remainder is passed to decryption.

How encryption works

1

Generate a random nonce

Before encrypting each message, a fresh 24-byte nonce is read from crypto/rand. Using crypto/rand (not math/rand) ensures the nonce is cryptographically unpredictable.
var nonce [24]byte
_, err := io.ReadFull(rand.Reader, nonce[:])
2

Seal the plaintext

box.SealAfterPrecomputation encrypts the plaintext using XSalsa20 and appends a Poly1305 MAC. The nonce is passed alongside the shared key precomputed during the key exchange.
ciphertext := box.SealAfterPrecomputation(nonce[:], plaintext, &nonce, sharedKey)
The result is nonce || ciphertext — the nonce slice is used as the initial output buffer, so the prepend is built in.
3

Send the frame

The encrypted bytes are handed to the transport layer as an opaque byte slice:
err = transport.SendFrame(conn, ciphertext)

How decryption works

1

Receive the frame

The transport layer reads an encrypted frame from the connection:
encrypted, err := transport.ReceiveFrame(conn)
2

Extract the nonce

The first 24 bytes of the received data are copied into a nonce array. If the message is shorter than 24 bytes, decryption is rejected immediately.
if len(encrypted) < 24 {
    return nil, fmt.Errorf("encrypted message to short")
}
var nonce [24]byte
copy(nonce[:], encrypted[:24])
ciphertext := encrypted[24:]
3

Open the ciphertext

box.OpenAfterPrecomputation verifies the Poly1305 MAC and, if valid, decrypts the message. If the MAC check fails — due to tampering, corruption, or a wrong key — the function returns false and decryption is aborted.
plaintext, ok := box.OpenAfterPrecomputation(nil, ciphertext, &nonce, sharedKey)
if !ok {
    return nil, fmt.Errorf("decryption failed")
}

Message authentication

The Poly1305 MAC is computed over the ciphertext using the shared key. This means any modification to the encrypted bytes — even a single flipped bit — causes box.OpenAfterPrecomputation to return false. There is no way to forge or alter a message without knowledge of the shared key.
If decryption fails, the error is surfaced to the caller and the message is discarded. p2p-chat does not attempt to recover or retry on authentication failure.

Per-message nonce security

Using a fresh random nonce for every message means that even if the same plaintext is sent twice, the ciphertext will be different each time. This prevents traffic analysis from detecting repeated messages.
XSalsa20 requires a 192-bit (24-byte) nonce. The extended nonce space makes random generation safe: the probability of a nonce collision is negligible across billions of messages on a single connection.
A counter nonce would require both peers to maintain synchronized state. Random nonces are simpler and eliminate the risk of desynchronization, which matters in a peer-to-peer setting where either side may send at any time.

No message persistence

p2p-chat holds decrypted message content only in memory. There are no logs, databases, or files where message content is written. When the connection closes, the session key and all associated plaintext are discarded.

Key exchange

How the shared key is derived from ephemeral X25519 key pairs at the start of each connection.

Build docs developers (and LLMs) love