Skip to main content

Module pair_channel

Module pair_channel 

Source
Expand description

PAIRED-CHANNELS Sprint E: end-to-end encryption for LCT pair messages.

Composes the Sprint A ECDH primitive (KeyPair::ecdh_with_peer) with HKDF-SHA256 (session-key derivation) + ChaCha20-Poly1305 AEAD (payload encryption + authentication). This is the moment “the hub cannot read content” stops being aspirational — the seal/open happens entirely at endpoints; the hub stores opaque ciphertext.

§Key derivation

shared_secret = ECDH(my_x25519_secret, peer_x25519_public)
session_key   = HKDF-SHA256(
                    salt = pair_id_bytes,            // pair-distinguishing salt
                    ikm  = shared_secret,
                    info = "web4-paired-channel-v1",
                    L    = 32 bytes,
                )

The salt being pair_id addresses the open question in the PRD (§8.1): two LCTs can have multiple distinct pairs without session-key reuse — the same shared secret produces a different session key per pair. Both endpoints know pair_id from the hub (it’s metadata), so they derive identical session keys without coordination.

§Wire format

sealed = nonce_12_bytes || ciphertext_n_bytes

ChaCha20-Poly1305 uses a 12-byte nonce. The ciphertext includes the 16-byte Poly1305 authentication tag at the end (AEAD-attached, per the standard). At the wire layer (over JSON to the hub), the sealed bytes are base64-encoded — that part’s the caller’s responsibility (Sealed::to_base64 / Sealed::from_base64 are provided as convenience).

§Nonce strategy (Sprint E MVP)

Random 12-byte nonce per message. With 2^96 possible nonces and ChaCha20-Poly1305’s birthday-bound at 2^48 messages per key, collision probability is negligible at any practical message volume. Sprint F adds proper per-session counter nonces + ephemeral ratchet keys for forward secrecy; Sprint E is the static-key baseline.

§What this gives you

  • Confidentiality: hub stores opaque ciphertext; only the two pair participants can decrypt.
  • Integrity / authenticity-of-payload: AEAD detects tampering (the Poly1305 tag fails if a byte flips). Authenticity-of-sender still rides the envelope signature at the REST layer — the AEAD only proves “whoever knew the session key wrote this,” which the envelope signature pins to a specific LCT.

§What this does NOT give you (deferred)

  • Forward secrecy — Sprint F. If an LCT’s static key is later compromised, an attacker who captured past ciphertexts can derive the session key and decrypt them. Sprint F’s ephemeral-key ratchet closes this.
  • Future secrecy / post-compromise security — full Signal double-ratchet, deferred per the PRD §6 out-of-scope list.
  • Group channels — 2-party only.

Structs§

EphemeralKeyPair
An ephemeral X25519 keypair for one pair-session. Caller must persist the secret locally (we never put it on the wire) and publish the public via PairingRequested / PairingConfirmed.
Sealed
A sealed pair message: 12-byte nonce prefix + ciphertext (with trailing 16-byte Poly1305 tag). Caller transports the raw bytes (typically base64 over JSON to the hub).
SessionKey
A 32-byte session key derived from an ECDH shared secret + pair_id salt. Carries a redacted Debug like SharedSecret.

Functions§

decrypt
Decrypt a Sealed blob under session_key. Returns plaintext on success; errors (AEAD authentication failed) if the ciphertext was tampered or the wrong session key was used.
derive_session_key
Derive the per-pair session key from an ECDH shared secret + the pair’s identifier. Both endpoints can do this independently from public information (peer’s LCT pubkey, pair_id from the hub) + their own private key.
derive_session_key_v2
V2 session-key derivation. Mixes the LCT-static ECDH and the per-session ephemeral ECDH. Both endpoints derive the same key from their own secret + the peer’s two publics + pair_id.
encrypt
Encrypt plaintext under session_key with a fresh random nonce. Output is nonce || ciphertext_with_tag wrapped in Sealed.
ephemeral_public_from_hex
Decode a 32-byte X25519 public from hex. Used when reading the peer’s ephemeral pub from a PairingRequested / PairingConfirmed event in the ledger.
open
End-to-end convenience: given my LCT keypair, the peer’s LCT public key, the pair_id, and a sealed blob — recover the plaintext.
open_fs
Symmetric inverse of seal_fs.
seal
End-to-end convenience: given my LCT keypair, the peer’s LCT public key, the pair_id, and a plaintext — produce a sealed blob the recipient can open using the symmetric inverse path.
seal_fs
End-to-end seal with forward secrecy. Caller supplies their own LCT keypair + ephemeral keypair + peer’s LCT public + peer’s ephemeral public + pair_id + plaintext.