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:
| Element | Wartość |
|---|---|
| Ciąg protokołu | RPE2E01 |
| Prefiks zaszyfrowanej wiadomości | +RPE2E01 |
| Tag handshake CTCP | RPEE2E |
| Pole wersji handshake | v=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 Ed25519e: efemeryczny publiczny klucz X25519wn: nonce dla klucza sesji zawiniętego przez AEADw: ciphertext z zawiniętym kluczem sesjin: nonce anty-replay uwzględniany w podpisywanym payloadzies: 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 hexts: unix timestamp w sekundachpart/total: indeks chunka z limitemtotal <= 16nonce_b64: 24-bajtowy nonce XChaCha20ct_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_handlenie wchodzi do AAD- powiązanie nadawcy jest wymuszane na poziomie lookupu w keyringu przez
(handle_z_prefixu_IRC, context) - domyślna tolerancja timestampu to
300sekund - 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):
| Kierunek | Kontekst | Widok Alice | Widok 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.
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:
| Tryb | Zachowanie |
|---|---|
auto-accept | pierwszy kontakt jest automatycznie uznawany za zaufany |
normal | powstaje oczekujące żądanie wymagające accept/decline |
quiet | nieznani 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::PlatypusiFFI::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/.