RPE2E Protocol
RPE2E stands for Repartee End-to-End. Version 1 is the protocol currently implemented in native form by repartee and in companion scripts for WeeChat and irssi-compatible clients such as erssi.
This page describes the protocol as implemented today, not an aspirational design draft.
Scope and goals
RPE2E v1 adds end-to-end encryption for IRC channels and private conversations without changing IRC transport. The server still routes traffic, but compatible clients exchange keys out of band and send encrypted payloads over normal IRC messages.
The design goal is pragmatic interoperability across three environments:
- native Rust in repartee
- Python script for WeeChat
- Perl script for irssi and erssi
Version identifiers
RPE2E v1 currently uses these identifiers:
| Item | Value |
|---|---|
| Protocol string | RPE2E01 |
| Encrypted message prefix | +RPE2E01 |
| Handshake CTCP tag | RPEE2E |
| Handshake version field | v=1 |
The protocol name is RPE2E. The handshake tag is RPEE2E.
Security model
RPE2E encrypts message bodies end-to-end, but it does not hide IRC metadata. The server can still observe:
- nicknames and
ident@host - channel names or PM targets
- timing and approximate message sizes
- that a handshake took place
Trust is TOFU-oriented: the first contact can require manual accept, can be auto-accepted, or can be silently ignored depending on channel mode. Fingerprint verification is still required if you want protection against active impersonation.
RPE2E v1 also intentionally does not implement a Signal-style double ratchet. Session keys rotate on handshake and REKEY, not on every message.
Handshake transport
Handshake traffic is carried inside CTCP framing sent via NOTICE.
RPEE2E KEYREQ v=1 c=#x p=<b64u32> e=<b64u32> n=<b64u16> s=<b64u64>
RPEE2E KEYRSP v=1 c=#x p=<b64u32> e=<b64u32> wn=<b64u24> w=<b64u> n=<b64u16> s=<b64u64>
RPEE2E REKEY v=1 c=#x p=<b64u32> e=<b64u32> wn=<b64u24> w=<b64u> n=<b64u16> s=<b64u64>
Field summary:
c: context — a channel name, or for a DM the recipient-keyed@<handle>(see Context model below)p: long-term Ed25519 identity public keye: ephemeral X25519 public keywn: wrap nonce for AEAD-wrapped session keyw: wrapped session key ciphertextn: anti-replay nonce included in the signed payloads: Ed25519 signature
KEYREQ starts the exchange. KEYRSP returns a wrapped session key. REKEY is the unsolicited distribution form used to rotate an outgoing session key to already trusted peers.
Cryptographic construction
RPE2E v1 uses:
- Ed25519 for long-term identity keys and handshake signatures
- X25519 for ECDH during key exchange
- HKDF-SHA256 for wrap-key derivation
- XChaCha20-Poly1305 for message encryption and wrapped session keys
For REKEY, repartee maps the peer's Ed25519 identity public key to X25519 using the RFC 7748 Appendix A birational map, matching libsodium-compatible clients.
Encrypted message wire format
Encrypted channel or PM traffic is sent as a normal IRC line body:
+RPE2E01 <msgid> <ts> <part>/<total> <nonce_b64>:<ct_b64>
Each chunk is an independent cryptographic unit:
msgid: random 8-byte message identifier, hex-encodedts: unix timestamp in secondspart/total: chunk index withtotal <= 16nonce_b64: 24-byte XChaCha20 noncect_b64: ciphertext plus Poly1305 tag
Receivers decrypt chunks immediately. There is no stateful reassembly buffer in the protocol.
AAD and replay window
The current implementation authenticates this AAD layout:
PROTO
|| be16(channel.len) || channel
|| be16(8) || msgid
|| be16(8) || ts_be
|| be16(1) || part
|| be16(1) || total
Notes:
- the sender handle is not included in AAD
- sender binding is enforced at the keyring lookup layer using
(handle_from_IRC_prefix, context) - the default timestamp tolerance is
300seconds - fields are length-prefixed to prevent delimiter ambiguity
For a channel the channel field is the channel name verbatim; for a direct message it is the recipient-keyed context described under Context model below. The AAD byte layout is byte-identical in both cases — only the string differs. Golden test vector for a DM (@~bob@b.host):
build_aad("@~bob@b.host", msgid=[01×8], ts=100, part=1, total=1) =
52 50 45 32 45 30 31 "RPE2E01"
00 0c 40 7e 62 6f 62 40 62 2e 68 6f 73 74 be16(12) || "@~bob@b.host"
00 08 01 01 01 01 01 01 01 01 be16(8) || msgid
00 08 00 00 00 00 00 00 00 64 be16(8) || ts=100 (be64)
00 01 01 be16(1) || part=1
00 01 01 be16(1) || total=1
Context model
Every RPE2E session is keyed by a context string. That string is the first length-prefixed field of the AAD, the keyring/session lookup key, and the HKDF wrapInfo/rekeyInfo. It is negotiated once during the handshake and pinned to the session — never recomputed per message.
Channels
For a real channel the context is the channel name verbatim, for example #repartee. A channel uses one context for both directions.
Direct messages — recipient-keyed context
For a direct message (private query) the context is the recipient's server-stamped handle:
context(DM) = "@" + <ident@host of the party that RECEIVES the message>
ident@host is the raw server-stamped value — including any leading ~ and any cloak/vhost — never the nick. Example: @~alice@static.example.org.
Because the recipient differs by direction, a DM conversation between Alice and Bob uses two contexts, one per direction (sessions are directional):
| Direction | Context | Alice's view | Bob's view |
|---|---|---|---|
| Alice → Bob | @<bob_handle> | outgoing (peer = Bob) | incoming (self = Bob) |
| Bob → Alice | @<alice_handle> | incoming (self = Alice) | outgoing (peer = Alice) |
So locally each client encrypts keyed by @<peer_handle> (the recipient is the peer) and decrypts keyed by @<own_handle> (the recipient is you).
Why the recipient, not the sender or a sorted pair
The encrypted wire line carries no channel field — the receiver reconstructs the context from local state, so it must be derivable from wire-visible identifiers (ident@host). Keying on the recipient means the context string is owned and stamped by exactly one party — the one it names — and used verbatim by the other, so the two sides can never disagree on the bytes (e.g. ~bob@ip vs bob@cloak). A sorted pair of both handles would force each side to independently reconstruct the other party's handle, re-introducing that mismatch as a load-bearing AEAD input.
Handshake keying for DMs
The KEYREQ for a direction is sent by the party that wants to receive it, and stamps c= with its own handle; the responder echoes c= back verbatim — it never recomputes or remaps it:
recipient (initiator) ── KEYREQ c=@<own_handle> ──▶ sender (responder)
recipient ◀── KEYRSP c=@<own_handle> ── sender (echoed verbatim)
An unsolicited REKEY is likewise stamped by the recipient with @<own_handle> (the peer is rotating the key it uses to send to you). The signed payload, the HKDF wrapInfo/rekeyInfo, the installed session row, and every per-message AAD all use this one agreed string.
Volatile handles
ident@host can change mid-session (services vhost, oper vhost, CHGHOST). The inbound session map keys on the live ident@host, so a handle change forces a session miss → automatic KEYREQ → re-handshake, exactly as a peer reconnect does. The transport self-heals, but the trust decision still applies: a known fingerprint arriving under a new handle is classified HandleChanged and stays refused until /e2e reverify (the same TOFU gate as any handle change) unless an autotrust rule covers it.
Each client keeps both the peer's handle (from the message prefix / CHGHOST) and its own handle current (from an echo-message echo of its own line, a self-USERHOST, or its own CHGHOST). While the own handle is still unknown — e.g. between registration and the self-USERHOST reply — an incoming DM has no recipient context; repartee waits for the next message rather than falling back to keying by the sender's handle, which would negotiate the wrong direction. On every (re)connect the own handle is reset at registration and re-seeded, since the server may assign a different ident/host/cloak.
Channel modes and trust states
RPE2E v1 supports three per-context modes:
| Mode | Behavior |
|---|---|
auto-accept | first contact is trusted automatically |
normal | pending request with explicit accept/decline |
quiet | unknown peers are ignored until manually handled |
Peer trust state is tracked as pending, trusted, or revoked.
Client scripts and downloads
Official companion scripts are published in the repartee repository:
WeeChat requirements
- WeeChat with Python scripting enabled
- Python 3
PyNaClinstalled, for examplepip install pynacl- load
scripts/weechat/rpe2e.py
irssi and erssi requirements
- irssi or erssi with Perl scripting enabled
libsodiumavailable on the system- Perl modules used by the script, including
Crypt::NaCl::Sodium,MIME::Base64,JSON::PP,Digest::SHA,FFI::Platypus, andFFI::Platypus::Buffer - load
scripts/irssi/rpe2e.pl
Current v1 limitations
- no metadata hiding beyond encrypted message bodies
- no per-message forward secrecy / double ratchet
- only compatible clients can read
+RPE2E01payloads - export/import is operationally useful, but exported key material is plaintext and must be protected accordingly
Relationship to repartee
repartee is the reference implementation at the moment. The protocol page here follows the current Rust code in src/e2e/ and the matching client scripts in scripts/.