Powrót do strony głównej
Kryptografia

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żkaMechanizmStatus
Wiadomości bezpośrednie — kompilacje przeglądarkowe i deweloperskieSealed box z libsodium — uzgadnianie klucza X25519, szyfrowanie uwierzytelnione XSalsa20-Poly1305. Wybierane, gdy natywny moduł libsignal jest nieobecny.Szyfrowane
Wiadomości bezpośrednie — kompilacja desktopowa Electronlibsignal 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łoweWysyłane jako tekst jawny opakowany w base64. Szyfrowanie kanałów jest napisane, ale zwraca znacznik tekstu jawnego.Nieszyfrowane
X3DH + Double RatchetZaimplementowane w oparciu o libsignal, importowane wyłącznie jako typ. Żadna ścieżka wykonania go nie tworzy.Planowane
Przechowywanie kluczy prywatnychKlucze 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

PLANOWANE
Napisane w oparciu o libsignal i możliwe do przejrzenia w kodzie źródłowym. Jest importowane wyłącznie jako typ — żaden dostarczany klient go nie tworzy. Wszystko w tej sekcji traktuj jako projekt docelowy, a nie obecne zachowanie.

X3DH 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

PLANOWANE
Żadne wydanie Railguna nie stosuje dziś mechanizmu ratchet do kluczy. Na ścieżce sealed box każda wiadomość bezpośrednia jest szyfrowana tym samym długoterminowym kluczem odbiorcy; na ścieżce desktopowej Electron nie jest szyfrowana wcale.

Gdy 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

PLANOWANE

Forward 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śćMechanizmStatus
Poufność wiadomości bezpośrednichSealed box tylko na ścieżce przeglądarkowej/deweloperskiej; handler desktopowy Electron koduje w base64Częściowe
Integralność wiadomości bezpośrednichZnacznik Poly1305 na ścieżce sealed box; ścieżka desktopowa Electron wysyła nieuwierzytelnione base64Częściowe
Klucze pozostają na urządzeniuLokalny magazyn kluczy; żadna ścieżka kodu nie przesyła kluczy prywatnychZaimplementowane
Dostarczanie offlineKoperty kolejkowane w Redis z 30-dniowym TTL, usuwane po potwierdzeniuZaimplementowane
Poufność kanałówDystrybucja klucza grupowego nie jest zaimplementowana; treść jest wysyłana w czytelnej postaciNiezaimplementowane
Uwierzytelnianie nadawcySealed box jest z założenia anonimowyNiezaimplementowane
Forward secrecyWymaga Double Ratchet, którego żaden klient nie tworzyPlanowane
Odzyskiwanie po kompromitacjiWymaga ratchetu DH, którego żaden klient nie tworzyPlanowane
Niezależny audytNie deklaruje się żadnego ukończonego zewnętrznego audytu kryptograficznegoNieukończone
Gwarancja postkwantowaNie deklaruje się żadnej ogólnej gwarancji bezpieczeństwa postkwantowegoNie 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.