Пароль не является криптографическим ключом и не может использоваться
напрямую в примитивах TweetNaCl.js. Любая функция вроде
nacl.secretbox или nacl.box требует ключ
фиксированной энтропии (обычно 32 байта), тогда как пароль — это
низкоэнтропийная строка переменной длины.
Преобразование пароля в ключ выполняется через функции выработки ключа (KDF — Key Derivation Function). Основные варианты: PBKDF2, scrypt, Argon2. Каждый из них увеличивает стоимость перебора за счёт вычислений, памяти или их комбинации.
TweetNaCl.js реализует примитивы:
nacl.secretbox — симметричное шифрование
(XSalsa20-Poly1305)nacl.box — асимметричное шифрование (Curve25519 +
XSalsa20-Poly1305)Оба требуют ключи фиксированной длины:
Пароль необходимо преобразовать в такой ключ через KDF:
Типовая последовательность:
nacl.secretboxPBKDF2 — наиболее распространённый стандарт, поддерживается браузерами через 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:
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));
}
);
});
}
Параметры:
Использование:
const key = await deriveKeyScrypt(password, salt);
const encrypted = nacl.secretbox(data, nonce, key);
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:
TweetNaCl.js не накладывает ограничений на источник ключа, если он:
Типовая структура шифрования:
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:
Формат хранения часто сериализуется:
{
"alg": "argon2id",
"salt": "...",
"nonce": "...",
"params": {
"mem": 65536,
"time": 3,
"parallelism": 1
},
"ciphertext": "..."
}
При использовании nacl.box пароль обычно не используется
напрямую для генерации ключевой пары, но может применяться для защиты
приватного ключа.
Схема:
const kp = nacl.box.keyPair();
const encKey = await deriveKeyArgon2(password, salt);
const encryptedSecretKey = nacl.secretbox(kp.secretKey, nonce, encKey);
Некоторые KDF возвращают больше данных, чем требуется. TweetNaCl требует ровно 32 байта:
const key32 = derived.slice(0, 32);
Важно избегать:
PBKDF2:
scrypt:
Argon2:
TweetNaCl.js остаётся минималистичной криптобиблиотекой и не включает KDF по дизайну. Это означает, что вся безопасность парольной модели полностью зависит от внешнего слоя.
Ключевой принцип: