Сквозное шифрование (E2EE) строится на принципе, при котором данные шифруются на устройстве отправителя и расшифровываются только на устройстве получателя. Сервер в такой схеме выполняет роль транспорта и не имеет доступа к содержимому сообщений.
Криптографическая основа в JavaScript часто реализуется через библиотеку TweetNaCl.js — компактную и безопасную реализацию NaCl (Networking and Cryptography library). В контексте браузерных и Node.js приложений она предоставляет минимальный набор primitives, достаточный для построения полноценной E2EE-системы.
Библиотека опирается на ограниченный, но мощный набор функций:
nacl.box — асимметричное шифрование (Curve25519 +
XSalsa20-Poly1305)nacl.secretbox — симметричное шифрование
(XSalsa20-Poly1305)nacl.box.keyPair() — генерация пары ключей
(public/private)nacl.randomBytes(n) — генерация случайных значений
(nonce, ключи)nacl.sign — цифровые подписи (Ed25519)При проектировании E2EE-схемы учитываются следующие предположения:
Каждый участник системы имеет постоянную пару ключей:
import nacl from 'tweetnacl';
nacl.util = require('tweetnacl-util');
const keyPair = nacl.box.keyPair();
const publicKey = keyPair.publicKey;
const privateKey = keyPair.secretKey;
Публичный ключ распространяется через сервер или каталог пользователей, приватный хранится локально.
Для начала защищённого общения требуется обмен публичными ключами.
После этого создаётся общий секрет через
nacl.box.before.
const sharedKey = nacl.box.before(remotePublicKey, localPrivateKey);
sharedKey используется как симметрический ключ для
дальнейших сообщений, что снижает вычислительные затраты по сравнению с
асимметричным шифрованием каждого сообщения.
Nonce (number used once) — обязательный элемент для предотвращения повторного воспроизведения сообщений.
const nonce = nacl.randomBytes(nacl.box.nonceLength);
Особенности:
const message = nacl.util.decodeUTF8('секретное сообщение');
const encrypted = nacl.box(
message,
nonce,
remotePublicKey,
localPrivateKey
);
const decrypted = nacl.box.open(
encrypted,
nonce,
remotePublicKey,
localPrivateKey
);
const text = nacl.util.encodeUTF8(decrypted);
После установления общего ключа часто применяется более лёгкий механизм:
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const key = sharedKey;
const ciphertext = nacl.secretbox(message, nonce, key);
Расшифрование:
const plaintext = nacl.secretbox.open(ciphertext, nonce, key);
Практическая реализация требует упаковки данных в единый формат:
{
senderId: "userA",
recipientId: "userB",
nonce: "...",
payload: "...",
timestamp: 1710000000
}
Часто используется бинарное или base64-кодирование:
Типовая схема установления защищённого канала:
function encryptMessage(text, sharedKey) {
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const messageUint8 = nacl.util.decodeUTF8(text);
const box = nacl.secretbox(messageUint8, nonce, sharedKey);
return {
nonce: nacl.util.encodeBase64(nonce),
payload: nacl.util.encodeBase64(box)
};
}
function decryptMessage(packet, sharedKey) {
const nonce = nacl.util.decodeBase64(packet.nonce);
const box = nacl.util.decodeBase64(packet.payload);
const message = nacl.secretbox.open(box, nonce, sharedKey);
return nacl.util.encodeUTF8(message);
}
В E2EE-системах ключи делятся на несколько категорий:
Рекомендуемая модель хранения:
Базовый nacl.box.before не обеспечивает полноценной
форвардной секретности, так как ключи остаются статичными.
Для повышения устойчивости применяются:
Для предотвращения повторов сообщений используется комбинация:
Пример:
const seenNonces = new Set();
function isReplay(nonce) {
if (seenNonces.has(nonce)) return true;
seenNonces.add(nonce);
return false;
}
TweetNaCl возвращает null при ошибке:
const opened = nacl.secretbox.open(box, nonce, key);
if (!opened) {
// сообщение повреждено или ключ неверный
}
Это важный механизм контроля целостности.
Для высоконагруженных систем учитываются следующие моменты:
Uint8Array вместо строкПрактические системы часто комбинируют подходы:
nacl.box для обмена ключамиnacl.secretbox для массового шифрования сообщенийnacl.sign для верификации идентичности отправителяДополнительный уровень доверия обеспечивается цифровыми подписями:
const keyPair = nacl.sign.keyPair();
const signed = nacl.sign(messageUint8, keyPair.secretKey);
const verified = nacl.sign.open(signed, keyPair.publicKey);
Подпись подтверждает неизменность сообщения и его источник.
Типовая архитектура включает:
При использовании только TweetNaCl.js остаются архитектурные ограничения:
Эти ограничения компенсируются надстроенной архитектурой приложения.