Жизненный цикл ключа: создание, использование, хранение, уничтожение

В криптографической модели TweetNaCl.js ключи являются базовым строительным элементом для всех операций: шифрования, подписи и обмена данными. Библиотека реализует компактную версию NaCl (Networking and Cryptography library), где каждая криптосхема опирается на строго определённые типы ключей.

Генерация ключевой пары (box)

Для асимметричного шифрования используется схема Curve25519 через nacl.box.keyPair().

const nacl = require('tweetnacl');

const keyPair = nacl.box.keyPair();

const publicKey = keyPair.publicKey;
const secretKey = keyPair.secretKey;

Свойства:

  • publicKey — открытая часть, передаётся другим участникам
  • secretKey — приватная часть, должна оставаться строго локальной
  • длина ключей фиксирована (32 байта)

Ключевая пара используется для:

  • шифрования сообщений между двумя сторонами
  • установки общего секретного канала через Diffie–Hellman

Генерация ключей для подписи (signing keys)

Для цифровой подписи используется Ed25519:

const signKeyPair = nacl.sign.keyPair();

const publicKey = signKeyPair.publicKey;
const secretKey = signKeyPair.secretKey;

Особенность модели:

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

Используется для:

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

Симметричный ключ (secretbox)

Для симметричного шифрования применяется nacl.secretbox, где ключ уже не разделяется на пары:

const key = nacl.randomBytes(32);

Используется единый ключ:

  • для шифрования
  • для расшифрования

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

Шифрование и расшифрование (box)

Модель основана на комбинации:

  • собственного приватного ключа
  • публичного ключа получателя
  • nonce (одноразового значения)
const nonce = nacl.randomBytes(24);

const message = new TextEncoder().encode("data");

const encrypted = nacl.box(
  message,
  nonce,
  receiverPublicKey,
  senderSecretKey
);

const decrypted = nacl.box.open(
  encrypted,
  nonce,
  senderPublicKey,
  receiverSecretKey
);

Ключевой принцип: Каждая сторона использует свою секретную часть и публичную часть собеседника.


Симметричное шифрование (secretbox)

const nonce = nacl.randomBytes(24);
const key = nacl.randomBytes(32);

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

const decrypted = nacl.secretbox.open(encrypted, nonce, key);

Особенности:

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

Подпись и проверка

const signed = nacl.sign(message, secretKey);

const verified = nacl.sign.open(signed, publicKey);

Свойства:

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

Хранение ключей

Память JavaScript и её ограничения

JavaScript не предоставляет прямого управления памятью. Это означает:

  • невозможно гарантированно освободить память вручную
  • ключи могут оставаться в heap до сборки мусора
  • копии данных могут появляться при операциях с Buffer/Uint8Array

Хранение в оперативной памяти

Наиболее безопасный вариант — хранение только в RAM:

let secretKey = nacl.box.keyPair().secretKey;

Особенности:

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

Хранение в браузере (localStorage / IndexedDB)

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

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

Пример небезопасного хранения:

localStorage.setItem(
  "secretKey",
  JSON.stringify(Array.from(secretKey))
);

Более устойчивый вариант — использование IndexedDB с дополнительным шифрованием ключа другим паролем пользователя.


Хранение на сервере

В серверных приложениях ключи обычно:

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

Форматы сериализации ключей

TweetNaCl.js использует Uint8Array. При хранении часто применяются преобразования:

function encodeKey(key) {
  return Buffer.from(key).toString("base64");
}

function decodeKey(str) {
  return new Uint8Array(Buffer.from(str, "base64"));
}

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

Проблема очистки памяти в JavaScript

JavaScript не гарантирует физическое обнуление памяти после удаления переменной:

secretKey = null;

Это:

  • удаляет ссылку
  • не очищает фактические байты сразу

Явное затирание массива

Для уменьшения риска утечки используется ручное обнуление:

function wipe(array) {
  for (let i = 0; i < array.length; i++) {
    array[i] = 0;
  }
}

Применение:

wipe(secretKey);
secretKey = null;

Жизненный цикл ключа в памяти

  1. Генерация

    • случайные байты создаются через криптостойкий RNG
  2. Использование

    • участие в операциях шифрования / подписи
  3. Промежуточное хранение

    • временно находится в RAM или структуре данных
  4. Удаление ссылок

    • переменные обнуляются
  5. Попытка перезаписи памяти

    • частичное затирание массива

Ограничения полного уничтожения

В JavaScript невозможно гарантировать:

  • отсутствие копий ключа в памяти
  • отсутствие копий в оптимизированных структурах движка V8
  • отсутствие следов в swap (на уровне ОС)

Практические аспекты управления жизненным циклом

Разделение ролей ключей

Ключи различаются по назначению:

  • ephemeral keys — временные, создаются и уничтожаются быстро
  • persistent keys — используются длительно (например, identity key)
  • session keys — живут в рамках одного соединения

Минимизация времени жизни

Чем меньше ключ существует в памяти, тем ниже риск компрометации. Типичная модель:

  • генерация перед операцией
  • использование
  • немедленное удаление

Изоляция ключевых данных

Ключи не должны смешиваться с:

  • логами
  • сериализованными объектами приложения
  • UI-слоем
  • кешами и промежуточными структурами

Ошибки управления жизненным циклом

Типовые проблемы:

  • хранение ключей в глобальных переменных
  • случайное логирование Uint8Array
  • копирование через spread оператор
  • сохранение в JSON без контроля
// опасное копирование
const copy = [...secretKey];

Поведение garbage collector

GC в V8:

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

Это делает невозможным строгий контроль над физическим расположением ключей.


Итоговая модель жизненного цикла

Криптографический ключ в TweetNaCl.js проходит через последовательность состояний:

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

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