Построение схемы сквозного шифрования (E2EE)

Сквозное шифрование (E2EE) строится на принципе, при котором данные шифруются на устройстве отправителя и расшифровываются только на устройстве получателя. Сервер в такой схеме выполняет роль транспорта и не имеет доступа к содержимому сообщений.

Криптографическая основа в JavaScript часто реализуется через библиотеку TweetNaCl.js — компактную и безопасную реализацию NaCl (Networking and Cryptography library). В контексте браузерных и Node.js приложений она предоставляет минимальный набор primitives, достаточный для построения полноценной E2EE-системы.


Криптографические примитивы TweetNaCl.js

Библиотека опирается на ограниченный, но мощный набор функций:

  • 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);

Особенности:

  • nonce должен быть уникальным для каждой пары (ключ, сообщение)
  • повтор nonce с тем же ключом критически ломает безопасность
  • обычно используется счётчик или случайная генерация с контролем уникальности

Шифрование сообщений через nacl.box

Шифрование

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);

Симметричное шифрование через nacl.secretbox

После установления общего ключа часто применяется более лёгкий механизм:

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);

Структура E2EE-сообщения

Практическая реализация требует упаковки данных в единый формат:

{
  senderId: "userA",
  recipientId: "userB",
  nonce: "...",
  payload: "...",
  timestamp: 1710000000
}

Часто используется бинарное или base64-кодирование:

  • nonce
  • ciphertext
  • метаданные (нешифрованные или частично зашифрованные)

Формирование защищённого канала

Типовая схема установления защищённого канала:

  1. Генерация ключевых пар обоих участников
  2. Обмен публичными ключами через сервер
  3. Вычисление общего секрета
  4. Переход на симметричное шифрование
  5. Использование nonce для каждого сообщения

Протокол передачи сообщений

Отправка

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-системах ключи делятся на несколько категорий:

  • долгосрочные (identity key pair)
  • сессионные (derived shared keys)
  • временные (ephemeral keys)

Рекомендуемая модель хранения:

  • приватные ключи — только в secure storage (IndexedDB, Keychain, Keystore)
  • публичные ключи — серверный каталог
  • shared keys — память с возможной очисткой после сессии

Форвардная секретность

Базовый nacl.box.before не обеспечивает полноценной форвардной секретности, так как ключи остаются статичными.

Для повышения устойчивости применяются:

  • регулярная ротация ключей
  • ephemeral key pairs на каждую сессию
  • протоколы типа Double Ratchet (реализуются поверх NaCl)

Защита от повторной отправки (replay attack)

Для предотвращения повторов сообщений используется комбинация:

  • nonce-контроль
  • временные метки
  • хранение уже обработанных идентификаторов сообщений

Пример:

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 вместо строк
  • минимизация base64-конвертаций
  • кэширование shared key
  • повторное использование nonce-генераторов с контролем уникальности

Гибридная модель E2EE

Практические системы часто комбинируют подходы:

  • 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);

Подпись подтверждает неизменность сообщения и его источник.


Структура полноценной E2EE-системы на TweetNaCl.js

Типовая архитектура включает:

  • модуль управления ключами
  • сервис обмена публичными ключами
  • слой сессий (shared secrets)
  • криптографический слой (box/secretbox)
  • транспортный слой (WebSocket/HTTP)
  • механизм хранения nonce и защиты от повторов

Ограничения модели на NaCl

При использовании только TweetNaCl.js остаются архитектурные ограничения:

  • отсутствие встроенного ratchet-протокола
  • необходимость ручного управления nonce
  • отсутствие протокола группового шифрования
  • необходимость самостоятельной реализации протокольной логики

Эти ограничения компенсируются надстроенной архитектурой приложения.