Кэширование shared key: когда это уместно

В библиотеке TweetNaCl.js механизм шифрования на основе box строится вокруг концепции общего секрета (shared key), получаемого через операцию умножения на эллиптической кривой Curve25519. Этот секрет вычисляется из:

  • приватного ключа отправителя
  • публичного ключа получателя

Функция nacl.box.before(theirPublicKey, mySecretKey) возвращает именно этот shared key, который затем используется в симметричном шифровании.

По сути:

  • операция box = вычисление shared key + симметричное шифрование
  • операция box.before = только вычисление shared key

Проблема повторных вычислений

Каждый вызов nacl.box() включает дорогостоящую операцию вычисления shared key. При интенсивном обмене сообщениями между одними и теми же участниками это становится узким местом:

  • вычисление Curve25519 значительно медленнее симметричного шифрования
  • при частых сообщениях происходит избыточная работа
  • увеличивается нагрузка на CPU

Кэширование shared key

Идея заключается в том, чтобы вычислить shared key один раз и переиспользовать его:

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

Далее вместо nacl.box используется:

nacl.box.after(message, nonce, sharedKey);

И для расшифровки:

nacl.box.open.after(ciphertext, nonce, sharedKey);

Когда кэширование уместно

1. Долгоживущие сессии

Если два участника обмениваются большим количеством сообщений в рамках одной сессии:

  • чат между пользователями
  • WebSocket-соединения
  • P2P-коммуникации

В таких сценариях:

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

2. Высокочастотный обмен данными

Системы, где сообщения отправляются десятки или сотни раз в секунду:

  • онлайн-игры
  • потоковые данные
  • синхронизация состояния

Повторное вычисление shared key становится неоправданной нагрузкой.

3. Ограниченные ресурсы

Среды с ограниченной вычислительной мощностью:

  • мобильные устройства
  • браузеры на слабых устройствах
  • IoT

Кэширование снижает энергопотребление и задержки.

4. Серверные приложения с большим числом соединений

Если сервер обслуживает множество клиентов, но каждый клиент взаимодействует длительное время:

  • уменьшение нагрузки на CPU
  • снижение latency
  • более предсказуемая производительность

Когда кэширование нежелательно

1. Одноразовые сообщения

Если взаимодействие происходит один раз:

  • отправка одного зашифрованного сообщения
  • короткие запросы

Кэширование не даёт выгоды и усложняет код.

2. Частая смена ключей

Если используется ротация ключей:

  • ephemeral keys (одноразовые ключи)
  • протоколы с forward secrecy

В таких случаях shared key быстро устаревает и кэш становится бесполезным или даже опасным.

3. Повышенные требования к безопасности

Хранение shared key увеличивает поверхность атаки:

  • ключ дольше находится в памяти
  • возможны утечки через дампы памяти
  • увеличивается риск компрометации

Особенно критично:

  • в браузере (XSS)
  • в небезопасной среде выполнения

Безопасность кэширования

Ограничение времени жизни

Shared key не должен храниться бесконечно:

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

Изоляция памяти

  • хранение в локальной области видимости
  • избегание глобальных переменных
  • минимизация копирования

Очистка после использования

В JavaScript это сложно гарантировать, но рекомендуется:

sharedKey.fill(0);

после завершения работы.

Структура кэша

В случае нескольких собеседников требуется структура хранения:

const sharedKeyCache = new Map();

Ключом может быть:

  • публичный ключ собеседника (в виде строки)
  • или хэш пары ключей

Пример:

function getSharedKey(theirPublicKey, mySecretKey) {
  const keyId = Buffer.from(theirPublicKey).toString('hex');

  if (!sharedKeyCache.has(keyId)) {
    const sharedKey = nacl.box.before(theirPublicKey, mySecretKey);
    sharedKeyCache.set(keyId, sharedKey);
  }

  return sharedKeyCache.get(keyId);
}

Инвалидация кэша

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

  • удаление при разрыве соединения
  • пересоздание при смене ключей
  • ограничение размера кэша

Пример:

sharedKeyCache.delete(keyId);

Сравнение: box vs box.after

Характеристика nacl.box nacl.box.after
Простота высокая требует подготовки
Производительность ниже выше
Повторное использование нет да
Подходит для разовых операций потоковых сценариев

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

Типичный сценарий:

  1. Установление соединения
  2. Обмен публичными ключами
  3. Вычисление shared key (один раз)
  4. Кэширование
  5. Множественные вызовы box.after
  6. Очистка при завершении

Ошибки при кэшировании

Повторное использование nonce

Даже при кэшировании shared key:

  • nonce должен быть уникальным для каждого сообщения
  • нарушение этого правила ломает безопасность

Хранение в небезопасных местах

  • localStorage
  • sessionStorage
  • логирование

Shared key должен существовать только в оперативной памяти.

Игнорирование ротации ключей

Если ключи обновляются, но кэш не очищается:

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

Итоговая оценка подхода

Кэширование shared key — это оптимизация, оправданная при:

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

Но оно требует дисциплины:

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

В противном случае выгода в производительности может быть нивелирована снижением безопасности.