Правила безопасного обращения с nonce

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

Nonce не является секретом, но обязан быть уникальным для каждой операции шифрования при одном и том же ключе. Его задача — гарантировать, что одинаковые сообщения, зашифрованные одним ключом, будут давать разные шифротексты. Это защищает от анализа повторяющихся паттернов и критически важно для предотвращения атак на повторное использование данных.

В TweetNaCl.js nonce имеет фиксированный размер — 24 байта. Любое отклонение от требований по длине приводит к некорректной работе или снижению безопасности.


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

Уникальность строго обязательна

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

Nonce не обязан быть случайным, но обязан быть неповторяющимся

Случайная генерация допустима, но не гарантирует отсутствие коллизий. Поэтому в высоконагруженных системах предпочтительнее детерминированные схемы: счётчики, комбинации префиксов и инкрементов, либо распределённые генераторы с гарантией уникальности.

Запрещено переиспользование nonce с тем же ключом

Даже при шифровании разных сообщений повтор одного nonce делает возможным проведение атак на основе XOR-анализа шифротекстов. В конструкции stream cipher это приводит к восстановлению информации о сообщениях.


Генерация nonce в JavaScript

В TweetNaCl.js nonce обычно создаётся через криптографически стойкий генератор случайных чисел:

import nacl from 'tweetnacl';
nacl.randomBytes(24);

Однако использование случайных значений допустимо только при наличии уверенности в отсутствии повторов. В распределённых системах предпочтительнее комбинированные подходы:

  • префикс идентификатора узла
  • временная метка
  • локальный счётчик

Пример детерминированной схемы:

function createNonce(nodeId, counter) {
  const nonce = new Uint8Array(24);

  nonce.set(nodeId.slice(0, 8), 0);
  const view = new DataView(nonce.buffer);
  view.setBigUint64(16, BigInt(counter));

  return nonce;
}

Такая схема обеспечивает уникальность при условии корректного управления состоянием счётчика.


Ошибки, приводящие к компрометации

Использование фиксированного nonce

const nonce = new Uint8Array(24);

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

Повтор nonce при ретраях отправки

Повторная отправка сообщения с тем же nonce и ключом создаёт возможность восстановления исходного текста через анализ разницы шифротекстов.

Глобальный случайный nonce без контроля состояния

При высокой частоте шифрования возможны коллизии, особенно при длительной работе системы.


Управление уникальностью в распределённых системах

В системах с несколькими узлами проблема nonce становится сложнее. Основная цель — гарантировать глобальную уникальность без централизованного координатора.

Используются следующие стратегии:

Разделение пространства nonce

Каждый узел получает свой диапазон значений или уникальный префикс идентификатора.

Комбинация времени и счётчика

function generateNonce() {
  const nonce = new Uint8Array(24);
  const timestamp = Date.now();
  const counter = localCounter++;

  const view = new DataView(nonce.buffer);
  view.setBigUint64(0, BigInt(timestamp));
  view.setBigUint64(16, BigInt(counter));

  return nonce;
}

Такая схема требует строгого контроля часов и атомарности счётчика.


Свойства, которые нельзя нарушать

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

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

В системах с долгоживущими ключами важно сохранять состояние счётчика или последнего использованного значения. Потеря состояния приводит к риску повторов после перезапуска приложения.

Часто применяются:

  • запись в локальное хранилище
  • хранение в базе данных
  • атомарные инкременты в Redis или аналогичных системах

Взаимодействие nonce с nacl.box

В конструкции nacl.box(message, nonce, publicKey, secretKey) nonce становится частью входных данных для функции, формирующей ключевой поток. При одинаковом ключе изменение nonce полностью меняет результат шифрования.

Это свойство делает nonce эквивалентом «вектора инициализации», но с более строгим требованием уникальности.


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

Повтор nonce приводит к генерации одинакового keystream в stream cipher. Это позволяет атакующему:

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

В некоторых сценариях компрометация одного nonce приводит к раскрытию всех сообщений, защищённых одним ключом.


Ограничения генераторов случайных чисел

Даже криптографически стойкие генераторы не гарантируют отсутствие повторов при масштабировании системы. Ошибка проектирования обычно связана не с качеством случайности, а с отсутствием контроля состояния.

По этой причине nonce не рассматривается как случайная величина, а как строго управляемый идентификатор операции шифрования.


Детерминированные и недетерминированные подходы

Недетерминированные подходы:

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

Детерминированные подходы:

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

Основной принцип безопасного проектирования nonce

Nonce рассматривается как ресурс, который исчерпывается в пределах пары «ключ + система генерации». Любая архитектура должна исходить из того, что повтор невозможен на уровне логики системы, а не проверяется постфактум.