При передаче данных через WebSocket отсутствует встроенный механизм аутентификации сообщений на уровне протокола, поэтому защита целостности и подлинности полностью ложится на прикладной уровень. Это делает криптографическую подпись каждого сообщения обязательной частью безопасной архитектуры, особенно в системах с авторизацией пользователей, финансовыми операциями или управлением состоянием сервера.
Stanford JavaScript Crypto Library (SJCL) предоставляет набор примитивов для построения таких механизмов: HMAC, SHA-хэши, симметричное шифрование и генерацию случайных чисел. На практике именно HMAC на основе SHA-256 чаще всего используется для аутентификации сообщений в WebSocket-потоке.
WebSocket обеспечивает постоянное двустороннее соединение между клиентом и сервером, но сам по себе не гарантирует:
Даже при использовании TLS соединения остаётся логическая проблема: сервер не знает, что конкретное сообщение действительно сформировано авторизованным клиентом в текущем контексте сессии.
На прикладном уровне каждое сообщение дополняется криптографическим кодом целостности (MAC — Message Authentication Code). В SJCL для этого применяется HMAC:
(K, m) = H( (K opad) H((K ipad) m) )
где:
Клиент и сервер заранее разделяют секретный ключ, который никогда не передаётся по сети.
SJCL предоставляет простой интерфейс для вычисления HMAC через SHA-256:
const sjcl = require('sjcl');
const secretKey = "shared-secret-key";
// вычисление HMAC для сообщения
function signMessage(message) {
const hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(secretKey), sjcl.hash.sha256);
const hash = hmac.encrypt(message);
return sjcl.codec.base64.fromBits(hash);
}
Функция signMessage возвращает подпись, которая затем
отправляется вместе с сообщением.
Для обеспечения проверяемости структура сообщения обычно расширяется:
{
"payload": {
"action": "updateBalance",
"amount": 150
},
"timestamp": 1710000000,
"signature": "base64-hmac-value"
}
Важно, что подпись вычисляется не только от payload, но и от временной метки:
function buildSignedMessage(payload) {
const message = JSON.stringify(payload);
const timestamp = Date.now();
const dataToSign = message + ":" + timestamp;
const signature = signMessage(dataToSign);
return {
payload,
timestamp,
signature
};
}
Сервер выполняет обратную операцию: пересчитывает HMAC и сравнивает значения.
function verifyMessage(msg) {
const message = JSON.stringify(msg.payload);
const dataToSign = message + ":" + msg.timestamp;
const hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(secretKey), sjcl.hash.sha256);
const expected = sjcl.codec.base64.fromBits(hmac.encrypt(dataToSign));
return expected === msg.signature;
}
Критически важно использовать сравнение с защитой от timing attacks в реальных системах, хотя SJCL напрямую этого не гарантирует.
Одна из ключевых проблем WebSocket-аутентификации — повторное использование валидной подписи.
Решение строится на двух элементах:
function buildSignedMessage(payload) {
const message = JSON.stringify(payload);
const timestamp = Date.now();
const nonce = sjcl.random.randomWords(4, 0).join("");
const dataToSign = message + ":" + timestamp + ":" + nonce;
const signature = signMessage(dataToSign);
return {
payload,
timestamp,
nonce,
signature
};
}
На сервере nonce сохраняется в кеше использованных значений. Повторное появление приводит к отклонению сообщения.
SJCL включает криптографически стойкий генератор случайных чисел:
const nonce = sjcl.random.randomWords(4, 0);
Он зависит от энтропии браузера или Node.js окружения. При недостаточной энтропии система может быть уязвима, поэтому важно инициализировать генератор событийными источниками (mousemove, keyboard, timing events).
Безопасность всей схемы полностью определяется качеством ключа:
SJCL позволяет формировать ключи через:
const key = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0));
TLS защищает канал, но не защищает от логических атак внутри сессии. Подменённое сообщение в рамках валидного соединения остаётся проблемой.
Отсутствие timestamp и nonce позволяет атакующему:
SHA-1 или MD5 в HMAC приводят к криптографической деградации системы. SJCL поддерживает SHA-256 как базовый безопасный вариант.
Типичный цикл обработки выглядит следующим образом:
Включение sessionId в подписываемые данные предотвращает перенос подписи между пользователями:
const dataToSign = sessionId + ":" + message + ":" + timestamp + ":" + nonce;
Сервер отклоняет сообщения старше фиксированного интервала (например, 30 секунд), снижая окно атаки.
Аутентификация часто комбинируется с шифрованием:
const encrypted = sjcl.encrypt(secretKey, JSON.stringify(payload));
Однако важно понимать, что:
В устойчивой реализации SJCL применяется как криптографический слой поверх транспортного:
Такое разделение позволяет масштабировать систему без изменения криптографической логики при росте нагрузки или добавлении новых типов сообщений.