Деривация ключей из пароля: связка с scrypt, argon2, pbkdf2

Пароль не является криптографическим ключом и не может использоваться напрямую в примитивах TweetNaCl.js. Любая функция вроде nacl.secretbox или nacl.box требует ключ фиксированной энтропии (обычно 32 байта), тогда как пароль — это низкоэнтропийная строка переменной длины.

Преобразование пароля в ключ выполняется через функции выработки ключа (KDF — Key Derivation Function). Основные варианты: PBKDF2, scrypt, Argon2. Каждый из них увеличивает стоимость перебора за счёт вычислений, памяти или их комбинации.


Роль KDF при работе с TweetNaCl.js

TweetNaCl.js реализует примитивы:

  • nacl.secretbox — симметричное шифрование (XSalsa20-Poly1305)
  • nacl.box — асимметричное шифрование (Curve25519 + XSalsa20-Poly1305)

Оба требуют ключи фиксированной длины:

  • secretbox: 32 байта
  • box: 32-байтовые ключи (или ключевые пары)

Пароль необходимо преобразовать в такой ключ через KDF:

  • вход: пароль + соль
  • выход: 32 байта (или больше, затем усечение/разделение)

Базовая схема преобразования пароля в ключ

Типовая последовательность:

  1. Пользователь вводит пароль
  2. Генерируется случайная соль
  3. Выполняется KDF (PBKDF2 / scrypt / Argon2)
  4. Получается 32-байтовый ключ
  5. Ключ используется в nacl.secretbox
  6. Соль и параметры сохраняются вместе с шифротекстом

PBKDF2 (WebCrypto API)

PBKDF2 — наиболее распространённый стандарт, поддерживается браузерами через WebCrypto.

async function deriveKeyPBKDF2(password, salt) {
  const enc = new TextEncoder();

  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    enc.encode(password),
    "PBKDF2",
    false,
    ["deriveBits"]
  );

  const derivedBits = await crypto.subtle.deriveBits(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: 200000,
      hash: "SHA-256"
    },
    keyMaterial,
    256
  );

  return new Uint8Array(derivedBits);
}

Использование с TweetNaCl:

import nacl from "tweetnacl";

const key = await deriveKeyPBKDF2(password, salt);

const nonce = nacl.randomBytes(24);

const box = nacl.secretbox(messageUint8, nonce, key);

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

  • Простая реализация
  • Широкая поддержка
  • Уязвим к GPU-ускоренному перебору по сравнению с scrypt/argon2

scrypt (scrypt-js)

scrypt добавляет требование памяти, усложняя атаки на GPU/ASIC.

import { scrypt } from "scrypt-js";

function deriveKeyScrypt(password, salt) {
  return new Promise((resolve, reject) => {
    scrypt(
      password,
      salt,
      16384,
      8,
      1,
      32,
      (err, progress, key) => {
        if (err) reject(err);
        else if (key) resolve(new Uint8Array(key));
      }
    );
  });
}

Параметры:

  • N = 16384 (стоимость CPU + памяти)
  • r = 8 (блоки)
  • p = 1 (параллелизм)
  • 32 байта — выход под TweetNaCl

Использование:

const key = await deriveKeyScrypt(password, salt);

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

Argon2 (argon2-browser / wasm)

Argon2 считается наиболее современным KDF и победителем Password Hashing Competition.

Пример с argon2-browser:

import argon2 from "argon2-browser";

async function deriveKeyArgon2(password, salt) {
  const result = await argon2.hash({
    pass: password,
    salt: salt,
    type: argon2.ArgonType.Argon2id,
    mem: 65536,
    time: 3,
    parallelism: 1,
    hashLen: 32
  });

  return new Uint8Array(result.hash);
}

Свойства Argon2id:

  • устойчивость к side-channel атакам
  • баланс CPU + memory hardness
  • рекомендован для новых систем хранения паролей

Интеграция с nacl.secretbox

TweetNaCl.js не накладывает ограничений на источник ключа, если он:

  • Uint8Array длиной 32 байта

Типовая структура шифрования:

function encrypt(message, password) {
  const salt = nacl.randomBytes(16);
  const nonce = nacl.randomBytes(24);

  const key = await deriveKeyArgon2(password, salt);

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

  return {
    box,
    nonce,
    salt
  };
}

Расшифровка:

async function decrypt(box, nonce, salt, password) {
  const key = await deriveKeyArgon2(password, salt);

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

  return message;
}

Хранение соли и параметров

Для восстановления ключа необходимо сохранять:

  • salt (обязательно)

  • nonce (для secretbox)

  • параметры KDF:

    • iterations (PBKDF2)
    • memory/time cost (scrypt/argon2)
    • version алгоритма

Формат хранения часто сериализуется:

{
  "alg": "argon2id",
  "salt": "...",
  "nonce": "...",
  "params": {
    "mem": 65536,
    "time": 3,
    "parallelism": 1
  },
  "ciphertext": "..."
}

Разделение ключей для box (асимметричное шифрование)

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

Схема:

  1. Генерируется keyPair:
const kp = nacl.box.keyPair();
  1. Приватный ключ шифруется через KDF:
const encKey = await deriveKeyArgon2(password, salt);

const encryptedSecretKey = nacl.secretbox(kp.secretKey, nonce, encKey);
  1. Публичный ключ хранится открыто

Усечение и нормализация ключей

Некоторые KDF возвращают больше данных, чем требуется. TweetNaCl требует ровно 32 байта:

const key32 = derived.slice(0, 32);

Важно избегать:

  • повторного использования одного и того же salt с разными паролями
  • уменьшения сложности KDF ради производительности

Типичные ошибки при связке KDF и TweetNaCl

  • использование UTF-8 пароля как ключа напрямую
  • отсутствие salt
  • фиксированный salt для всех пользователей
  • слишком низкие параметры PBKDF2 (например, < 10 000 итераций)
  • использование nonce повторно с тем же ключом
  • хранение ключа в памяти дольше необходимого

Сравнение KDF для использования с nacl.secretbox

PBKDF2:

  • высокая совместимость
  • слабее против GPU атак
  • подходит для legacy-систем

scrypt:

  • memory-hard
  • хорошая защита от ASIC
  • средняя сложность внедрения

Argon2:

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

Совместимость с архитектурой TweetNaCl.js

TweetNaCl.js остаётся минималистичной криптобиблиотекой и не включает KDF по дизайну. Это означает, что вся безопасность парольной модели полностью зависит от внешнего слоя.

Ключевой принцип:

  • TweetNaCl.js обеспечивает криптографические операции
  • KDF обеспечивает устойчивость пароля к перебору
  • безопасность системы определяется слабейшим звеном KDF-параметров