Protokół RPE2E

RPE2E oznacza Repartee End-to-End. Wersja 1 jest obecnie zaimplementowana natywnie w repartee oraz w skryptach towarzyszących dla WeeChat i klientów zgodnych z irssi, takich jak erssi.

Ta strona opisuje protokół tak, jak działa dziś w implementacji, a nie jako projekt “na przyszłość”.

Zakres i cele

RPE2E v1 dodaje szyfrowanie end-to-end dla kanałów IRC i prywatnych rozmów bez zmiany transportu IRC. Serwer nadal routuje ruch, ale zgodne klienty wymieniają klucze poza pasmem i przesyłają zaszyfrowane payloady przez zwykłe wiadomości IRC.

Celem projektu była praktyczna interoperacyjność w trzech środowiskach:

  • natywna implementacja Rust w repartee
  • skrypt Python dla WeeChat
  • skrypt Perl dla irssi i erssi

Identyfikatory wersji

RPE2E v1 używa obecnie następujących identyfikatorów:

ElementWartość
Ciąg protokołuRPE2E01
Prefiks zaszyfrowanej wiadomości+RPE2E01
Tag handshake CTCPRPEE2E
Pole wersji handshakev=1

Nazwa protokołu to RPE2E. Tag handshake to RPEE2E.

Model bezpieczeństwa

RPE2E szyfruje treść wiadomości end-to-end, ale nie ukrywa metadanych IRC. Serwer nadal widzi:

  • nicki i ident@host
  • nazwy kanałów lub cele PM
  • czas i przybliżony rozmiar wiadomości
  • sam fakt wykonania handshake

Model zaufania jest zorientowany na TOFU: pierwszy kontakt może wymagać ręcznego zaakceptowania, może być auto-akceptowany albo ignorowany zależnie od trybu kanału. Weryfikacja fingerprintów jest nadal potrzebna, jeśli chcesz chronić się przed aktywnym podszywaniem.

RPE2E v1 celowo nie implementuje też double ratchet w stylu Signal. Klucze sesji rotują przy handshake i REKEY, a nie przy każdej wiadomości.

Transport handshake

Ruch handshake jest przenoszony wewnątrz ramek CTCP wysyłanych przez 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>

Podsumowanie pól:

  • c: kontekst — nazwa kanału, albo dla DM kluczowany odbiorcą @<handle> (patrz Model kontekstu poniżej)
  • p: długoterminowy publiczny klucz tożsamości Ed25519
  • e: efemeryczny publiczny klucz X25519
  • wn: nonce dla klucza sesji zawiniętego przez AEAD
  • w: ciphertext z zawiniętym kluczem sesji
  • n: nonce anty-replay uwzględniany w podpisywanym payloadzie
  • s: podpis Ed25519

KEYREQ rozpoczyna wymianę. KEYRSP zwraca zawinięty klucz sesji. REKEY to wariant nieproszony, używany do dystrybucji nowego klucza wyjściowego do już zaufanych peerów.

Konstrukcja kryptograficzna

RPE2E v1 używa:

  • Ed25519 dla długoterminowych kluczy tożsamości i podpisów handshake
  • X25519 dla ECDH podczas wymiany kluczy
  • HKDF-SHA256 do wyprowadzania klucza wrap
  • XChaCha20-Poly1305 do szyfrowania wiadomości i zawijania kluczy sesji

Dla REKEY repartee mapuje publiczny klucz tożsamości Ed25519 peera do X25519 przy użyciu mapy biracjonalnej z RFC 7748 Appendix A, zgodnie z klientami kompatybilnymi z libsodium.

Wire format zaszyfrowanych wiadomości

Zaszyfrowany ruch kanałowy albo PM jest wysyłany jako zwykłe body linii IRC:

+RPE2E01 <msgid> <ts> <part>/<total> <nonce_b64>:<ct_b64>

Każdy chunk jest niezależną jednostką kryptograficzną:

  • msgid: losowy identyfikator wiadomości 8 B, zapisany jako hex
  • ts: unix timestamp w sekundach
  • part/total: indeks chunka z limitem total <= 16
  • nonce_b64: 24-bajtowy nonce XChaCha20
  • ct_b64: ciphertext wraz z tagiem Poly1305

Odbiorca odszyfrowuje chunki natychmiast. Protokół nie używa stanowego bufora do składania wiadomości.

AAD i okno replay

Aktualna implementacja uwierzytelnia następujący układ AAD:

PROTO
  || be16(channel.len) || channel
  || be16(8)           || msgid
  || be16(8)           || ts_be
  || be16(1)           || part
  || be16(1)           || total

Uwagi:

  • sender_handle nie wchodzi do AAD
  • powiązanie nadawcy jest wymuszane na poziomie lookupu w keyringu przez (handle_z_prefixu_IRC, context)
  • domyślna tolerancja timestampu to 300 sekund
  • pola są prefiksowane długością, żeby uniknąć niejednoznaczności delimiterów

Dla kanału pole channel to nazwa kanału dosłownie; dla prywatnej wiadomości (DM) to kontekst kluczowany odbiorcą, opisany niżej w sekcji Model kontekstu. Układ bajtów AAD jest identyczny w obu przypadkach — różni się tylko ciąg znaków. Złoty wektor testowy dla 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

Model kontekstu

Każda sesja RPE2E jest kluczowana ciągiem context. Ten ciąg to pierwsze pole AAD prefiksowane długością, klucz lookupu keyringu/sesji oraz HKDF wrapInfo/rekeyInfo. Jest negocjowany raz podczas handshake i przypięty do sesji — nigdy nie jest przeliczany per wiadomość.

Kanały

Dla prawdziwego kanału kontekstem jest nazwa kanału dosłownie, np. #repartee. Kanał używa jednego kontekstu dla obu kierunków.

Prywatne wiadomości — kontekst kluczowany odbiorcą

Dla prywatnej wiadomości (query) kontekstem jest odbiorca — jego stemplowany przez serwer handle:

context(DM) = "@" + <ident@host strony, która ODBIERA wiadomość>

ident@host to surowa wartość stemplowana przez serwer — łącznie z wiodącym ~ oraz każdym cloakiem/vhostem — nigdy nick. Przykład: @~alice@static.example.org.

Ponieważ odbiorca różni się zależnie od kierunku, rozmowa DM między Alice a Bobem używa dwóch kontekstów, po jednym na kierunek (sesje są kierunkowe):

KierunekKontekstWidok AliceWidok Boba
Alice → Bob@<bob_handle>wychodzący (peer = Bob)przychodzący (self = Bob)
Bob → Alice@<alice_handle>przychodzący (self = Alice)wychodzący (peer = Alice)

Lokalnie każdy klient szyfruje kluczując po @<peer_handle> (odbiorcą jest peer) i odszyfrowuje kluczując po @<own_handle> (odbiorcą jesteś ty).

Dlaczego odbiorca, a nie nadawca ani posortowana para

Zaszyfrowana linia wire nie niesie pola kanału — odbiorca rekonstruuje kontekst z lokalnego stanu, więc musi być on wyprowadzalny z identyfikatorów widocznych na wire (ident@host). Kluczowanie po odbiorcy oznacza, że ciąg kontekstu jest posiadany i stemplowany przez dokładnie jedną stronę — tę, którą nazywa — i używany dosłownie przez drugą, więc obie strony nigdy nie mogą się nie zgodzić co do bajtów (np. ~bob@ip vs bob@cloak). Posortowana para obu handle zmuszałaby każdą stronę do niezależnej rekonstrukcji handle drugiej strony, co ponownie wprowadziłoby ten rozjazd jako nośne wejście AEAD.

Note: Korekta protokołu (poł. 2026). Wcześniejsze buildy wyprowadzały kontekst DM niespójnie między ścieżką szyfrowania a odszyfrowania, więc ciphertext DM nigdy nie uwierzytelniał się poprawnie między dwoma klientami. Reguła kluczowania po odbiorcy powyżej jest interoperacyjnym zachowaniem uzgodnionym między implementacjami RPE2E i to je dostarcza repartee dziś. Kluczowanie kanałów jest bez zmian.

Kluczowanie handshake dla DM

KEYREQ dla danego kierunku jest wysyłany przez stronę, która chce odbierać ten kierunek, i stempluje c= swoim własnym handle; responder odsyła c= dosłownie — nie przelicza go ani nie remapuje:

odbiorca (inicjator)  ── KEYREQ c=@<own_handle> ──▶  nadawca (responder)
odbiorca              ◀── KEYRSP c=@<own_handle> ──  nadawca  (echo dosłowne)

Nieproszony REKEY jest analogicznie stemplowany przez odbiorcę jako @<own_handle> (peer rotuje klucz, którym wysyła do ciebie). Podpisywany payload, HKDF wrapInfo/rekeyInfo, zainstalowany wiersz sesji oraz każde AAD per wiadomość używają tego jednego uzgodnionego ciągu.

Zmienne handle

ident@host może zmienić się w trakcie sesji (vhost services, vhost opera, CHGHOST). Mapa sesji przychodzących kluczuje po żywym ident@host, więc zmiana handle wymusza miss sesji → automatyczny KEYREQ → ponowny handshake, dokładnie jak przy reconnect peera. Transport sam się leczy, ale decyzja o zaufaniu nadal obowiązuje: znany fingerprint przychodzący pod nowym handle jest klasyfikowany jako HandleChanged i pozostaje odrzucony aż do /e2e reverify (ta sama bramka TOFU co przy każdej zmianie handle), o ile nie obejmuje go reguła autotrust.

Każdy klient utrzymuje aktualny zarówno handle peera (z prefiksu wiadomości / CHGHOST), jak i własny handle (z echa własnej linii przez echo-message, z własnego USERHOST, albo z własnego CHGHOST). Dopóki własny handle jest nieznany — np. między rejestracją a odpowiedzią na własny USERHOST — przychodzący DM nie ma kontekstu odbiorcy; repartee czeka na kolejną wiadomość zamiast kluczować po handle nadawcy, co negocjowałoby zły kierunek. Przy każdym (re)connect własny handle jest resetowany przy rejestracji i ponownie zasiewany, bo serwer może przydzielić inny ident/host/cloak.

Tryby kanału i stany zaufania

RPE2E v1 wspiera trzy tryby per kontekst:

TrybZachowanie
auto-acceptpierwszy kontakt jest automatycznie uznawany za zaufany
normalpowstaje oczekujące żądanie wymagające accept/decline
quietnieznani peerzy są ignorowani do czasu ręcznej obsługi

Stan zaufania peera jest śledzony jako pending, trusted albo revoked.

W repartee kanały i tryby E2E można konfigurować poleceniami /e2e on|off|mode albo interaktywnie w zakładce E2E Channels w /wizard (/config), która obsługuje reguły kanałów per sieć (network + channel + enabled + mode). Podczas łączenia przez bouncer (soju.im/bouncer-networks), każda utworzona sieć potomna utrzymuje własny odizolowany identyfikator połączenia, dzięki czemu klucze i konteksty E2E nigdy nie kolidują między sieciami.

Skrypty klienckie i pobieranie

Oficjalne skrypty towarzyszące są publikowane w repozytorium repartee:

Wymagania dla WeeChat

  • WeeChat z włączoną obsługą skryptów Python
  • Python 3
  • zainstalowany PyNaCl, np. pip install pynacl
  • załadowany scripts/weechat/rpe2e.py

Wymagania dla irssi i erssi

  • irssi albo erssi z obsługą skryptów Perl
  • dostępny w systemie libsodium
  • moduły Perl używane przez skrypt, w tym Crypt::NaCl::Sodium, MIME::Base64, JSON::PP, Digest::SHA, FFI::Platypus i FFI::Platypus::Buffer
  • załadowany scripts/irssi/rpe2e.pl

Obecne ograniczenia v1

  • brak ukrywania metadanych poza treścią wiadomości
  • brak per-message forward secrecy / double ratchet
  • tylko kompatybilni klienci potrafią odczytać payloady +RPE2E01
  • export/import jest praktycznie użyteczny, ale eksportowany materiał kluczowy jest jawny i musi być odpowiednio chroniony

Relacja z repartee

repartee jest obecnie implementacją referencyjną. Ta strona protokołu opiera się na aktualnym kodzie Rust w src/e2e/ oraz na zgodnych skryptach klienckich w scripts/.