Essenger Protocol
Сквозное шифрование с прямой секретностью и пост-квантовой гибридной криптографией
1. Введение
Essenger Protocol обеспечивает сквозное шифрование сообщений в личных и групповых чатах на веб-, iOS- и macOS-клиентах. Он сочетает расширенный тройной обмен Диффи-Хеллмана (X3DH) с прямой секретностью (Double Ratchet) каждого сообщения, аутентифицированное шифрование AES-256-GCM и пост-квантовый гибридный слой — квантовый щит (ML-KEM-768, FIPS 203).
Essenger Protocol достигает четырёх целей безопасности:
- Конфиденциальность — сообщения может прочитать только адресат.
- Целостность — любое изменение шифротекста обнаруживается через аутентификационные теги AES-GCM и HMAC-SHA256.
- Прямая секретность (Forward secrecy) — компрометация долгосрочных ключей не раскрывает прошлые сообщения. Эфемерные ключи X25519 генерируются для каждого сообщения (уровень аккаунта) или на каждый шаг DH-рэтчета (уровень сессии).
- Пост-квантовая устойчивость — гибридный режим v3/v4 комбинирует X25519 с ML-KEM-768, чтобы сообщения оставались защищёнными даже при появлении квантового компьютера, способного взломать криптографию на эллиптических кривых.
Шифрование обязательно. Режима передачи открытого текста не существует.
2. Модель угроз
От чего мы защищаем
- Пассивный перехватчик — злоумышленник, наблюдающий за всем сетевым трафиком между клиентами и сервером, не может прочитать содержимое сообщений.
- Скомпрометированный сервер — сервер хранит только шифротексты, публичные ключи и зашифрованные блобы приватных ключей. Полная компрометация сервера не раскрывает открытый текст сообщений или приватные ключи.
- Будущий квантовый компьютер — гибридные сообщения v3/v4 комбинируют классический X25519 с ML-KEM-768. Злоумышленник, который записывает шифротекст сегодня и позже получит криптографически значимый квантовый компьютер, не сможет расшифровать гибридно-зашифрованные сообщения.
- Имперсонация при компрометации ключа — аутентификация отправителя через HMAC-SHA256 по статическому общему секрету ECDH предотвращает подмену личности даже при компрометации сервера.
От чего мы не защищаем
- Компрометация устройства — если злоумышленник получает полный контроль над устройством (root-доступ, вредоносное ПО), он может читать расшифрованные сообщения из памяти или локального хранилища.
- Метаданные (частично) — сервер знает, кто с кем общается и когда. Sealed sender скрывает личность отправителя внутри E2E-нагрузки, но сервер всё ещё видит получателя и время.
- Обход аутентификации пользователя — протокол не защищает от социальной инженерии или перехвата аккаунта на уровне приложения. TOTP 2FA и TOFU смягчают, но не устраняют этот риск.
3. Криптографические примитивы
| Примитив | Алгоритм | Ссылка | Применение |
|---|---|---|---|
| Согласование ключей | X25519 | RFC 7748 | ECDH в X3DH, Double Ratchet, шифрование на уровне аккаунта, аутентификация отправителя |
| Цифровые подписи | Ed25519 | RFC 8032 | Проверка подписанного пре-ключа |
| Аутентифицированное шифрование | AES-256-GCM | NIST SP 800-38D | Шифрование сообщений, оборачивание ключей, шифрование хранилища |
| Аутентификация сообщений | HMAC-SHA256 | RFC 2104 | KDF симметричного рэтчета, аутентификация отправителя, MAC хранилища |
| Вывод ключей | HKDF-SHA256 | RFC 5869 | Вывод корневого ключа, вывод ключа сообщения, комбинирование гибридного секрета |
| Пост-квантовый KEM | ML-KEM-768 | FIPS 203 | Гибридная инкапсуляция ключей в сообщениях v3/v4 |
| KDF для пароля (основной) | Argon2id | RFC 9106 | Шифрование приватного ключа (64 МБ, t=3, p=1) |
| KDF для пароля (запасной) | PBKDF2-SHA256 | RFC 8018 | Устаревшее шифрование приватного ключа (600K итераций) |
Библиотеки реализации
- Веб-клиент:
@noble/curves(X25519, Ed25519),@noble/hashes(HKDF, HMAC, SHA-256),@noble/post-quantum(ML-KEM-768), Web Crypto API (AES-GCM),argon2-browser(Argon2id) - iOS/macOS-клиент: Apple CryptoKit (
Curve25519,AES.GCM,HKDF<SHA256>,HMAC<SHA256>)
4. Протокол обмена ключами (X3DH)
Установка сессии использует протокол расширенного тройного обмена Диффи-Хеллмана (X3DH). Каждое устройство поддерживает следующий ключевой материал:
- Identity Key (IK) — долгосрочная пара ключей X25519, генерируется при регистрации устройства. Публичный ключ загружается на сервер как
identityKeyPublic. - Identity Signing Key — пара ключей Ed25519, используется для подписи Signed Pre-Key. Загружается как
identitySigningKeyPublic. - Signed Pre-Key (SPK) — среднесрочная пара ключей X25519, подписанная Identity Signing Key (
ed25519Sign(signingPriv, spk.publicKey)). Также служит начальным DH-рэтчет-ключом для ответчика. - One-Time Pre-Keys (OTPK) — пакет из 20 эфемерных пар ключей X25519, загружаемых при регистрации. Каждый OTPK используется не более одного раза.
Поток протокола
Спецификация KDF
function x3dhKdf(ikm):
salt = 0x00 * 32 // 32 нулевых байта
info = "EssengerX3DH"
return HKDF-SHA256(ikm, salt, info, outputLen=32)
Инициатор всегда проверяет подпись Ed25519 на Signed Pre-Key перед продолжением. Если проверка не проходит, установка сессии прерывается. Это предотвращает подмену SPK злоумышленником.
5. Прямая секретность (Double Ratchet)
После завершения X3DH обе стороны инициализируют сессию Double Ratchet. Рэтчет обеспечивает прямую секретность и восстановление после взлома через непрерывную эволюцию ключей.
Состояние рэтчета
| Поле | Тип | Описание |
|---|---|---|
DHs | Пара ключей X25519 | Наша текущая пара DH-рэтчет-ключей |
DHr | Публичный ключ X25519 | Их текущий публичный DH-рэтчет-ключ |
RK | 32 байта | Корневой ключ |
CKs | 32 байта | Цепной ключ отправки |
CKr | 32 байта | Цепной ключ приёма |
Ns | целое число | Счётчик отправленных сообщений |
Nr | целое число | Счётчик полученных сообщений |
PN | целое число | Длина предыдущей цепи отправки |
skippedKeys | словарь | "DHr_b64:N" → ключ сообщения (макс. 500) |
processedSet | словарь/множество | ID уже расшифрованных сообщений (защита от replay) |
Инициализация
Алиса (инициатор) выполняет немедленный шаг DH-рэтчета, используя SPK Боба как начальный удалённый рэтчет-ключ:
// Алиса создаёт сессию
DHs = X25519.generate()
dhOut = X25519(DHs.priv, SPKb)
(RK, CKs) = kdfRK(SK, dhOut)
CKr = null
DHr = SPKb
Боб (ответчик) использует свой SPK как начальную DH-рэтчет-пару без начальной цепи отправки:
// Боб создаёт сессию
DHs = SPKb_keypair
DHr = null
RK = SK
CKs = null
CKr = null
Симметричный рэтчет ключей
function kdfCK(ck):
mk = HMAC-SHA256(ck, 0x01) // ключ сообщения
newCK = HMAC-SHA256(ck, 0x02) // следующий цепной ключ
return (newCK, mk)
Шаг DH-рэтчета
function dhRatchetStep(state, newDHr):
state.PN = state.Ns
state.Ns = 0
state.Nr = 0
state.DHr = newDHr
// Выводим цепь приёма из текущей DH-пары
dhOut1 = X25519(state.DHs.priv, newDHr)
(RK1, CKr) = kdfRK(state.RK, dhOut1)
state.RK = RK1
state.CKr = CKr
// Генерируем новую DH-пару и выводим цепь отправки
state.DHs = X25519.generate()
dhOut2 = X25519(state.DHs.priv, newDHr)
(RK2, CKs) = kdfRK(RK1, dhOut2)
state.RK = RK2
state.CKs = CKs
KDF корневого ключа
function kdfRK(rk, dhOut):
output = HKDF-SHA256(
ikm = dhOut,
salt = rk,
info = "EssengerRatchet",
len = 64
)
return (output[0:32], output[32:64]) // (новыйКорневойКлюч, цепнойКлюч)
Шифрование сообщения
function encrypt(state, plaintext):
(newCK, mk) = kdfCK(state.CKs)
state.CKs = newCK
header = { dh: state.DHs.pub, pn: state.PN, n: state.Ns }
state.Ns += 1
ad = localIK || remoteIK || encodeHeader(header)
ciphertext = AES-256-GCM(mk, plaintext, ad)
return (header, ciphertext)
Связанные данные (AEAD AD)
AD привязывает шифротекст к identity-ключам обеих сторон и заголовку сообщения:
AD = senderIdentityKey(32) || receiverIdentityKey(32) || encodeHeader(header)
// Бинарное кодирование заголовка (детерминистическое, без зависимости от JSON)
encodeHeader(header) = dh(32 байта) || pn(4 байта, big-endian) || n(4 байта, big-endian)
// Общая длина AD: 32 + 32 + 40 = 104 байта
Комбинированный формат AES-GCM
Весь шифротекст AES-256-GCM использует комбинированный формат, совместимый с Apple CryptoKit:
Пропущенные ключи сообщений
Сообщения, пришедшие не по порядку, обрабатываются путём сохранения пропущенных ключей,
индексированных по "DHr_base64:messageNumber". Константы:
MAX_SKIP = 50— максимум сообщений, которые можно пропустить за один шаг цепи.MAX_TOTAL_SKIPPED = 500— максимум сохранённых пропущенных ключей на сессию.
Откат состояния при ошибке
Перед изменением состояния при расшифровке создаётся снимок рэтчет-состояния. Если расшифровка AES-GCM не удаётся (несовпадение аутентификационного тега), состояние откатывается к снимку. Это предотвращает повреждение состояния злонамеренными или повреждёнными сообщениями.
6. Квантовый щит (ML-KEM-768, v3/v4)
Шифрование на уровне аккаунта поддерживает гибридный пост-квантовый режим, комбинирующий X25519 ECDH с ML-KEM-768 (механизм инкапсуляции ключей на основе модульных решёток, FIPS 203). Это обеспечивает защиту от атаки «собери сейчас, расшифруй потом»: даже если весь шифротекст записан сегодня, будущий квантовый компьютер не сможет вычислить ключи сообщений.
Версии формата сообщений
| Версия | Байт-префикс | Описание |
|---|---|---|
| v1 | 0x01 (неявный) | Статический X25519 ECDH. Устаревший, без прямой секретности. Сохранён только для совместимости со старыми сообщениями. |
| v2 | 0x02 | Эфемерный X25519 ECDH. Прямая секретность для каждого сообщения. |
| v3 | 0x03 | Гибрид ML-KEM-768 + эфемерный X25519. Фиксированный размер PQ-шифротекста (1088 байт). |
| v4 | 0x04 | Гибрид ML-KEM-768 + эфемерный X25519, с 2-байтным префиксом длины PQ-шифротекста для расширяемости. |
Wire-формат v2 (текущий по умолчанию)
// Вывод ключа v2
ephPriv = random(32)
ephPub = X25519.publicKey(ephPriv)
shared = X25519(ephPriv, recipientPub)
messageKey = HKDF-SHA256(shared, salt=ephPub, info="essenger-e2e-v2", len=32)
// ephPriv обнуляется сразу после использования
Wire-формат v4 (гибридный, текущий)
Комбинирование гибридных ключей
// Отправитель (шифрование)
x25519Shared = X25519(ephPriv, recipientX25519Pub)
(pqCipherText, pqShared) = ML-KEM-768.Encapsulate(recipientPQPub)
combinedSecret = x25519Shared || pqShared
messageKey = HKDF-SHA256(combinedSecret, salt=ephPub, info="essenger-hybrid-v1", len=32)
// Получатель (расшифровка)
x25519Shared = X25519(myPriv, ephPub)
pqShared = ML-KEM-768.Decapsulate(pqCipherText, myPQPriv)
combinedSecret = x25519Shared || pqShared
messageKey = HKDF-SHA256(combinedSecret, salt=ephPub, info="essenger-hybrid-v1", len=32)
Гибридная комбинация безопасна, пока хотя бы один из двух алгоритмов не взломан. Если X25519 будет взломан квантовым компьютером, но ML-KEM-768 останется надёжным — комбинированный ключ по-прежнему безопасен, и наоборот. Конкатенация с последующим HKDF является безопасным комбинатором в стандартной модели.
Wire-формат v3 (устаревший гибрид)
v3 идентичен v4, но без поля pqCTLen (размер PQ-шифротекста жёстко задан как 1088 байт). v4 добавляет префикс длины для будущей расширяемости на бо́льшие параметры KEM.
Обратная совместимость
При расшифровке проверяется первый байт шифротекста для определения версии:
0x04 для v4, 0x03 для v3, 0x02 для v2.
Если ни один не совпал — сообщение обрабатывается как v1 (устаревший статический ECDH). Новые сообщения
всегда отправляются как v2 (когда у получателя нет PQ-ключа) или v4 (когда PQ-ключ есть).
7. Аутентификация отправителя
Каждое сообщение на уровне аккаунта содержит HMAC аутентификации отправителя, доказывающий, что отправитель владеет приватным ключом, соответствующим заявленному публичному ключу, без участия сервера.
Вычисление
function computeSenderAuth(ciphertextBytes, senderPriv, recipientPub):
staticShared = X25519(senderPriv, recipientPub)
authKey = HKDF-SHA256(staticShared, salt=empty, info="essenger-sender-auth", len=32)
mac = HMAC-SHA256(authKey, ciphertextBytes)
// Обнуляем staticShared и authKey немедленно
return mac // 32 байта
Проверка
function verifySenderAuth(ciphertextBytes, expectedMac, senderPub, myPriv):
staticShared = X25519(myPriv, senderPub)
authKey = HKDF-SHA256(staticShared, salt=empty, info="essenger-sender-auth", len=32)
computed = HMAC-SHA256(authKey, ciphertextBytes)
return constantTimeEqual(computed, expectedMac)
Формат конверта
Аутентифицированный шифротекст передаётся как JSON-конверт:
{
"ct": "<base64 версионированный шифротекст>",
"sa": "<base64 HMAC-SHA256, 32 байта>"
}
Кросс-платформенная совместимость
JSON-обёртка гарантирует, что и веб-клиент (@noble/curves), и iOS-клиент (CryptoKit)
могут разбирать один и тот же формат сообщений. Базовый общий секрет X25519 детерминистичен
во всех реализациях.
Аутентификация отправителя проверяется только при наличии публичного ключа отправителя в локальном TOFU-кэше. Для неизвестных отправителей проверка пропускается (мягкая деградация). После того как TOFU запомнит ключ, все последующие сообщения аутентифицируются.
8. Sealed Sender
Sealed sender скрывает личность отправителя внутри E2E-зашифрованной нагрузки, чтобы серверу не нужно было знать, кто отправил сообщение (личность отправителя узнаёт только получатель).
Формат
Когда sealed sender активен, открытый текст оборачивается в JSON-конверт перед шифрованием:
{
"_ss": true, // флаг sealed sender
"sid": "<ID отправителя>",
"sun": "<имя отправителя>",
"msg": "<текст сообщения>"
}
Этот JSON шифруется публичным ключом получателя стандартным путём v2/v3/v4.
Получатель расшифровывает, обнаруживает _ss: true и извлекает
настоящую личность отправителя из нагрузки.
Логика распаковки
function unwrapSealedSender(plaintext):
if plaintext starts with '{':
parsed = JSON.parse(plaintext)
if parsed._ss === true && typeof parsed.msg === 'string':
return { text: parsed.msg, senderId: parsed.sid, senderUsername: parsed.sun }
return plaintext // не sealed sender, возвращаем как есть
Sealed sender скрывает личность отправителя от сервера на уровне приложения. Однако сервер по-прежнему видит получателя, время отправки и размер сообщения. Метаданные транспортного уровня (IP-адреса, идентификаторы TLS-сессий) этим механизмом не скрываются.
9. Групповое E2E v2
Групповое шифрование использует эфемерный ECDH для каждого сообщения с оборачиванием ключа для каждого участника. Это обеспечивает прямую секретность групповых сообщений — компрометация долгосрочного ключа любого участника не раскрывает прошлые сообщения группы, потому что эфемерный приватный ключ уничтожается после каждого сообщения.
Поток шифрования
- Генерируем случайный 32-байтный
messageKey. - Генерируем эфемерную пару ключей X25519
(ephPriv, ephPub). - Шифруем открытый текст с
messageKeyчерез AES-256-GCM (случайный 12-байтный IV). - Для каждого участника группы:
- Вычисляем:
shared = X25519(ephPriv, memberPub) - Выводим ключ оборачивания:
wrappingKey = HKDF-SHA256(shared, salt=ephPub, info="essenger-group-e2e-v2", len=32) - Оборачиваем:
wrappedMK = AES-256-GCM(wrappingKey, messageKey, randomIV) - Сохраняем как:
base64(mkIV(12) || wrappedMK+tag) - Обнуляем ключ оборачивания немедленно.
- Вычисляем:
- Обнуляем
ephPriv(эфемерный приватный ключ никогда не покидает память).
Wire-формат
{
"v": 2,
"eph": "<base64 эфемерный публичный ключ, 32 байта>",
"iv": "<base64 AES-GCM nonce, 12 байт>",
"ct": "<base64 шифротекст + тег>",
"mk": {
"<username1>": "<base64 mkIV(12) || wrappedMK+tag>",
"<username2>": "<base64 mkIV(12) || wrappedMK+tag>",
...
}
}
Поток расшифровки
- Разбираем JSON-нагрузку, проверяем
v === 2. - Ищем свой username в словаре
mk. Если отсутствует — сообщение не предназначено для этого пользователя. - Выводим ключ оборачивания:
shared = X25519(myPriv, ephPub), затем HKDF как выше. - Разворачиваем: разделяем
mkIV(12) || wrappedMK+tag, расшифровываем ключом оборачивания. - Расшифровываем шифротекст развёрнутым
messageKey.
Прямая секретность в группах
Поскольку эфемерный приватный ключ генерируется заново для каждого сообщения и уничтожается сразу после вычисления всех ключей оборачивания, компрометация статического ключа любого участника раскрывает только будущие сообщения (где атакующий может вычислить ECDH), но не прошлые.
Добавление и удаление участников
- Добавление: новые участники просто включаются в словарь
mkпоследующих сообщений. Они не могут расшифровать сообщения, отправленные до их добавления (нет доступа к предыдущим эфемерным ключам). - Удаление: удалённые участники исключаются из словаря
mk. Поскольку каждое сообщение использует свежий эфемерный ключ, удалённый участник не может вычислить будущие ключи оборачивания.
Устаревший формат v1
Протокол v1 для групп использовал статические ключи отправителя без оборачивания для каждого участника.
Сохранён только для обратной совместимости (decryptGroupMessageV1 делегирует в accountE2E.decrypt).
Авто-определение сначала проверяет groupCiphertextV2, откатываясь к ciphertext для v1.
10. Управление ключами
Trust On First Use (TOFU)
Identity-ключи верифицируются через TOFU. При первом контакте с устройством пользователя
identity-ключ сохраняется локально под ключом tofu_{username}:{deviceID}. При последующих
сессиях сохранённый ключ сравнивается с полученным. Если они отличаются, сессия отклоняется
с кодом ошибки IDENTITY_KEY_CHANGED.
// TOFU-проверка (по устройству)
storedIK = load("tofu_{username}:{deviceID}")
if storedIK exists and storedIK !== remoteIK:
throw "Identity key changed — возможна MITM-атака"
else if storedIK absent:
save("tofu_{username}:{deviceID}", remoteIK)
Числа безопасности (Safety Numbers)
Пользователи могут вручную верифицировать друг друга, сравнивая числа безопасности. Число безопасности вычисляется путём лексикографической сортировки публичных ключей обеих сторон, конкатенации, хеширования SHA-256 и форматирования в 12 групп по 5 цифр:
function generateSafetyNumber(myPubB64, theirPubB64):
sorted = [myPubB64, theirPubB64].sort()
combined = sorted[0] + sorted[1]
hash = SHA-256(combined)
digits = ""
for i in 0..29:
digits += String(hash[i] % 100).padStart(2, '0')
// Форматируем как 12 групп по 5 цифр
return groups of 5, space-separated // напр. "04821 73920 ..."
Аудит-лог прозрачности ключей
Сервер ведёт append-only таблицу key_audit_log, фиксирующую все события,
связанные с ключами. Каждая запись содержит:
| Поле | Тип | Описание |
|---|---|---|
user_id | UUID | Пользователь, чей ключ изменился |
action | string | Тип события (register, rotate, revoke) |
public_key_hash | string | SHA-256 хеш нового публичного ключа |
device_info | string | Имя/тип устройства, инициировавшего изменение |
ip_address | string | IP-адрес источника запроса |
created_at | datetime | Временная метка |
Аудит-лог защищён от записи политиками PostgreSQL для предотвращения ретроактивной модификации.
Ротация ключей
- Signed Pre-Keys периодически перегенерируются. Назначается новый случайный
keyID, генерируется новая пара X25519, и публичный ключ подписывается identity signing key. - One-Time Pre-Keys генерируются пакетами по 20. Когда серверный запас исчерпан, клиент загружает свежий пакет.
- Версия протокола сессии: клиент отслеживает константу
PROTOCOL_VERSION(текущее значение3). При обновлении все устаревшие сессии очищаются для принудительного переустановления с новым протоколом.
Хранение приватных ключей
Приватные ключи хранятся локально на устройстве (режим device-local). В состоянии покоя приватные ключи
в localStorage шифруются ключом шифрования хранилища (SEK), который существует только
в sessionStorage (очищается при закрытии вкладки):
Формат: "sek1:" + base64(IV(12) || AES-GCM(SEK, privateKey))
SEK = случайные 32 байта, хранится только в sessionStorage
Для ключей, зашифрованных на сервере, приватный ключ шифруется паролем пользователя:
- Argon2id (основной):
0xA2 || salt(16) || IV(12) || AES-GCM(derived, privKey). Параметры: память 64 МБ, time cost 3, параллелизм 1, длина хеша 32 байта. - PBKDF2 (запасной):
salt(16) || IV(12) || AES-GCM(derived, privKey). 600K итераций SHA-256. Используется, когда Argon2 WASM недоступен. Устаревшие ключи со 100K итераций автоматически мигрируются.
11. Восстановление
Восстановление аккаунта использует мнемоническую фразу BIP39 (12 слов, 128 бит энтропии) как человекочитаемую резервную копию криптографической идентичности.
Генерация мнемоники
phrase = BIP39.generateMnemonic(wordlist=english, entropy=128)
// Генерирует 12 английских слов, напр. "abandon ability able about above absent..."
Вывод ключа восстановления
seed = BIP39.mnemonicToSeed(phrase) // 64 байта
recoveryKey = HKDF-SHA256(
ikm = seed,
salt = "essenger-recovery-v1",
info = "essenger-recovery-v1",
len = 32
)
// recoveryKey: Uint8Array(32)
Формат зашифрованного бэкапа (RP1)
Приватный ключ шифруется ключом восстановления и загружается как резервная копия:
// Шифрование
salt = random(16)
aesKey = HKDF-SHA256(recoveryKey, salt, info="essenger-e2e-privkey-v1", len=32)
iv = random(12)
blob = "RP1" || salt || iv || AES-GCM(aesKey, privateKey, iv)
output = base64(blob)
Поток восстановления
- Пользователь вводит мнемоническую фразу из 12 слов.
- Фраза валидируется по словарю BIP39 (с нормализацией: нижний регистр, удаление нумерации, схлопывание пробелов).
- Ключ восстановления выводится через HKDF.
- Блоб RP1 расшифровывается. Полученный приватный ключ верифицируется по ожидаемому публичному ключу.
- При успехе приватный ключ загружается в память и session storage.
Хранение мнемоники
Мнемоническая фраза опционально кэшируется в sessionStorage на время текущей сессии,
зашифрованная per-session ключом AES-256-GCM, который существует только в памяти. Мнемоника
никогда не записывается в localStorage и не отправляется на сервер.
12. Вопросы безопасности
Защита от повторного воспроизведения (replay)
Double Ratchet поддерживает множество processedSet (веб) / processedMessages (iOS),
в котором записывается DHr_base64:messageNumber каждого успешно расшифрованного сообщения.
Если ID сообщения уже есть в множестве, расшифровка отклоняется с ошибкой replayDetected.
- Веб: множество очищается при превышении 10 000 записей, удаляя записи старше 30 дней.
- iOS: ограниченное FIFO-вытеснение удаляет 1 000 старейших записей при превышении 10 000.
Сравнение за постоянное время
Вся проверка MAC использует сравнение за постоянное время для предотвращения атак по таймингу:
function equal(a, b):
if a.length !== b.length: return false
diff = 0
for i in 0..a.length:
diff |= a[i] ^ b[i]
return diff === 0
Обнуление ключевого материала
Эфемерные приватные ключи, общие секреты и выведенный ключевой материал обнуляются (.fill(0))
немедленно после использования, как правило в блоке finally. Это минимизирует окно,
в течение которого чувствительный материал существует в памяти.
Rate limiting PreKey-сообщений
Ответчик (Боб) ограничивает новые PreKey-сессии до 5 на пользователя за 60 секунд. Это предотвращает атаки исчерпания ресурсов, при которых злоумышленник заваливает пользователя PreKey-сообщениями для расходования одноразовых пре-ключей.
Блокировка сессий
Одновременные операции шифрования/расшифровки на одной рэтчет-сессии сериализуются через
per-session мьютексы (_acquireSessionLock). Это предотвращает гонки состояний,
которые могут повредить состояние рэтчета в асинхронных средах.
Миграция версии протокола
Константа версии протокола (PROTOCOL_VERSION = 3) отслеживается в localStorage.
При увеличении версии все существующие сессии очищаются для принудительного переустановления.
Это гарантирует, что изменения формата AD не вызовут тихих ошибок расшифровки.
Откат состояния
И веб-, и iOS-реализации создают снимок рэтчет-состояния перед мутациями при расшифровке. Если расшифровка AES-GCM не удаётся (несовпадение тега), состояние восстанавливается из снимка. Это предотвращает необратимое повреждение сессии одним повреждённым сообщением.
Известные ограничения
- Нет отрицаемости: HMAC аутентификации отправителя использует статический ECDH-ключ, создавая криптографическое доказательство того, что конкретный отправитель создал сообщение.
- Метаданные группы: словарь
mkв сообщениях group v2 раскрывает состав группы любому наблюдателю, который видит шифротекст (хотя содержимое сообщения зашифровано). - Модель памяти JavaScript:
.fill(0)наUint8Array— это best-effort в рантаймах со сборщиком мусора. JavaScript-движок может сохранять копии чувствительных данных в памяти. - TOFU без PKI: начальный обмен ключами доверяется при первом использовании. Активная MITM-атака в момент первого контакта может подменить ключи. Числа безопасности позволяют ручную верификацию, но не принуждают к ней.
13. Ссылки
- 1M. Marlinspike, T. Perrin. "The X3DH Key Agreement Protocol." Signal Foundation, 2016.
- 2T. Perrin, M. Marlinspike. "The Double Ratchet Algorithm." Signal Foundation, 2016.
- 3A. Langley, M. Hamburg, S. Turner. "Elliptic Curves for Security." RFC 7748, January 2016.
- 4S. Josefsson, I. Liusvaara. "Edwards-Curve Digital Signature Algorithm (EdDSA)." RFC 8032, January 2017.
- 5H. Krawczyk, P. Eronen. "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)." RFC 5869, May 2010.
- 6H. Krawczyk, M. Bellare, R. Canetti. "HMAC: Keyed-Hashing for Message Authentication." RFC 2104, February 1997.
- 7M. Dworkin. "Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM)." NIST SP 800-38D, November 2007.
- 8NIST. "Module-Lattice-Based Key-Encapsulation Mechanism Standard." FIPS 203, August 2024.
- 9A. Biryukov, D. Dinu, D. Khovratovich. "Argon2: the memory-hard function for password hashing and other applications." RFC 9106, September 2021.
- 10K. Moriarty, et al. "PKCS #5: Password-Based Cryptography Specification Version 2.1." RFC 8018, January 2017.
- 11P. Palatinus, et al. "Mnemonic code for generating deterministic keys." BIP-0039, 2013.
- 12P. Schwabe, et al. "CRYSTALS-KYBER Algorithm Specifications and Supporting Documentation." NIST PQC Round 3 submission, 2022.