Справочник по реализации шифрования
Шифрование, которое работает, уже того шифрования, которое написано, и эта страница существует ради этого разрыва. 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.