Dokumentacja implementacji szyfrowania
Szyfrowanie, które jest wykonywane, jest węższe niż szyfrowanie, które zostało napisane, i właśnie tej różnicy dotyczy ta strona. Konstrukcje sealed box z libsodium to jedyne szyfrowanie wiadomości, jakie Railgun wykonuje — i wykonują się wyłącznie na tej ścieżce klienta, na której natywny moduł libsignal jest nieobecny. W kompilacji desktopowej Electron, gdzie libsignal faktycznie się ładuje, handler szyfrujący wiadomości bezpośrednie jest wciąż atrapą, która koduje tekst jawny w base64. Wiadomości społecznościowe i kanałowe nie są szyfrowane na żadnej ścieżce. Protokół w stylu Signal opisany w drugiej połowie tej strony jest napisany i przejrzany, i nie jest podłączony do żadnego klienta, którego można pobrać.
To jest dokumentacja architektury, a nie niezależny audyt bezpieczeństwa. Sekcje 1 i 2 opisują to, co jest dostarczane. Sekcje od 4 do 7 opisują projekt, do którego Railgun dąży, i są wszędzie oznaczone jako Planowane. Nic z tego, co tu napisano, nie powinno być odczytywane jako twierdzenie, że klient Railguna obecnie egzekwuje właściwość oznaczoną jako planowana.
Co jest dostarczane dzisiaj
| Ścieżka | Mechanizm | Status |
|---|---|---|
| Wiadomości bezpośrednie — kompilacje przeglądarkowe i deweloperskie | Sealed box z libsodium — uzgadnianie klucza X25519, szyfrowanie uwierzytelnione XSalsa20-Poly1305. Wybierane, gdy natywny moduł libsignal jest nieobecny. | Szyfrowane |
| Wiadomości bezpośrednie — kompilacja desktopowa Electron | libsignal się ładuje, więc encryptDm jest kierowane do handlera IPC Electron — atrapy, która koduje tekst jawny w base64. Przekaźnik otrzymuje czytelny tekst. | Nieszyfrowane |
| Wiadomości społecznościowe i kanałowe | Wysyłane jako tekst jawny opakowany w base64. Szyfrowanie kanałów jest napisane, ale zwraca znacznik tekstu jawnego. | Nieszyfrowane |
| X3DH + Double Ratchet | Zaimplementowane w oparciu o libsignal, importowane wyłącznie jako typ. Żadna ścieżka wykonania go nie tworzy. | Planowane |
| Przechowywanie kluczy prywatnych | Klucze są generowane i przechowywane na urządzeniu. Żadna ścieżka kodu nie przesyła materiału klucza prywatnego ani klucza tożsamości. | Zaimplementowane |
1. Sealed box — szyfrowanie, które działa
Tam, gdzie Railgun w ogóle szyfruje wiadomość bezpośrednią, robi to za pomocą konstrukcji sealed box z libsodium — crypto_box_seal. Łączy ona uzgadnianie klucza X25519 z szyfrowaniem uwierzytelnionym XSalsa20-Poly1305. To prawdziwa, dobrze przeanalizowana kryptografia. Jest to również celowo prosta konstrukcja, a jej ograniczenia mają znaczenie. To, które klienty do niej docierają, opisuje sekcja 2: kompilacje przeglądarkowe i deweloperskie tak, kompilacja desktopowa Electron nie.
Jak budowany jest sealed box
// Sender has only the recipient's public key, PK_r
(pk_e, sk_e) = crypto_box_keypair() // fresh, single-use
nonce = BLAKE2b(pk_e || PK_r) // derived, not random
ct = crypto_box(plaintext, nonce, PK_r, sk_e)
sealed = pk_e || ct // ephemeral key is prepended
erase(sk_e)
Odbiorca odzyskuje efemeryczny klucz publiczny z początku wiadomości, ponownie wylicza ten sam wspólny sekret za pomocą własnego klucza prywatnego i otwiera pudełko. Znacznik Poly1305 sprawia, że zmodyfikowany szyfrogram nie daje się otworzyć, zamiast odszyfrować się do bezsensownych danych.
Co to daje
- • Poufność wobec przekaźnika i wobec każdego, kto jest na łączu.
- • Integralność — manipulacja jest wykrywana, a nie po cichu odszyfrowywana.
- • Anonimowość nadawcy: klucz efemeryczny nie ujawnia niczego o tym, kto wysłał wiadomość.
- • Brak inicjowania sesji, więc działa, gdy odbiorca jest offline.
Czego to nie daje
- • Brak forward secrecy. Długoterminowy klucz prywatny odbiorcy otwiera każdą wiadomość, jaka kiedykolwiek została do niego wysłana. Jeśli wycieknie, wraz z nim wycieka przechwycony szyfrogram.
- • Brak uwierzytelnienia nadawcy. Sealed box jest z założenia anonimowy; nie dowodzi niczego o tym, kto go napisał.
- • Brak odzyskiwania po kompromitacji. Nie ma żadnego mechanizmu ratchet, który pozwoliłby wyjść poza kompromitację klucza.
Te trzy luki są dokładnie tym, co mają zamknąć X3DH i Double Ratchet, i dlatego ta praca trwa. Dopóki nie zostanie wdrożona, uczciwe podsumowanie wiadomości bezpośredniej chronionej konstrukcją sealed box brzmi: mocne wobec pasywnego obserwatora sieci i wobec przekaźnika, słabe wobec przeciwnika, który ostatecznie zdobędzie klucz z urządzenia odbiorcy. To podsumowanie dotyczy wyłącznie miejsc, w których ten kod faktycznie się wykonuje.
2. Co nie jest szyfrowane
Wiadomości społecznościowe i kanałowe — większość tego, co użytkownicy Railguna faktycznie piszą — nie są dziś szyfrowane na żadnej ścieżce klienta. Funkcja szyfrowania kanałów istnieje i jest wobec siebie szczera: zapisuje ostrzeżenie i zwraca tekst jawny opakowany w base64 za łańcuchem znacznikowym, tak aby nikt dalej nie wziął go za szyfrogram.
// What encryptChannel actually returns
logger.warn("Channel encryption not yet implemented")
ciphertext = base64("[DEVCRYPTO:PLAINTEXT]" + plaintext)
Praktyczna konsekwencja: w przypadku treści społecznościowych i kanałowych serwer Railguna jest w tej samej sytuacji co każdy zwykły serwer czatu. Może odczytać to, co tam piszesz. Dystrybucja klucza grupowego — klucz na kanał przekazywany członkom wewnątrz konstrukcji sealed box — to planowana poprawka i nie została dostarczona.
Wiadomości bezpośrednie w kompilacji desktopowej Electron
Druga luka to wiadomości bezpośrednie w kliencie, z którego korzysta większość ludzi, a łatwo opisać ją odwrotnie, więc oto logika wyboru w takiej postaci, w jakiej wykonuje ją kod. initCrypto() pyta proces główny, czy libsignal został załadowany. @signalapp/libsignal-client jest realną zależnością i jest wymagany przy starcie, więc w dostarczanej kompilacji Electron rzeczywiście się ładuje — i właśnie ta odpowiedź powoduje wybór implementacji IPC Electron zamiast tej opartej na sealed box. Jej encryptDm przekazuje wywołanie do handlera crypto:encryptDm, który jest atrapą:
// crypto:encryptDm, Electron main process
const plaintextBytes = new TextEncoder().encode(plaintext)
ciphertext: toBase64(plaintextBytes) // not encryption
Zatem konstrukcje sealed box z sekcji 1 są używane wtedy, gdy libsignal jest nieobecny — w przeglądarce i w środowisku deweloperskim. W kliencie desktopowym wiadomość bezpośrednia dociera do przekaźnika jako tekst jawny zakodowany w base64. libsignal jest tam ładowany po to, by wygenerować klucz tożsamości i pakiet prekluczy; nie ustanawia żadnej sesji i niczego nie ratchetuje — crypto:ensureDmSession to log diagnostyczny bez ciała funkcji.
3. Curve25519 — krzywa leżąca u podstaw
Zarówno dostarczane konstrukcje sealed box, jak i niedostarczony projekt X3DH opierają się na tej samej krzywej eliptycznej: Curve25519, zaprojektowanej przez Daniela J. Bernsteina na potrzeby bardzo szybkiej wymiany kluczy Diffie-Hellman. Ta sekcja opisuje samą krzywą i dotyczy obu.
Równanie krzywej
y² = x³ + 486662x² + x
over the prime field 𝔽ₚ where p = 2²⁵⁵ − 19
Liczba pierwsza 2²⁵⁵ − 19 została wybrana, ponieważ umożliwia niezwykle szybką arytmetykę modularną. Współczynnik 486662 wybrano jako najmniejszą wartość dającą bezpieczną krzywą o wymaganych właściwościach bezpieczeństwa.
Generowanie kluczy
1. Klucz prywatny: Wygeneruj 32 losowe bajty za pomocą CSPRNG (kryptograficznie bezpiecznego generatora liczb pseudolosowych). Zastosuj clamping klucza: wyzeruj 3 najniższe bity i najwyższy bit oraz ustaw drugi najwyższy bit. Dzięki temu klucz jest wielokrotnością 8 i mieści się w prawidłowym zakresie.
a = random(32 bytes)
a[0] &= 248 // Clear low 3 bits
a[31] &= 127 // Clear high bit
a[31] |= 64 // Set second-highest bit
2. Klucz publiczny: Oblicz mnożenie skalarne klucza prywatnego przez punkt bazowy G = 9.
A = a · G // Scalar multiplication on Curve25519
Bezpieczeństwo opiera się na problemie logarytmu dyskretnego na krzywej eliptycznej (ECDLP): mając A i G, odzyskanie a jest obliczeniowo niewykonalne. Curve25519 zapewnia około 128 bitów bezpieczeństwa.
Wymiana kluczy Diffie-Hellman
Dwie strony (Alice i Bob) mogą obliczyć wspólny sekret, nigdy go nie przesyłając:
Alice: a (private), A = a·G (public)
Bob: b (private), B = b·G (public)
Alice computes: S = a · B = a · (b·G)
Bob computes: S = b · A = b · (a·G)
Both arrive at S = ab·G — the same shared secret
Podsłuchujący, który zna A i B (oba publiczne), nie może obliczyć S bez rozwiązania ECDLP. To założenie obliczeniowego Diffiego-Hellmana (CDH).
Sealed box to ta sama wymiana, w której jedna strona jest efemeryczna i zostaje odrzucona. X3DH, opisany niżej, to ta sama wymiana wykonana cztery razy na różnych parach kluczy, tak aby wynik nie tylko ukrywał treść, ale też uwierzytelniał obie strony.
4. X3DH — rozszerzone potrójne Diffie-Hellman
PLANOWANEX3DH rozwiązuje trudny problem: jak dwie osoby mają ustanowić szyfrowaną sesję, gdy jedna z nich jest offline? Tradycyjne Diffie-Hellman wymaga, by obie strony były online. X3DH używa wcześniej przesłanych pakietów kluczy, aby umożliwić asynchroniczne uzgadnianie klucza.
Rodzaje kluczy
Klucz tożsamości (IK)
Długoterminowa para kluczy Curve25519. Generowana raz, identyfikuje użytkownika kryptograficznie. Nigdy się nie zmienia, chyba że użytkownik zarejestruje się ponownie.
Podpisany preklucz (SPK)
Średnioterminowa para kluczy Curve25519, podpisana kluczem tożsamości. Rotowana okresowo. Podpis dowodzi, że należy ona do posiadacza klucza tożsamości.
Jednorazowy preklucz (OPK)
Efemeryczne pary kluczy Curve25519 przesyłane partiami. Każda jest używana raz, a potem usuwana. Zapewnia dodatkową warstwę forward secrecy dla pierwszej wiadomości.
Klucz efemeryczny (EK)
Nowa para kluczy Curve25519 generowana przez nadawcę dla każdej nowej sesji. Nigdy nie jest przechowywana po stronie serwera.
Uzgadnianie X3DH
Gdy Alice chce napisać do Boba (który może być offline), pobiera z serwera pakiet prekluczy Boba i wykonuje cztery obliczenia Diffie-Hellman:
// Alice has: IKₐ (her identity key), EKₐ (fresh ephemeral)
// Bob published: IKᵦ, SPKᵦ, OPKᵦ
DH1 = DH(IKₐ, SPKᵦ) // her identity ↔ his signed pre-key
DH2 = DH(EKₐ, IKᵦ) // her ephemeral ↔ his identity
DH3 = DH(EKₐ, SPKᵦ) // her ephemeral ↔ his signed pre-key
DH4 = DH(EKₐ, OPKᵦ) // her ephemeral ↔ his one-time pre-key
SK = KDF(DH1 || DH2 || DH3 || DH4)
// SK is the shared secret, derived via HKDF-SHA-256
Dlaczego cztery operacje DH?
- DH1 zapewnia wzajemne uwierzytelnienie (biorą w nim udział oba klucze tożsamości)
- DH2 gwarantuje, że klucz efemeryczny jest powiązany z tożsamością Boba
- DH3 zapewnia forward secrecy dzięki kluczowi efemerycznemu
- DH4 zapewnia dodatkowe forward secrecy. Jeśli żaden OPK nie jest dostępny, X3DH nadal działa z samymi DH1–DH3
DH1 to operacja, której sealed box nie ma. To dlatego dostarczana ścieżka nie potrafi powiedzieć, od kogo pochodzi wiadomość.
KDF — funkcja wyprowadzania klucza
Surowe wyniki DH są łączone za pomocą HKDF-SHA-256 (funkcji wyprowadzania klucza opartej na HMAC). HKDF wydobywa entropię z połączonych wyników DH i rozszerza ją do równomiernie losowego wspólnego sekretu:
PRK = HKDF-Extract(salt="", input=DH1||DH2||DH3||DH4)
SK = HKDF-Expand(PRK, info="RailGunX3DH", length=32)
5. Algorytm Double Ratchet
PLANOWANEGdy X3DH ustanowi początkowy wspólny sekret, kontrolę przejmuje Double Ratchet. Używa dwóch zazębiających się "zapadek", aby wyprowadzić nowy, unikalny klucz dla każdej wiadomości. Projekt chroni wcześniejsze wiadomości i ma przywracać ochronę przyszłych wiadomości po kolejnym kroku ratchetu.
Ratchet DH (asymetryczny)
Za każdym razem, gdy zmienia się kierunek rozmowy (Alice → Bob, potem Bob → Alice), następuje nowa wymiana kluczy Diffie-Hellman z użyciem świeżych kluczy efemerycznych. To przesuwa klucz główny naprzód o jeden krok "zapadki":
dh_out = DH(my_ratchet_key, their_ratchet_key)
root_key, chain_key = KDF(root_key, dh_out)
To właśnie zapewniałoby bezpieczeństwo po kompromitacji: napastnik, który wykradnie bieżący stan, traci dostęp z chwilą wykonania kolejnego kroku ratchetu DH.
Ratchet symetryczny (łańcuch skrótów)
Pomiędzy krokami ratchetu DH ratchet oparty na funkcji skrótu wyprowadza dla każdej wiadomości nowy klucz wiadomości z klucza łańcucha:
message_key = HMAC-SHA256(chain_key, 0x01)
chain_key = HMAC-SHA256(chain_key, 0x02)
Stary klucz łańcucha jest usuwany po każdym kroku. Klucz wiadomości szyfruje dokładnie jedną wiadomość, po czym jest usuwany. To jest mechanizm forward secrecy.
Postęp ratchetu
Zwizualizowany Double Ratchet wygląda tak. Każda strzałka to funkcja jednokierunkowa — można iść do przodu, ale nigdy do tyłu:
Root Key Chain:
RK₀ ──DH──→ RK₁ ──DH──→ RK₂ ──DH──→ RK₃ ...
Each RK step produces a Chain Key:
↓ ↓ ↓
CK₀ CK₁ CK₂
↓ ↓ ↓
Each CK step produces Message Keys:
CK₀→MK₀ CK₁→MK₃ CK₂→MK₅
CK₀→MK₁ CK₁→MK₄ CK₂→MK₆
CK₀→MK₂ CK₂→MK₇
Każdy MKₙ szyfrowałby dokładnie jedną wiadomość, a potem zostałby usunięty, więc napastnik, który zdobyłby MK₄, nie mógłby wyprowadzić MK₃ ani MK₅. Na dostarczanej dzisiaj ścieżce sealed box nie ma klucza dla pojedynczej wiadomości, który można ukraść, ani łańcucha, po którym można iść — jest jeden długoterminowy klucz, który otwiera wszystko.
6. Forward secrecy — czego by to wymagało
PLANOWANEForward secrecy oznacza, że kompromitacja kluczy długoterminowych nie kompromituje wcześniejszych kluczy sesji. Railgun dziś tego nie ma. Zapewniłyby to dwa mechanizmy i oba należą do opisanego wyżej planowanego protokołu:
Klucze efemeryczne w X3DH
Nadawca generuje nową parę kluczy efemerycznych dla każdej nowej sesji i usuwa jej prywatną połowę po uzgodnieniu. Napastnik, który później wykradnie klucze tożsamości obu stron, i tak nie odtworzy sekretu sesji. Konstrukcje sealed box rzeczywiście używają świeżego klucza efemerycznego po stronie nadawcy — ale połową wymiany po stronie odbiorcy jest jego klucz długoterminowy, i właśnie dlatego ta właściwość nie zachodzi.
Usuwanie kluczy ratchetu
Double Ratchet usuwa po użyciu stare klucze łańcucha i klucze wiadomości. Łańcuch KDF jest jednokierunkowy — mając CKₙ, można obliczyć CKₙ₊₁, ale nie CKₙ₋₁, więc kompromitacja w chwili T nie ujawnia niczego, co wysłano przed T.
Bezpieczeństwo po kompromitacji
Ratchet DH zapewnia także future secrecy: napastnik, który skompromituje bieżący stan sesji, traci go przy następnym kroku ratchetu, który wprowadza losowość, jakiej on nie posiada. Bez ratchetu skompromitowany klucz pozostaje użyteczny dla napastnika w nieskończoność.
7. Architektura przekaźnika i kolejki
Railgun przekazuje koperty i może je tymczasowo kolejkować na potrzeby dostarczenia offline. Przekaźnik jest zbudowany tak, by nie potrzebować tekstu jawnego wiadomości, ale dziś otrzymuje tekst jawny w dwóch przypadkach: wiadomości kanałowe, których żaden klient nie szyfruje, oraz wiadomości bezpośrednie z kompilacji desktopowej Electron, której handler IPC koduje w base64, zamiast szyfrować. Wiadomość bezpośrednia ze ścieżki sealed box dociera jako szyfrogram, którego przekaźnik nie może otworzyć.
Co widzi serwer
Serwer przechowuje:
- • Pakiety kluczy publicznych
- • Informacje rejestracyjne użytkownika
- • Metadane i członkostwo społeczności/kanałów
- • Szyfrogram sealed box, którego nie może otworzyć (wiadomości bezpośrednie ze ścieżki przeglądarkowej/deweloperskiej)
- • Tekst jawny wiadomości bezpośrednich zakodowany w base64 z kompilacji desktopowej Electron
- • Treść wiadomości kanałowych, dopóki nie zostanie dostarczone szyfrowanie kanałów
- • Rekordy audytu uwierzytelniania
Serwer nigdy nie otrzymuje:
- • Kluczy prywatnych ani kluczy tożsamości
- • Stanu sesji klienta
- • Trwałej kopii historii Twoich wiadomości
Wiadomości są przekazywane odbiorcom online w czasie rzeczywistym przez WebSocket. Dla urządzeń offline koperty są kolejkowane w Redis z maksymalnym TTL wynoszącym 30 dni i usuwane, gdy urządzenie je potwierdzi. Nie ma serwerowego archiwum wiadomości — historia klienta znajduje się w pamięci lokalnej na Twoim własnym urządzeniu, więc utracone urządzenie to utracona historia.
8. Podsumowanie właściwości bezpieczeństwa
| Właściwość | Mechanizm | Status |
|---|---|---|
| Poufność wiadomości bezpośrednich | Sealed box tylko na ścieżce przeglądarkowej/deweloperskiej; handler desktopowy Electron koduje w base64 | Częściowe |
| Integralność wiadomości bezpośrednich | Znacznik Poly1305 na ścieżce sealed box; ścieżka desktopowa Electron wysyła nieuwierzytelnione base64 | Częściowe |
| Klucze pozostają na urządzeniu | Lokalny magazyn kluczy; żadna ścieżka kodu nie przesyła kluczy prywatnych | Zaimplementowane |
| Dostarczanie offline | Koperty kolejkowane w Redis z 30-dniowym TTL, usuwane po potwierdzeniu | Zaimplementowane |
| Poufność kanałów | Dystrybucja klucza grupowego nie jest zaimplementowana; treść jest wysyłana w czytelnej postaci | Niezaimplementowane |
| Uwierzytelnianie nadawcy | Sealed box jest z założenia anonimowy | Niezaimplementowane |
| Forward secrecy | Wymaga Double Ratchet, którego żaden klient nie tworzy | Planowane |
| Odzyskiwanie po kompromitacji | Wymaga ratchetu DH, którego żaden klient nie tworzy | Planowane |
| Niezależny audyt | Nie deklaruje się żadnego ukończonego zewnętrznego audytu kryptograficznego | Nieukończone |
| Gwarancja postkwantowa | Nie deklaruje się żadnej ogólnej gwarancji bezpieczeństwa postkwantowego | Nie jest deklarowane |
Skąd pochodzi ta strona
Każdy status powyżej został odczytany z kodu źródłowego klienta, a nie z dokumentu projektowego. Repozytorium klienta nie jest obecnie publiczne, więc nie możemy prosić, byś sprawdził je samodzielnie — i to jest powód, by traktować tę stronę jako ujawnienie, a nie jako dowód. Pytania o konkretne twierdzenie są mile widziane pod adresem security@railgun.chat.