fix(relay): canonicalise envelope ID and timestamp on mailbox.Store

The mailbox previously trusted the client-supplied envelope ID and SentAt,
which enabled two attacks:
  - replay via re-broadcast: a malicious relay could resubmit the same
    ciphertext under multiple IDs, causing the recipient to receive the
    same plaintext repeatedly;
  - timestamp spoofing: senders could back-date or future-date messages
    to bypass the 7-day TTL or fake chronology.

Store() now recomputes env.ID as hex(sha256(nonce||ct)[:16]) and
overwrites env.SentAt with time.Now().Unix(). Both values are mutated
on the envelope pointer so downstream gossipsub publishes agree on the
normalised form.

Also documents /relay/send as non-E2E — the endpoint seals with the
relay's own key, which breaks end-to-end authenticity. Clients wanting
real E2E should POST /relay/broadcast with a pre-sealed envelope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
vsecoder
2026-04-18 17:41:22 +03:00
parent 546d2c503f
commit 78d97281f0
2 changed files with 30 additions and 5 deletions

View File

@@ -167,9 +167,16 @@ func relayInboxCount(rc RelayConfig) http.HandlerFunc {
// "msg_b64": "<base64-encoded plaintext>",
// }
//
// The relay node seals the message using its own X25519 keypair and broadcasts
// it on the relay gossipsub topic. No on-chain fee is attached — delivery is
// free for light clients using this endpoint.
// WARNING — NOT END-TO-END ENCRYPTED.
// The relay node seals the message using its OWN X25519 keypair, not the
// sender's. That means:
// - The relay can read the plaintext (msg_b64 arrives in the clear).
// - The recipient cannot authenticate the sender — they only see "a
// message from the relay".
// For real E2E messaging, clients should seal the envelope themselves and
// use POST /relay/broadcast instead. This endpoint is retained only for
// backwards compatibility with legacy integrations and for bootstrap
// scenarios where the sender doesn't have a long-lived X25519 key yet.
func relaySend(rc RelayConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {