nacl.box: шифрование сообщений между двумя сторонами

nacl.box реализует схему публично-ключевого шифрования с аутентификацией сообщений. В основе лежит комбинация Curve25519 для обмена ключами и XSalsa20-Poly1305 для шифрования и проверки целостности. Модель предполагает, что у каждой стороны есть пара ключей: публичный и приватный, а также используется одноразовый nonce для каждого сообщения.


Базовая модель безопасности

nacl.box обеспечивает одновременно:

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

Шифрование всегда выполняется от отправителя к получателю с использованием публичного ключа получателя и приватного ключа отправителя.


Генерация ключевой пары

Каждый участник коммуникации должен создать собственную пару ключей:

const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');

const keyPair = nacl.box.keyPair();

console.log(keyPair.publicKey);
console.log(keyPair.secretKey);
  • publicKey передаётся другим участникам
  • secretKey хранится строго локально

Размер ключей фиксирован:

  • 32 байта — публичный ключ
  • 32 байта — приватный ключ

Роль nonce и его критическая важность

Nonce (number used once) — это уникальное значение длиной 24 байта, которое обязано быть неповторимым для каждой пары ключей.

const nonce = nacl.randomBytes(nacl.box.nonceLength);

Ключевое правило: повтор nonce с теми же ключами полностью компрометирует безопасность сообщений

Nonce не является секретом, он передаётся вместе с зашифрованным сообщением.


Шифрование сообщения: nacl.box

Основной метод шифрования:

const message = nacl.util.decodeUTF8("Привет");

const box = nacl.box(
  message,
  nonce,
  recipientPublicKey,
  senderSecretKey
);

Параметры:

  • message — Uint8Array
  • nonce — 24-байтовый уникальный вектор
  • recipientPublicKey — публичный ключ получателя
  • senderSecretKey — приватный ключ отправителя

Результат:

  • зашифрованный Uint8Array (включает MAC для проверки целостности)

Дешифрование: nacl.box.open

Получатель использует собственный приватный ключ и публичный ключ отправителя:

const decrypted = nacl.box.open(
  box,
  nonce,
  senderPublicKey,
  recipientSecretKey
);

const text = nacl.util.encodeUTF8(decrypted);

Если:

  • nonce неверный
  • ключи не соответствуют
  • данные повреждены

результат будет null.


Пример полного обмена сообщениями

Участник A

const A = nacl.box.keyPair();

Участник B

const B = nacl.box.keyPair();

A отправляет сообщение B

const nonce = nacl.randomBytes(nacl.box.nonceLength);
const message = nacl.util.decodeUTF8("Сообщение от A к B");

const encrypted = nacl.box(
  message,
  nonce,
  B.publicKey,
  A.secretKey
);

B расшифровывает

const decrypted = nacl.box.open(
  encrypted,
  nonce,
  A.publicKey,
  B.secretKey
);

const result = nacl.util.encodeUTF8(decrypted);

Предварительно вычисленный общий ключ (nacl.box.before)

Для оптимизации повторных сообщений между двумя сторонами используется предварительное вычисление общего ключа.

Создание shared key

const sharedKey = nacl.box.before(
  recipientPublicKey,
  senderSecretKey
);

Этот ключ можно использовать для ускоренного шифрования.


Шифрование через box.after

const encrypted = nacl.box.after(
  message,
  nonce,
  sharedKey
);

Расшифровка через box.open.after

const decrypted = nacl.box.open.after(
  encrypted,
  nonce,
  sharedKey
);

Сравнение box и box.before

Обычный nacl.box

  • используется при разовых сообщениях
  • требует передачи публичного ключа на каждый вызов
  • проще в использовании

nacl.box.before

  • оптимизирован для множества сообщений между одними и теми же сторонами
  • снижает вычислительные затраты
  • требует хранения shared key

Требования к данным

Все входные и выходные данные должны быть Uint8Array.

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

nacl.util.decodeUTF8(str)
nacl.util.encodeUTF8(bytes)

Для бинарных данных часто применяются Base64 или Hex через сторонние библиотеки.


Критические ошибки при использовании

Повтор nonce

Использование одного nonce с одинаковыми ключами приводит к:

  • утечке информации о сообщениях
  • возможности восстановления текста атакующим

Неправильная пара ключей

Если публичный ключ не соответствует приватному — расшифровка невозможна.

Игнорирование проверки результата

const decrypted = nacl.box.open(...);

if (!decrypted) {
  // сообщение повреждено или подделано
}

Внутренние свойства безопасности

nacl.box обеспечивает:

  • authenticated encryption (AEAD-подобная модель)
  • защиту от подмены ciphertext
  • защиту от MITM при корректной передаче публичных ключей

Практическая модель обмена данными

Типичный протокол:

  1. Обмен публичными ключами
  2. Генерация уникального nonce
  3. Шифрование через nacl.box
  4. Передача {nonce + ciphertext}
  5. Дешифрование через nacl.box.open

Особенности реализации TweetNaCl.js

  • минималистичная криптобиблиотека без зависимостей
  • фиксированные размеры ключей и nonce
  • ориентирована на безопасность и простоту
  • отсутствует гибкость алгоритмов (намеренно)

Работа с бинарными сообщениями

nacl.box не ограничен текстом. Он работает с любыми байтами:

  • файлы
  • изображения
  • структурированные данные

Пример:

const fileBytes = new Uint8Array(fileBuffer);

const encrypted = nacl.box(
  fileBytes,
  nonce,
  B.publicKey,
  A.secretKey
);

Использование в реальных протоколах

nacl.box часто применяется как криптографический слой в:

  • мессенджерах
  • peer-to-peer системах
  • защищённых API
  • протоколах обмена ключами поверх WebRTC

Итоговая модель работы

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