Довідник із реалізації шифрування
Шифрування, яке працює, вужче за те шифрування, яке написане, і ця сторінка існує саме заради цього розриву. sealed box з libsodium — єдине шифрування повідомлень, яке Railgun виконує, і виконує його лише на тому клієнтському шляху, де нативний модуль libsignal відсутній. У настільній збірці Electron, де libsignal таки завантажується, обробник, що шифрує особисті повідомлення, досі є заглушкою, яка кодує відкритий текст у base64. Повідомлення спільнот і каналів не шифруються на жодному шляху. Протокол у стилі Signal, описаний у другій половині цієї сторінки, написаний і переглянутий — і не під'єднаний до жодного клієнта, який можна завантажити.
Це архітектурний довідник, а не незалежний аудит безпеки. Розділи 1 і 2 описують те, що постачається. Розділи з 4 до 7 описують проєкт, до якого Railgun рухається, і всюди позначені як Заплановано. Ніщо тут не слід читати як твердження, що клієнт Railgun уже забезпечує властивість, позначену як заплановану.
Що постачається сьогодні
| Шлях | Механізм | Статус |
|---|---|---|
| Особисті повідомлення — браузер і збірки для розробки | sealed box з libsodium — узгодження ключів X25519, автентифіковане шифрування XSalsa20-Poly1305. Обирається, коли нативний модуль libsignal відсутній. | Шифрується |
| Особисті повідомлення — настільна збірка Electron | libsignal завантажується, тож encryptDm скеровується до обробника Electron IPC — заглушки, яка кодує відкритий текст у base64. Ретранслятор отримує читабельний текст. | Не шифрується |
| Повідомлення спільнот і каналів | Надсилаються як відкритий текст, загорнутий у base64. Шифрування каналів написане, але повертає маркер відкритого тексту. | Не шифрується |
| X3DH + Double Ratchet | Реалізовано поверх libsignal, імпортується лише як тип. Жоден шлях виконання його не створює. | Заплановано |
| Зберігання приватних ключів | Ключі генеруються і зберігаються на пристрої. Жоден шлях у коді не передає приватні ключі чи ключовий матеріал ідентичності. | Реалізовано |
1. Sealed box — шифрування, яке працює
Там, де Railgun взагалі шифрує особисте повідомлення, він робить це за допомогою sealed box з libsodium — конструкції crypto_box_seal. Вона поєднує узгодження ключів X25519 з автентифікованим шифруванням XSalsa20-Poly1305. Це справжня, добре перевірена криптографія. Це також навмисно проста конструкція, і її межі мають значення. Які клієнти до неї доходять — тема розділу 2: браузерні збірки та збірки для розробки доходять, настільна збірка Electron — ні.
Як будується 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)
Отримувач відновлює ефемерний відкритий ключ із початку повідомлення, наново обчислює той самий спільний секрет своїм приватним ключем і відкриває коробку. Тег Poly1305 означає, що змінений шифротекст не відкриється, а не розшифрується у сміття.
Що це вам дає
- • Конфіденційність від ретранслятора і від будь-кого на лінії зв'язку.
- • Цілісність — втручання виявляється, а не розшифровується мовчки.
- • Анонімність відправника: ефемерний ключ не розкриває нічого про те, хто його надіслав.
- • Жодного встановлення сеансу, тож це працює, коли отримувач офлайн.
Чого це вам не дає
- • Немає прямої секретності. Довготривалий приватний ключ отримувача відкриває кожне повідомлення, коли-небудь йому надіслане. Якщо він витече, разом із ним витече й перехоплений шифротекст.
- • Немає автентифікації відправника. sealed box анонімний за побудовою; він нічого не доводить про те, хто його написав.
- • Немає відновлення після компрометації. Немає храповика, щоб піти далі за компрометацію ключа.
Саме ці три прогалини й покликані закрити X3DH і Double Ratchet, тому ця робота й триває. Доки вона не завершена, чесний підсумок щодо особистого повідомлення в sealed box такий: сильно проти пасивного спостерігача мережі й проти ретранслятора, слабко проти супротивника, який зрештою отримає ключ із пристрою отримувача. Цей підсумок стосується лише тих місць, де цей код справді виконується.
2. Що не шифрується
Повідомлення спільнот і каналів — основна маса того, що більшість користувачів Railgun насправді пишуть, — сьогодні не шифруються на жодному клієнтському шляху. Функція шифрування каналів існує, і вона відверта сама про себе: вона записує попередження в журнал і повертає відкритий текст, загорнутий у base64 за рядком-маркером, щоб ніхто далі за ланцюжком не сплутав його з шифротекстом.
// What encryptChannel actually returns
logger.warn("Channel encryption not yet implemented")
ciphertext = base64("[DEVCRYPTO:PLAINTEXT]" + plaintext)
Практичний наслідок: щодо вмісту спільнот і каналів сервер Railgun перебуває в тому самому становищі, що й будь-який звичайний чат-сервер. Він може читати те, що ви там пишете. Розповсюдження групових ключів — ключ для кожного каналу, переданий учасникам усередині sealed box, — це заплановане виправлення, і воно не постачається.
Особисті повідомлення в настільній збірці Electron
Друга прогалина — це особисті повідомлення в клієнті, який запускає більшість людей, і її легко викласти навпаки, тож ось логіка вибору в тому вигляді, в якому її виконує код. initCrypto() запитує головний процес, чи завантажився libsignal. @signalapp/libsignal-client — це справжня залежність, яка потрібна під час запуску, тож у постачуваній збірці Electron він таки завантажується — і саме ця відповідь обирає реалізацію через Electron IPC замість реалізації на sealed box. Її encryptDm перенаправляє до обробника crypto:encryptDm, який є заглушкою:
// crypto:encryptDm, Electron main process
const plaintextBytes = new TextEncoder().encode(plaintext)
ciphertext: toBase64(plaintextBytes) // not encryption
Отже, sealed box із розділу 1 задіюються тоді, коли libsignal відсутній — у браузері та під час розробки. У настільному клієнті особисте повідомлення доходить до ретранслятора як відкритий текст, закодований у base64. libsignal завантажується там, щоб згенерувати ключ ідентичності та набір попередніх ключів; він не встановлює жодного сеансу і нічого не прокручує храповиком — crypto:ensureDmSession це налагоджувальний запис у журнал без тіла.
3. Curve25519 — базова крива
І sealed box, які постачаються, і проєкт X3DH, який не постачається, спираються на ту саму еліптичну криву: Curve25519, розроблену Деніелом Дж. Бернстайном для швидкісного обміну ключами за Діффі-Геллманом. Цей розділ описує саму криву і стосується обох.
Рівняння кривої
y² = x³ + 486662x² + x
over the prime field 𝔽ₚ where p = 2²⁵⁵ − 19
Просте число 2²⁵⁵ − 19 було обране тому, що воно дає змогу виконувати модулярну арифметику надзвичайно швидко. Коефіцієнт 486662 було обрано як найменше значення, що дає безпечну криву з потрібними властивостями безпеки.
Генерація ключів
1. Приватний ключ: Згенеруйте 32 випадкові байти за допомогою CSPRNG (криптографічно стійкого генератора псевдовипадкових чисел). Обріжте ключ, обнуливши три молодші біти та старший біт і встановивши другий за старшинством біт. Це гарантує, що ключ кратний 8 і лежить у допустимому діапазоні.
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. Відкритий ключ: Обчисліть скалярне множення приватного ключа на базову точку G = 9.
A = a · G // Scalar multiplication on Curve25519
Безпека спирається на задачу дискретного логарифмування на еліптичній кривій (ECDLP): маючи A і G, обчислювально неможливо відновити a. Curve25519 забезпечує приблизно 128 бітів безпеки.
Обмін ключами Діффі-Геллмана
Дві сторони (Аліса і Боб) можуть обчислити спільний секрет, жодного разу його не передаючи:
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
Перехоплювач, який знає A і B (обидва відкриті), не може обчислити S, не розв'язавши ECDLP. Це припущення обчислювальної задачі Діффі-Геллмана (CDH).
sealed box — це той самий обмін, у якому одну сторону зроблено ефемерною і викинуто. X3DH, описаний нижче, — це той самий обмін, виконаний чотири рази над різними парами ключів, так що результат не лише приховує вміст, а й автентифікує обидві сторони.
4. X3DH — розширений потрійний Діффі-Геллман
ЗАПЛАНОВАНОX3DH розв'язує складну задачу: як двоє людей встановлюють зашифрований сеанс, коли один із них офлайн? Традиційний Діффі-Геллман вимагає, щоб обидві сторони були онлайн. X3DH використовує заздалегідь завантажені набори ключів, щоб уможливити асинхронне узгодження ключів.
Типи ключів
Ключ ідентичності (IK)
Довготривала пара ключів Curve25519. Генерується один раз, криптографічно ідентифікує користувача. Ніколи не змінюється, якщо користувач не реєструється заново.
Підписаний попередній ключ (SPK)
Середньотривала пара ключів Curve25519, підписана ключем ідентичності. Періодично ротується. Підпис доводить, що він належить власникові ключа ідентичності.
Одноразовий попередній ключ (OPK)
Ефемерні пари ключів Curve25519, що завантажуються пакетами. Кожна використовується один раз, потім видаляється. Дає додатковий рівень прямої секретності для першого повідомлення.
Ефемерний ключ (EK)
Свіжа пара ключів Curve25519, яку відправник генерує для кожного нового сеансу. Ніколи не зберігається на сервері.
Рукостискання X3DH
Коли Аліса хоче написати Бобу (який може бути офлайн), вона отримує з сервера набір попередніх ключів Боба і виконує чотири обчислення Діффі-Геллмана:
// 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
Чому чотири операції DH?
- DH1 забезпечує взаємну автентифікацію (задіяні обидва ключі ідентичності)
- DH2 гарантує, що ефемерний ключ прив'язаний до ідентичності Боба
- DH3 забезпечує пряму секретність завдяки ефемерному ключу
- DH4 забезпечує додаткову пряму секретність. Якщо OPK недоступний, X3DH усе одно працює лише з DH1–DH3
DH1 — це саме та операція, якої sealed box не має. Саме тому шлях, що постачається, не може сказати вам, від кого надійшло повідомлення.
KDF — функція вироблення ключа
Сирі виходи DH об'єднуються за допомогою HKDF-SHA-256 (функції вироблення ключа на основі HMAC). HKDF видобуває ентропію зі склеєних виходів DH і розширює її у рівномірно випадковий спільний секрет:
PRK = HKDF-Extract(salt="", input=DH1||DH2||DH3||DH4)
SK = HKDF-Expand(PRK, info="RailGunX3DH", length=32)
5. Алгоритм Double Ratchet
ЗАПЛАНОВАНОЩойно X3DH встановлює початковий спільний секрет, керування перебирає Double Ratchet. Він використовує два зчеплені «храповики», щоб виводити новий унікальний ключ для кожного повідомлення. Схема захищає попередні повідомлення і має на меті відновлювати захист майбутніх повідомлень після чергового кроку храповика.
Храповик DH (асиметричний)
Щоразу, коли напрямок розмови змінюється (Аліса → Боб, потім Боб → Аліса), відбувається новий обмін ключами Діффі-Геллмана зі свіжими ефемерними ключами. Це «прокручує» кореневий ключ уперед:
dh_out = DH(my_ratchet_key, their_ratchet_key)
root_key, chain_key = KDF(root_key, dh_out)
Саме це давало б безпеку після компрометації: зловмисник, який викрав поточний стан, втрачає доступ, щойно відбувається новий крок храповика DH.
Симетричний храповик (хеш-ланцюг)
Між кроками храповика DH храповик на основі хешу виводить новий ключ повідомлення з ключа ланцюга для кожного повідомлення:
message_key = HMAC-SHA256(chain_key, 0x01)
chain_key = HMAC-SHA256(chain_key, 0x02)
Старий ключ ланцюга видаляється після кожного кроку. Ключ повідомлення шифрує рівно одне повідомлення, а потім видаляється. Це і є механізм прямої секретності.
Просування храповика
Наочно Double Ratchet виглядає так. Кожна стрілка — це одностороння функція: уперед пройти можна, назад — ніколи:
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₇
Кожен MKₙ шифрував би рівно одне повідомлення і потім видалявся б, тож зловмисник, який отримав MK₄, не зміг би вивести MK₃ чи MK₅. На шляху sealed box, який постачається сьогодні, немає ключа для окремого повідомлення, який можна вкрасти, і немає ланцюга, яким можна пройти, — є один довготривалий ключ, що відкриває все.
6. Пряма секретність — що для неї потрібно
ЗАПЛАНОВАНОПряма секретність означає, що компрометація довготривалих ключів не компрометує минулі сеансові ключі. Railgun сьогодні її не має. Її забезпечили б два механізми, і обидва належать до описаного вище запланованого протоколу:
Ефемерні ключі в X3DH
Відправник генерує свіжу ефемерну пару ключів для кожного нового сеансу і видаляє приватну половину після рукостискання. Зловмисник, який пізніше викраде ключі ідентичності обох сторін, усе одно не зможе відтворити сеансовий секрет. sealed box справді використовують свіжий ефемерний ключ на боці відправника, але половиною обміну з боку отримувача є його довготривалий ключ — саме тому ця властивість не виконується.
Видалення ключів храповика
Double Ratchet видаляє старі ключі ланцюга та ключі повідомлень після використання. Ланцюг KDF односторонній: маючи CKₙ, можна обчислити CKₙ₊₁, але не CKₙ₋₁, тож компрометація в момент T не розкриває нічого, надісланого до T.
Безпека після компрометації
Храповик DH забезпечує також майбутню секретність: зловмисник, який скомпрометував поточний стан сеансу, втрачає його на наступному кроці храповика, що вносить випадковість, якої він не має. Без храповика скомпрометований ключ залишається корисним для зловмисника необмежено довго.
7. Архітектура ретрансляції та черг
Railgun ретранслює конверти і може тимчасово ставити їх у чергу для доставки офлайн. Ретранслятор побудований так, щоб йому не був потрібен відкритий текст повідомлень, але сьогодні він отримує відкритий текст у двох випадках: повідомлення каналів, які не шифрує жоден клієнт, і особисті повідомлення з настільної збірки Electron, чий обробник IPC кодує їх у base64, а не шифрує. Особисте повідомлення зі шляху sealed box надходить як шифротекст, який ретранслятор не може відкрити.
Що бачить сервер
Сервер зберігає:
- • Набори відкритих ключів
- • Реєстраційні дані користувача
- • Метадані спільнот/каналів і склад учасників
- • Шифротекст sealed box, який він не може відкрити (особисті повідомлення зі шляху браузера/розробки)
- • Відкритий текст особистих повідомлень, закодований у base64, з настільної збірки Electron
- • Вміст повідомлень каналів — доки не буде випущено шифрування каналів
- • Записи аудиту автентифікації
Сервер ніколи не отримує:
- • Приватні ключі або ключі ідентичності
- • Стан клієнтського сеансу
- • Довготривалу копію історії ваших повідомлень
Повідомлення ретранслюються онлайн-отримувачам у реальному часі через WebSocket. Для офлайн-пристроїв конверти ставляться в чергу в Redis з максимальним TTL у 30 днів і видаляються, щойно пристрій підтверджує їх отримання. Серверного архіву повідомлень немає — історія клієнта зберігається в локальному сховищі на вашому власному пристрої, тож втрачений пристрій — це втрачена історія.
8. Підсумок властивостей безпеки
| Властивість | Механізм | Статус |
|---|---|---|
| Конфіденційність особистих повідомлень | sealed box лише на шляху браузера/розробки; обробник настільної збірки Electron кодує в base64 | Частково |
| Цілісність особистих повідомлень | Тег Poly1305 на шляху sealed box; шлях настільної збірки Electron надсилає неавтентифікований base64 | Частково |
| Ключі залишаються на пристрої | Локальне сховище ключів; жоден шлях у коді не передає приватні ключі | Реалізовано |
| Офлайн-доставка | Конверти ставляться в чергу в Redis з TTL у 30 днів і видаляються після підтвердження | Реалізовано |
| Конфіденційність каналів | Розповсюдження групових ключів не реалізовано; вміст надсилається у читабельному вигляді | Не реалізовано |
| Автентифікація відправника | sealed box анонімні за побудовою | Не реалізовано |
| Пряма секретність | Потребує Double Ratchet, який не створює жоден клієнт | Заплановано |
| Відновлення після компрометації | Потребує храповика DH, який не створює жоден клієнт | Заплановано |
| Незалежний аудит | Завершений сторонній криптографічний аудит не заявляється | Не завершено |
| Постквантова гарантія | Жодної загальної гарантії постквантової безпеки не заявляється | Не заявляється |
Звідки взялася ця сторінка
Кожен статус вище було прочитано з вихідного коду клієнта, а не з проєктного документа. Репозиторій клієнта наразі не є публічним, тож ми не можемо попросити вас перевірити його самостійно — і це причина розцінювати цю сторінку як розкриття інформації, а не як доказ. Запитання щодо конкретного твердження вітаються на security@railgun.chat.