X3DH (Extended Triple Diffie-Hellman) строится вокруг идеи установления общего секретного ключа между двумя сторонами, которые могут никогда не встречаться одновременно, при этом обеспечивая аутентификацию и защиту от атак типа man-in-the-middle. В экосистеме JavaScript криптографии, где используется TweetNaCl.js или его более низкоуровневый аналог nacl.js, реализация этой схемы опирается на операции над кривой Curve25519, а также на корректное управление набором предварительно созданных ключей (prekeys).
Библиотека TweetNaCl.js предоставляет минималистичный набор примитивов, необходимых для построения протоколов обмена ключами и шифрования:
nacl.box.keyPair() — генерация ключевой пары Curve25519
(X25519 для обмена ключами)nacl.scalarMult() — выполнение Diffie-Hellman на
Curve25519nacl.sign.keyPair() — генерация ключей Ed25519 для
цифровой подписиnacl.hash() — криптографическое хэширование
(SHA-512-эквивалент в реализации NaCl)nacl.secretbox() — симметричное шифрование после
установления ключаВажно понимать, что X3DH не реализован в библиотеке напрямую — он строится поверх этих примитивов.
Протокол X3DH предполагает участие двух сторон:
Bob заранее публикует набор публичных ключей:
Alice использует эти данные для вычисления общего секрета без необходимости синхронного взаимодействия.
У Bob формируются следующие ключевые элементы:
Identity Key (IK_B) Постоянная ключевая пара Ed25519/X25519, идентифицирующая пользователя.
Signed PreKey (SPK_B) Временный ключ Curve25519, подписанный приватным Identity Key для подтверждения подлинности.
One-Time PreKeys (OPK_B) Дополнительные одноразовые ключи для повышения безопасности первого соединения.
Подпись SPK_B выполняется через Ed25519:
const signed = nacl.sign.detached(spkPublicKey, identitySecretKey);
Identity Key и PreKey создаются через:
const ik = nacl.sign.keyPair(); // Ed25519
const spk = nacl.box.keyPair(); // Curve25519
В реальных реализациях identity key часто используется в двух пространствах: для подписи и для Diffie-Hellman (через преобразование Ed25519 → X25519).
Alice формирует собственную пару ключей:
const aliceIK = nacl.box.keyPair();
const aliceEK = nacl.box.keyPair(); // ephemeral key
Эфемерный ключ (EK) критически важен: он обеспечивает forward secrecy, поскольку используется только в рамках одной сессии.
Alice получает от сервера:
Также Alice проверяет подпись SPK_B:
const valid = nacl.sign.detached.verify(
spkPublicKey,
spkSignature,
bobIdentityPublicKey
);
Если проверка не проходит, соединение должно быть отклонено.
X3DH использует несколько DH-операций:
В nacl.js это реализуется через:
const dh1 = nacl.scalarMult(aliceIK.secretKey, spkPublicKey);
const dh2 = nacl.scalarMult(aliceEK.secretKey, bobIKPublicKey);
const dh3 = nacl.scalarMult(aliceEK.secretKey, spkPublicKey);
Каждый результат представляет собой 32-байтовый shared secret.
Все DH-значения конкатенируются:
SK_input = DH1 || DH2 || DH3 || (DH4 если есть)
Далее применяется KDF (обычно HKDF-SHA256, реализуемый отдельно, так как TweetNaCl не включает полноценный HKDF):
const masterSecret = hkdf(
concat(dh1, dh2, dh3),
salt,
info,
32
);
Результатом является корневой ключ сессии.
TweetNaCl.js не предоставляет HKDF напрямую, поэтому используется комбинация:
nacl.hash() как базовый строительный блокКлючевой момент: raw output DH нельзя использовать напрямую как ключ шифрования.
Аутентификация достигается за счёт:
Таким образом, даже если атакующий подменит SPK_B, он не сможет корректно подписать его без приватного IK_B.
X3DH используется только для начального установления секретов. После этого включается Double Ratchet Algorithm:
Начальный masterSecret становится root key для ratchet:
rootKey = KDF(masterSecret, "root");
После выполнения X3DH формируются:
Инициализация:
session = {
rootKey: masterSecret,
sendChain: null,
recvChain: null,
dhPair: aliceEK
};
Работа с TweetNaCl.js накладывает ограничения:
Типичная ошибка — смешивание строк и бинарных данных:
// неправильно
const bad = "key" + dh1;
// правильно
const good = concatUint8Arrays(dh1, dh2);
One-Time PreKeys используются только один раз. После использования сервер обязан их удалить.
Если OPK отсутствует, протокол продолжает работу, но снижается уровень forward secrecy.
Эфемерный ключ Alice (EK_A) должен:
Любая повторная генерация EK_A с тем же значением нарушает модель безопасности X3DH.
Типовые проблемы при реализации:
nacl.randomBytesКорректная генерация случайных данных:
const random = nacl.randomBytes(32);
TweetNaCl.js разделяет:
Identity Key часто требуется в обеих формах. В низкоуровневых реализациях используется преобразование ключей через алгоритм Montgomery ladder representation.
После X3DH дальнейшая коммуникация происходит через:
ciphertext = nacl.secretbox(message, nonce, symmetricKey);
Где symmetricKey выводится из chain key Double Ratchet.
Nonce должен быть уникальным:
nacl.randomBytes(24);
X3DH не требует централизованного доверенного канала во время соединения, но предполагает доверие к:
Модель безопасности строится на предположении, что приватные ключи никогда не покидают устройство.
TweetNaCl.js оптимизирован под WebAssembly-отсутствие и работает в чистом JS:
В реальных системах поверх nacl.js обычно строится слой:
Каждый компонент строго отделяет криптографию от логики передачи данных.
X3DH защищает от:
Не защищает от:
X3DH лежит в основе современных end-to-end систем, где TweetNaCl.js часто используется как лёгкая реализация криптографии в браузере или Node.js окружении, заменяя более тяжёлые библиотеки.