Защищённый канал с ротацией ключей

Криптографическая библиотека TweetNaCl.js реализует компактный набор проверенных примитивов из NaCl: публично-ключевое шифрование (box), симметричное шифрование (secretbox), подписи и хеширование. При построении защищённого канала поверх этих примитивов ключевой задачей становится не только шифрование сообщений, но и управление жизненным циклом ключей, включая их периодическую ротацию.

Базовая модель защищённого канала

Защищённый канал в контексте TweetNaCl.js обычно строится на основе:

  • асимметричного обмена ключами (nacl.box.keyPair)
  • симметричного шифрования сообщений (nacl.box.after)
  • одноразовых nonce для каждого сообщения
  • общего сессионного ключа, производимого из ключей участников

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

import nacl from "tweetnacl";
import util from "tweetnacl-util";

const alice = nacl.box.keyPair();
const bob = nacl.box.keyPair();

const sharedKeyAlice = nacl.box.before(bob.publicKey, alice.secretKey);
const sharedKeyBob = nacl.box.before(alice.publicKey, bob.secretKey);

sharedKeyAlice и sharedKeyBob совпадают и используются как симметрический ключ.

Шифрование сообщений в канале

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

function encryptMessage(sharedKey, message, nonce) {
  const msgUint8 = util.decodeUTF8(message);
  const box = nacl.box.after(msgUint8, nonce, sharedKey);
  return util.encodeBase64(box);
}

function decryptMessage(sharedKey, boxMessage, nonce) {
  const boxUint8 = util.decodeBase64(boxMessage);
  const msg = nacl.box.open.after(boxUint8, nonce, sharedKey);
  return util.encodeUTF8(msg);
}

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


Проблема долговременного ключа

Использование одного sharedKey на протяжении всей сессии приводит к нескольким рискам:

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

Решение — ротация ключей.


Стратегия ротации ключей

Ротация ключей в TweetNaCl.js не встроена на уровне протокола, поэтому реализуется прикладным уровнем. Основная цель — периодически обновлять sharedKey без разрыва соединения.

Существует несколько подходов:

1. Ротация через новый keypair

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

function rotateKeys() {
  return nacl.box.keyPair();
}

Процесс:

  1. Одна сторона отправляет новый публичный ключ
  2. Вторая вычисляет новый sharedKey
  3. Сессия переключается на новый ключ

Недостаток: требует координации и синхронизации состояния.


2. Ротация через производные ключи (key derivation)

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

function deriveKey(oldKey, counter) {
  const input = util.decodeUTF8(oldKey + ":" + counter);
  return nacl.hash(input).slice(0, 32);
}

Каждая сторона увеличивает счётчик и получает новый симметрический ключ.

Преимущества:

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

Недостатки:

  • нет полноценной forward secrecy
  • при компрометации базового ключа страдает вся цепочка

3. Эпизодическая полная перегенерация (hybrid rotation)

На практике чаще используется комбинация:

  • короткоживущий session key через box.before
  • периодическая пересборка с новым keypair
  • промежуточная ротация через KDF

Реализация защищённого канала с ротацией

Инициализация сессии

class SecureChannel {
  constructor(ownKeyPair, remotePublicKey) {
    this.keyPair = ownKeyPair;
    this.remotePublicKey = remotePublicKey;
    this.sharedKey = nacl.box.before(remotePublicKey, ownKeyPair.secretKey);

    this.sendCounter = 0;
    this.recvCounter = 0;
  }

  createNonce(counter) {
    const nonce = new Uint8Array(24);
    const view = new DataView(nonce.buffer);
    view.setUint32(0, counter);
    return nonce;
  }
}

Отправка сообщения

send(message) {
  const nonce = this.createNonce(this.sendCounter++);
  const box = nacl.box.after(
    util.decodeUTF8(message),
    nonce,
    this.sharedKey
  );

  return {
    nonce: util.encodeBase64(nonce),
    payload: util.encodeBase64(box)
  };
}

Получение сообщения

receive(packet) {
  const nonce = util.decodeBase64(packet.nonce);
  const box = util.decodeBase64(packet.payload);

  const msg = nacl.box.open.after(
    box,
    nonce,
    this.sharedKey
  );

  this.recvCounter++;

  return util.encodeUTF8(msg);
}

Механизм ротации ключей внутри сессии

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

Вариант: временные сессионные ключи

rotateSessionKey() {
  const newKeyPair = nacl.box.keyPair();

  this.sharedKey = nacl.box.before(
    this.remotePublicKey,
    newKeyPair.secretKey
  );

  this.keyPair = newKeyPair;

  this.sendCounter = 0;
  this.recvCounter = 0;
}

При ротации важно:

  • синхронизировать момент переключения
  • передавать сигнал о смене ключа
  • сбрасывать nonce-счётчики

Forward secrecy и ограничения TweetNaCl.js

TweetNaCl.js предоставляет низкоуровневые примитивы, но не реализует полноценный протокол типа Signal. Поэтому:

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

Forward secrecy достигается только при частой генерации новых ключевых пар и уничтожении старых секретных ключей.


Практическая схема обновления ключей

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

  1. Установка initial keypair

  2. Вычисление shared key через box.before

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

  4. Периодическая ротация:

    • либо новый keypair + повторный обмен
    • либо derivation-based ключ
  5. Уничтожение старого состояния


Управление nonce в условиях ротации

Nonce должен быть строго уникальным в рамках конкретного ключа.

При смене ключа:

  • nonce сбрасывается
  • старые nonce становятся недействительными
  • повторное использование nonce недопустимо

Надёжная практика — связывать nonce с:

  • счётчиком сообщений
  • идентификатором сессии
  • версией ключа

Типовые ошибки реализации

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

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

Отсутствие синхронизации ротации

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

Хранение старых ключей

Сохранение предыдущих secretKey увеличивает поверхность атаки и снижает эффект ротации.


Устойчивый шаблон канала

Обобщённая модель выглядит как:

  • ephemeral keypair для каждой сессии
  • derived session key для сообщений
  • регулярная ротация состояния
  • уничтожение устаревших ключей
  • строгий контроль nonce

Такая схема позволяет приблизиться к свойствам современных протоколов защищённой связи даже при использовании минималистичных примитивов TweetNaCl.js.