Double Ratchet строится вокруг двух независимых механизмов обновления ключей: симметрического ратчета (symmetric-key ratchet) и диффи-хеллман ратчета (Diffie–Hellman ratchet). В упрощённой реализации на базе TweetNaCl.js основной акцент смещается на управление цепочками ключей и периодическое обновление общего секрета через ECDH на Curve25519.
TweetNaCl.js предоставляет минимальный набор операций, достаточный для построения Double Ratchet:
nacl.box.keyPair() — генерация пары ключей
Curve25519nacl.scalarMult() — ECDH (обмен Диффи–Хеллмана)nacl.secretbox() — симметричное шифрование
(XSalsa20-Poly1305)nacl.hash() — SHA-512nacl.randomBytes() — генерация случайных значенийКлючевое ограничение: отсутствует встроенный HKDF, поэтому его необходимо реализовать вручную через HMAC на базе SHA-512.
Каждая сторона диалога хранит минимальный набор состояний:
rootKey — корневой ключ (основа для вывода новых
ключей)chainKeySend — цепочка ключей отправкиchainKeyRecv — цепочка ключей полученияDHs — локовая DH-пара (ephemeral key pair)DHr — публичный ключ удалённой стороныmessageIndexSend / messageIndexRecv — счётчики
сообщенийВместо полноценного HKDF используется конструкция на HMAC:
function hmacSha512(key, data) {
return nacl.hash(concat(key, data));
}
Упрощённое разделение ключей:
rootKey, chainKey = KDF_RK(rootKey, DH_output)messageKey, chainKey = KDF_CK(chainKey)Где:
KDF_RK = HMAC(rootKey, DH_output || 0x01)
KDF_CK = HMAC(chainKey, 0x02)
KDF_MK = HMAC(chainKey, 0x03)
Каждое отправляемое сообщение использует новый ключ:
chainKeySend выводится messageKeychainKeySend обновляетсяnacl.secretboxПсевдокод:
function nextSendingKey(state) {
const mk = hmac(state.chainKeySend, new Uint8Array([3]));
state.chainKeySend = hmac(state.chainKeySend, new Uint8Array([2]));
return mk;
}
Шифрование:
function encrypt(state, plaintext) {
const key = nextSendingKey(state);
const nonce = nacl.randomBytes(24);
const ciphertext = nacl.secretbox(plaintext, nonce, key);
return { nonce, ciphertext };
}
При получении нового публичного ключа собеседника происходит полный пересчёт цепочек.
DHsDH_out = scalarMult(DHs.secretKey, DHr.publicKey)
rootKey и создаётся новая цепочка:rootKey, chainKeyRecv = KDF_RK(rootKey, DH_out)
Начальное состояние строится из предварительно обменённых ключей:
Упрощённая схема:
function initializeSession(theirPublicKey, ourKeyPair, sharedSecret) {
const DH = nacl.scalarMult(ourKeyPair.secretKey, theirPublicKey);
const rootKey = hmac(sharedSecret, DH);
const chainKeySend = hmac(rootKey, new Uint8Array([1]));
return {
rootKey,
chainKeySend,
chainKeyRecv: null,
DHs: ourKeyPair,
DHr: theirPublicKey
};
}
При получении сообщения выполняется:
messageKeynacl.secretbox.openfunction decrypt(state, packet) {
if (packet.DH !== state.DHr) {
dhRatchet(state, packet.DH);
}
const mk = hmac(state.chainKeyRecv, new Uint8Array([3]));
state.chainKeyRecv = hmac(state.chainKeyRecv, new Uint8Array([2]));
return nacl.secretbox.open(packet.ciphertext, packet.nonce, mk);
}
function dhRatchet(state, newDHr) {
state.DHr = newDHr;
state.DHs = nacl.box.keyPair();
const dhOut = nacl.scalarMult(state.DHs.secretKey, state.DHr);
const derived = kdfRoot(state.rootKey, dhOut);
state.rootKey = derived.rootKey;
state.chainKeyRecv = derived.chainKey;
}
Упрощённая модель предполагает ограниченное хранение промежуточных
messageKey, если сообщения приходят не по порядку:
Необходимо вручную строить derivation chain через HMAC.
Signal использует ChaCha20-Poly1305, но TweetNaCl применяет XSalsa20-Poly1305.
Метаданные (например DH ключи) приходится сериализовать вручную.
{
DH: Uint8Array(32),
nonce: Uint8Array(24),
ciphertext: Uint8Array
}
Поскольку Curve25519 ключи — это 32 байта, их обычно передают напрямую или через Base64:
function encodeKey(key) {
return Buffer.from(key).toString('base64');
}
function decodeKey(str) {
return Uint8Array.from(Buffer.from(str, 'base64'));
}
Double Ratchet опирается на связку:
Каждое сообщение разрушает предыдущее состояние ключа, обеспечивая forward secrecy и post-compromise security.
DH exchange → rootKey
↓
chainKey (send/recv)
↓
messageKey
↓
secretbox encryption
class RatchetSession {
constructor(state) {
this.rootKey = state.rootKey;
this.chainKeySend = state.chainKeySend;
this.chainKeyRecv = state.chainKeyRecv;
this.DHs = state.DHs;
this.DHr = state.DHr;
}
encrypt(plaintext) {}
decrypt(packet) {}
dhRatchet(newDH) {}
}