WebSocket обеспечивает двусторонний канал передачи данных в режиме реального времени, однако сам по себе не гарантирует конфиденциальность содержимого сообщений. Даже при использовании WSS (WebSocket Secure), криптографическая защита заканчивается на уровне транспортного канала. Данные внутри протокола остаются открытыми для серверной логики и промежуточной обработки.
Использование TweetNaCl.js или nacl.js позволяет выстроить дополнительный слой шифрования поверх WebSocket, обеспечивая сквозную защиту сообщений между клиентами.
TweetNaCl.js реализует упрощённую и безопасную криптографию на основе NaCl (Networking and Cryptography library). Основной примитив для обмена сообщениями:
nacl.box — асимметричное шифрованиеnacl.box.keyPair() — генерация ключевой парыnacl.randomBytes() — генерация noncenacl.box.before() — предварительное вычисление общего
ключаОсновная модель:
Перед началом обмена через WebSocket создаётся криптографическая идентичность клиента.
import nacl from "tweetnacl";
import naclUtil from "tweetnacl-util";
const keyPair = nacl.box.keyPair();
const publicKey = keyPair.publicKey;
const secretKey = keyPair.secretKey;
Публичный ключ передаётся второй стороне через WebSocket в открытом виде:
socket.send(JSON.stringify({
type: "init",
publicKey: naclUtil.encodeBase64(publicKey)
}));
После обмена публичными ключами вычисляется shared secret:
const sharedKey = nacl.box.before(
remotePublicKey,
secretKey
);
Этот ключ используется для всех последующих сообщений, что снижает вычислительные затраты по сравнению с полным асимметричным шифрованием каждого пакета.
Каждое сообщение должно быть зашифровано отдельно. Критически важный элемент — nonce, который должен быть уникальным для каждого сообщения.
function encryptMessage(message, sharedKey) {
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const messageUint8 = naclUtil.decodeUTF8(message);
const box = nacl.box.after(
messageUint8,
nonce,
sharedKey
);
return {
nonce: naclUtil.encodeBase64(nonce),
payload: naclUtil.encodeBase64(box)
};
}
Отправка через WebSocket:
const encrypted = encryptMessage("hello", sharedKey);
socket.send(JSON.stringify({
type: "message",
data: encrypted
}));
При получении данных выполняется обратная операция:
function decryptMessage(encrypted, sharedKey) {
const nonce = naclUtil.decodeBase64(encrypted.nonce);
const box = naclUtil.decodeBase64(encrypted.payload);
const messageUint8 = nacl.box.open.after(
box,
nonce,
sharedKey
);
if (!messageUint8) {
throw new Error("Decryption failed");
}
return naclUtil.encodeUTF8(messageUint8);
}
Типичная структура пакета:
{
"type": "message",
"data": {
"nonce": "base64...",
"payload": "base64..."
}
}
Такой подход позволяет отделить транспортный слой от криптографического.
Nonce должен быть строго уникален для каждой пары (ключ, сообщение). Повторное использование nonce с тем же ключом приводит к полной компрометации безопасности.
Практика генерации:
nacl.randomBytes(24)Более устойчивый вариант — комбинация:
const nonce = new Uint8Array(24);
nonce.set(nacl.randomBytes(16));
nonce.set(counterBytes, 16);
WebSocket часто используется для потоковых данных. В таком случае каждое сообщение рассматривается как независимый криптографический блок.
Схема:
function sendChunked(data, sharedKey, socket) {
const chunkSize = 1024;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
const encrypted = encryptMessage(chunk, sharedKey);
socket.send(JSON.stringify({
type: "chunk",
data: encrypted,
index: i / chunkSize
}));
}
}
const buffer = [];
function handleChunk(message) {
const decrypted = decryptMessage(message.data, sharedKey);
buffer[message.index] = decrypted;
}
function assemble() {
return buffer.join("");
}
TweetNaCl обеспечивает аутентифицированное шифрование. Это означает:
Однако дополнительная логика может включать:
const state = {
lastIndex: -1
};
function validateSequence(index) {
if (index <= state.lastIndex) {
return false;
}
state.lastIndex = index;
return true;
}
socket.addEventListener("message", (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "message") {
const text = decryptMessage(msg.data, sharedKey);
}
});
Для повышения безопасности применяется ротация ключей:
function rotateKeys() {
const newPair = nacl.box.keyPair();
socket.send(JSON.stringify({
type: "rotate",
publicKey: naclUtil.encodeBase64(newPair.publicKey)
}));
}
Использование TweetNaCl.js поверх WebSocket накладывает архитектурные особенности:
nacl.box.afternacl.box.open.afterОшибки дешифрования означают:
const result = nacl.box.open.after(box, nonce, sharedKey);
if (!result) {
// пакет игнорируется без восстановления данных
}
Комбинация WebSocket и TweetNaCl.js формирует слой, в котором: