Управление ключами: хранение, ротация, уничтожение

Библиотека TweetNaCl.js реализует криптографические примитивы поверх строгого, минималистичного API. Управление ключами начинается с их генерации, которая в большинстве сценариев опирается на встроенные функции:

import nacl from "tweetnacl";

// Для асимметричного шифрования (box)
const keyPair = nacl.box.keyPair();

// Для цифровой подписи
const signKeyPair = nacl.sign.keyPair();

Каждая ключевая пара представляет собой структуру из двух Uint8Array:

  • publicKey
  • secretKey

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

  • box: 32 байта
  • sign: 64 байта для секретного ключа (включает публичную часть)

Ключи не сериализуются автоматически и не имеют встроенного механизма хранения, что требует явной работы с представлением данных.


Форматы представления ключей

Внутренний формат TweetNaCl — Uint8Array, однако для хранения и передачи чаще используются текстовые представления.

Base64 кодирование

function toBase64(u8) {
  return btoa(String.fromCharCode(...u8));
}

function fromBase64(str) {
  return new Uint8Array(atob(str).split("").map(c => c.charCodeAt(0)));
}

Hex представление

function toHex(u8) {
  return [...u8].map(b => b.toString(16).padStart(2, "0")).join("");
}

Выбор формата влияет на безопасность и совместимость. Base64 чаще используется в веб-контексте, hex — в логах и низкоуровневых системах.


Хранение ключей в клиентских приложениях

Память процесса

Наиболее безопасный с точки зрения браузерного окружения способ — хранение ключей исключительно в оперативной памяти:

let sessionKeyPair = nacl.box.keyPair();

Недостаток — потеря при перезагрузке страницы и отсутствие персистентности.


localStorage и sessionStorage

Хранение в localStorage выглядит простым, но создаёт существенные риски:

localStorage.setItem("secretKey", toBase64(keyPair.secretKey));

Проблемы:

  • доступ через XSS
  • отсутствие шифрования
  • синхронный доступ (блокировка main thread)
  • длительное хранение

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


IndexedDB

Более устойчивый вариант для хранения криптографических материалов:

  • асинхронный доступ
  • структурированные данные
  • возможность хранения бинарных буферов
const record = {
  id: "keypair-1",
  publicKey: keyPair.publicKey,
  secretKey: keyPair.secretKey
};

Даже при использовании IndexedDB сохраняется проблема компрометации при XSS.


Шифрование ключей перед хранением

Практика “key wrapping” используется для защиты секретов:

const encryptedSecretKey = nacl.secretbox(
  keyPair.secretKey,
  nonce,
  masterKey
);

Где:

  • masterKey — производный ключ от пароля (через PBKDF2 / Argon2 вне nacl.js)
  • nonce — уникальное случайное значение

Ротация ключей

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

Причины ротации

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

Версионирование ключей

Типовая структура хранения:

const keyRecord = {
  keyId: "2026-01",
  publicKey,
  secretKey,
  createdAt: Date.now()
};

Каждое сообщение или зашифрованный блок связывается с keyId.


Параллельное хранение ключей

Во время переходного периода используются несколько активных ключей:

  • текущий ключ для новых данных
  • старые ключи для расшифровки исторических данных

Пример логики выбора ключа

function getActiveKey(keyStore) {
  return keyStore.sort((a, b) => b.createdAt - a.createdAt)[0];
}

Этапы ротации

  1. генерация новой пары ключей
  2. публикация нового публичного ключа
  3. сохранение старого ключа в режиме “decrypt-only”
  4. обновление ссылок на активный ключ
  5. постепенное удаление устаревших ключей

Уничтожение ключей

Полное удаление ключей в JavaScript ограничено моделью управления памятью, но существуют практические подходы.


Очистка Uint8Array

function wipe(buffer) {
  buffer.fill(0);
}

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


Обнуление ссылок

keyPair.secretKey = null;
keyPair.publicKey = null;

Удаление ссылок помогает GC, но не гарантирует немедленного освобождения памяти.


Ограничения среды выполнения

В браузере отсутствует:

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

Поэтому уничтожение ключей рассматривается как снижение риска, а не абсолютное удаление.


Изоляция ключей

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

  • генерация только при необходимости
  • хранение в замкнутых областях видимости
  • немедленное обнуление после использования
function encryptMessage(msg, keyPair) {
  const nonce = nacl.randomBytes(24);

  const encrypted = nacl.box(
    msg,
    nonce,
    keyPair.publicKey,
    keyPair.secretKey
  );

  keyPair.secretKey.fill(0);

  return { encrypted, nonce };
}

Эфемерные ключи и forward secrecy

TweetNaCl.js поддерживает сценарии, где ключи используются одноразово.

Эфемерная пара ключей

const ephemeral = nacl.box.keyPair();

Применяется для:

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

Диффи-Хеллман обмен

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

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


Абстракция хранилища ключей

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

class KeyStore {
  constructor() {
    this.keys = [];
  }

  add(keyPair) {
    this.keys.push({
      id: crypto.randomUUID(),
      ...keyPair,
      createdAt: Date.now()
    });
  }

  getLatest() {
    return this.keys[this.keys.length - 1];
  }

  remove(id) {
    this.keys = this.keys.filter(k => k.id !== id);
  }
}

Частые ошибки при управлении ключами

  • хранение секретных ключей в открытом виде в localStorage
  • повторное использование nonce в box и secretbox
  • отсутствие версии ключей при шифровании
  • сохранение ключей без привязки к жизненному циклу сессии
  • игнорирование очистки Uint8Array

Работа с nonce и привязка к ключам

Nonce в TweetNaCl.js должен быть уникальным для каждой операции:

const nonce = nacl.randomBytes(24);

Повторное использование nonce с тем же ключом приводит к компрометации безопасности.


Управление жизненным циклом ключей

Модель жизненного цикла обычно включает состояния:

  • создан
  • активен
  • устаревший
  • отозван
  • уничтожен

Каждое состояние влияет на возможность использования ключа для:

  • шифрования
  • расшифровки
  • подписи
  • верификации

Изоляция криптографических операций

Практика уменьшения утечек через архитектурное разделение:

  • отдельные модули генерации ключей
  • изолированные функции шифрования
  • минимизация длительности существования секретов
function cryptoScope(fn) {
  const keyPair = nacl.box.keyPair();
  try {
    return fn(keyPair);
  } finally {
    keyPair.secretKey.fill(0);
  }
}