Производительность при многократном обмене с одним адресатом

При сценариях, где один отправитель многократно взаимодействует с одним и тем же получателем, наивное использование криптографических примитивов приводит к избыточным вычислениям. В библиотеке TweetNaCl.js основная нагрузка приходится на операции с публичными ключами (кривые эллиптической криптографии), особенно при использовании nacl.box.

Каждый вызов nacl.box по умолчанию включает:

  • вычисление общего секрета через скалярное умножение
  • шифрование сообщения с использованием XSalsa20-Poly1305

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


Предвычисление общего ключа

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

const sharedKey = nacl.box.before(theirPublicKey, mySecretKey);

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

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

Аналогично для расшифровки:

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

Преимущества подхода

  • Исключается повторное скалярное умножение
  • Существенное снижение CPU-нагрузки при потоке сообщений
  • Стабильная задержка шифрования

Ограничения

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

Стоимость операций

Сравнение по сложности

Операция Относительная стоимость
nacl.box высокая
nacl.box.before высокая (однократно)
nacl.box.after низкая
nacl.secretbox низкая

Основной выигрыш достигается за счёт переноса тяжёлой операции (before) вне цикла.


Работа с nonce

Даже при использовании предвычисленного ключа, каждый вызов nacl.box.after требует уникального nonce.

Требования к nonce

  • длина 24 байта
  • уникальность для каждой операции
  • отсутствие повторов при одном и том же ключе

Практика генерации

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

При интенсивном обмене может использоваться счётчик:

function incrementNonce(nonce) {
  for (let i = nonce.length - 1; i >= 0; i--) {
    if (++nonce[i] !== 0) break;
  }
}

Такой подход снижает нагрузку на генератор случайных чисел.


Минимизация аллокаций памяти

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

Рекомендации

  • переиспользование буферов (Uint8Array)
  • избегание лишнего копирования данных
  • работа с фиксированными размерами сообщений, если возможно

Пример:

const messageBuffer = new Uint8Array(1024);

Переиспользование буфера вместо создания нового при каждом сообщении уменьшает давление на GC.


Пакетная обработка сообщений

При отправке серии сообщений:

  • возможно объединение небольших сообщений в одно
  • уменьшение количества вызовов криптографических функций

Однако:

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

Использование Web Workers

В браузерной среде криптографические операции могут блокировать основной поток.

Перенос в Worker

  • изоляция вычислений
  • отсутствие лагов интерфейса
  • параллельная обработка

Пример архитектуры:

  • основной поток отправляет данные в Worker
  • Worker выполняет nacl.box.after
  • результат возвращается через postMessage

Сравнение с secretbox

Если обмен происходит в рамках уже установленного защищённого канала, можно отказаться от box и перейти к secretbox:

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

Отличия

Характеристика box secretbox
Тип ключа асимметричный симметричный
Производительность ниже выше
Удобство выше (без обмена ключами) требует ключа

В сценарии «один адресат — много сообщений» после начального обмена ключами предпочтительнее использовать secretbox.


Кэширование ключей

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

  • хранение sharedKey для каждой пары
  • использование структуры вроде Map<publicKey, sharedKey>
const cache = new Map();

function getSharedKey(pubKey) {
  if (!cache.has(pubKey)) {
    cache.set(pubKey, nacl.box.before(pubKey, mySecretKey));
  }
  return cache.get(pubKey);
}

Важные аспекты

  • контроль объёма памяти
  • очистка неиспользуемых ключей
  • защита от утечек

Влияние длины сообщений

Алгоритм XSalsa20 работает как потоковый шифр, поэтому:

  • время шифрования линейно зависит от длины сообщения
  • маленькие сообщения имеют фиксированную накладную стоимость (инициализация)

Практический вывод

  • большое количество коротких сообщений менее эффективно, чем меньшее количество длинных

Оптимизация сериализации

Перед шифрованием данные часто преобразуются:

const messageUint8 = new TextEncoder().encode(message);

И обратно:

const message = new TextDecoder().decode(decrypted);

Узкие места

  • частое создание TextEncoder / TextDecoder
  • лишние преобразования

Оптимизация

const encoder = new TextEncoder();
const decoder = new TextDecoder();

Переиспользование экземпляров снижает накладные расходы.


Влияние среды выполнения

Node.js

  • быстрее за счёт оптимизированного V8
  • отсутствие ограничений браузера

Браузер

  • зависит от реализации Web Crypto fallback
  • может уступать Node.js при интенсивных нагрузках

Ограничения TweetNaCl.js

Библиотека ориентирована на безопасность и компактность, а не на максимальную производительность:

  • отсутствие SIMD-оптимизаций
  • чистый JavaScript без нативных ускорений
  • предсказуемое, но не максимальное быстродействие

При экстремальных нагрузках возможен переход на:

  • WebAssembly-реализации
  • нативные библиотеки

Практическая схема оптимизированного обмена

  1. Генерация ключей сторон
  2. Однократный обмен публичными ключами
  3. Предвычисление sharedKey
  4. Использование nacl.box.after или nacl.secretbox
  5. Контроль уникальности nonce
  6. Переиспользование буферов и кодировщиков

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