Генерация ключей, nonce и соли вручную

Библиотека TweetNaCl.js предоставляет минималистичный набор примитивов для асимметричной и симметричной криптографии, основанный на NaCl/libsodium. Основной принцип — отсутствие «магии»: разработчик сам управляет ключами, nonce и вспомогательными значениями, что снижает скрытую сложность, но увеличивает ответственность.

Асинхронность отсутствует и влияние на генерацию

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


Генерация ключевых пар

Ключевая пара для публичной криптографии (box)

Для обмена зашифрованными сообщениями используется nacl.box, основанный на Curve25519.

import nacl from 'tweetnacl';

const keyPair = nacl.box.keyPair();

console.log(keyPair.publicKey);
console.log(keyPair.secretKey);

Структура:

  • publicKey — может передаваться открыто
  • secretKey — строго конфиденциальен

Размеры:

  • publicKey: 32 байта
  • secretKey: 32 байта

Особенность реализации — ключи генерируются через криптографически стойкий генератор случайных чисел, встроенный в nacl.randomBytes.


Подписи (sign)

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

const signKeyPair = nacl.sign.keyPair();

Отличие от box:

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

Генерация случайных данных

Ключевой строительный блок всей криптографии в TweetNaCl.js:

const bytes = nacl.randomBytes(32);

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

  • ключей
  • nonce
  • временных токенов
  • соли (внешние реализации KDF)

Важно:

  • функция использует crypto.getRandomValues в браузере
  • в Node.js — через соответствующий криптографический источник

Нельзя заменять на Math.random() — это ломает криптостойкость полностью.


Генерация nonce

Nonce (number used once) — одноразовое значение, критически важное для nacl.box и nacl.secretbox.

Размер nonce

  • всегда 24 байта
const nonce = nacl.randomBytes(24);

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

Nonce должен:

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

Подходы к генерации nonce

1. Полностью случайный nonce
const nonce = nacl.randomBytes(24);

Плюсы:

  • простота

Минусы:

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

2. Счётчик + случайная часть
const counter = new Uint32Array([messageIndex]);
const randomPart = nacl.randomBytes(20);

const nonce = new Uint8Array(24);
nonce.set(randomPart);
nonce.set(new Uint8Array(counter.buffer), 20);

Плюсы:

  • гарантированная уникальность при контролируемом счётчике

3. Детеминированный nonce (опасный, но иногда применяемый)
// пример: хеш от ключа + индекса

Используется только при строгом контроле протокола.


Ошибки при работе с nonce

Повтор nonce с тем же ключом

Самая критическая ошибка в nacl.secretbox:

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

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

const nonce = new Uint8Array(24); // ❌ всегда нули

Это полностью ломает безопасность.


Соль (salt) и её использование

TweetNaCl.js не предоставляет встроенного KDF (key derivation function), поэтому соль используется только при внешних алгоритмах:

  • PBKDF2 (Web Crypto API)
  • scrypt (через сторонние библиотеки)
  • argon2 (через wasm/Node.js модули)

Генерация соли

const salt = nacl.randomBytes(16);

Типичные размеры:

  • 16 байт (минимально распространённый стандарт)
  • 32 байта (усиленная версия)

Использование соли в связке с внешним KDF

Пример через Web Crypto API:

const salt = crypto.getRandomValues(new Uint8Array(16));

const keyMaterial = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode("password"),
  { name: "PBKDF2" },
  false,
  ["deriveKey"]
);

const key = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt,
    iterations: 100000,
    hash: "SHA-256"
  },
  keyMaterial,
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);

Ручное управление ключами

В системах на TweetNaCl.js часто ключи хранятся и передаются явно:

const secretKey = nacl.randomBytes(32);
const publicKey = nacl.box.keyPair.fromSecretKey(secretKey).publicKey;

Или наоборот:

const keyPair = nacl.box.keyPair();
const extractedPublic = keyPair.publicKey;

Хранение и сериализация

Ключи и nonce обычно сериализуются:

Uint8Array → Base64

const base64Key = Buffer.from(keyPair.secretKey).toString('base64');

Base64 → Uint8Array

const secretKey = Uint8Array.from(Buffer.from(base64Key, 'base64'));

Практические правила генерации

Ключи

  • всегда использовать nacl.box.keyPair() или nacl.sign.keyPair()
  • не пытаться генерировать вручную

Nonce

  • строго 24 байта
  • никогда не переиспользовать
  • желательно включать счётчик

Соль

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

Типовые архитектуры генерации

Клиентская генерация ключей

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

Серверная генерация

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

Гибридная схема

  • клиент генерирует ephemeral keys
  • сервер хранит long-term identity keys

Частые ошибки при ручной генерации

  • использование Math.random() вместо nacl.randomBytes
  • повтор nonce при циклической отправке сообщений
  • хранение secretKey в localStorage без шифрования
  • отсутствие версии ключей при обновлении алгоритмов

Минимальный корректный шаблон генерации

import nacl from 'tweetnacl';

const keyPair = nacl.box.keyPair();

function createNonce() {
  return nacl.randomBytes(24);
}

function encrypt(message, recipientPublicKey, senderSecretKey) {
  const nonce = createNonce();
  const encrypted = nacl.box(
    new TextEncoder().encode(message),
    nonce,
    recipientPublicKey,
    senderSecretKey
  );

  return { nonce, encrypted };
}