На головну
Криптографія

Довідник із реалізації шифрування

Шифрування, яке працює, вужче за те шифрування, яке написане, і ця сторінка існує саме заради цього розриву. sealed box з libsodium — єдине шифрування повідомлень, яке Railgun виконує, і виконує його лише на тому клієнтському шляху, де нативний модуль libsignal відсутній. У настільній збірці Electron, де libsignal таки завантажується, обробник, що шифрує особисті повідомлення, досі є заглушкою, яка кодує відкритий текст у base64. Повідомлення спільнот і каналів не шифруються на жодному шляху. Протокол у стилі Signal, описаний у другій половині цієї сторінки, написаний і переглянутий — і не під'єднаний до жодного клієнта, який можна завантажити.

Це архітектурний довідник, а не незалежний аудит безпеки. Розділи 1 і 2 описують те, що постачається. Розділи з 4 до 7 описують проєкт, до якого Railgun рухається, і всюди позначені як Заплановано. Ніщо тут не слід читати як твердження, що клієнт Railgun уже забезпечує властивість, позначену як заплановану.

Що постачається сьогодні

ШляхМеханізмСтатус
Особисті повідомлення — браузер і збірки для розробкиsealed box з libsodium — узгодження ключів X25519, автентифіковане шифрування XSalsa20-Poly1305. Обирається, коли нативний модуль libsignal відсутній.Шифрується
Особисті повідомлення — настільна збірка Electronlibsignal завантажується, тож 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 — розширений потрійний Діффі-Геллман

ЗАПЛАНОВАНО
Написано поверх libsignal і доступне для перегляду в кодовій базі. Імпортується лише як тип — жоден постачуваний клієнт його не створює. Вважайте все в цьому розділі цільовим проєктом, а не поточною поведінкою.

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

ЗАПЛАНОВАНО
Жоден випуск Railgun сьогодні не прокручує ключі храповиком. На шляху sealed box кожне особисте повідомлення шифрується на той самий довготривалий ключ отримувача; на шляху настільної збірки Electron воно не шифрується взагалі.

Щойно 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.