При проектировании криптографических схем часто возникает необходимость получить несколько независимых ключей из одного пользовательского пароля: ключ шифрования данных, ключ аутентификации, ключ для IV или дополнительных сервисных операций. Прямое повторное использование одного и того же пароля для разных целей приводит к утечке структуры системы и ослаблению безопасности.
В экосистеме JavaScript и библиотеки CryptoJS эта задача решается через корректное применение функций KDF (Key Derivation Function) и принципов доменного разделения.
В CryptoJS все бинарные данные представлены в виде
WordArray — массива 32-битных слов.
const wordArray = CryptoJS.enc.Hex.parse("0011223344556677");
Ключевые особенности:
concat, clamp, slice
позволяют управлять фрагментами ключейЭто важно при разбиении одного большого деривированного ключа на несколько частей.
Самый прямолинейный способ — сгенерировать один большой ключ и разделить его на части.
const password = "super-password";
const salt = CryptoJS.enc.Utf8.parse("static-salt");
const masterKey = CryptoJS.PBKDF2(password, salt, {
keySize: 16, // 16 words = 512 bits
iterations: 100000,
hasher: CryptoJS.algo.SHA256
});
const encKey = CryptoJS.lib.WordArray.create(masterKey.words.slice(0, 4)); // 128 bit
const macKey = CryptoJS.lib.WordArray.create(masterKey.words.slice(4, 8)); // 128 bit
const ivSeed = CryptoJS.lib.WordArray.create(masterKey.words.slice(8, 12)); // 128 bit
Используется в системах, где требуется баланс между скоростью и безопасностью, но не максимальная криптографическая строгость.
Более безопасная модель — генерация каждого ключа отдельно с разными salt-значениями.
Каждый ключ получает уникальный контекст:
"enc""mac""iv"const password = "super-password";
const encKey = CryptoJS.PBKDF2(password, "enc-salt", {
keySize: 4,
iterations: 100000,
hasher: CryptoJS.algo.SHA256
});
const macKey = CryptoJS.PBKDF2(password, "mac-salt", {
keySize: 4,
iterations: 100000,
hasher: CryptoJS.algo.SHA256
});
const baseSalt = CryptoJS.enc.Utf8.parse("system-v1");
const encKey = CryptoJS.PBKDF2(password, baseSalt.concat(CryptoJS.enc.Utf8.parse("enc")), {
keySize: 4,
iterations: 120000
});
PBKDF2 не поддерживает контекстное разделение напрямую, поэтому используется конструкция, аналогичная HKDF.
const passwordWA = CryptoJS.enc.Utf8.parse(password);
const saltWA = CryptoJS.enc.Utf8.parse("hkdf-salt");
const prk = CryptoJS.HmacSHA256(passwordWA, saltWA);
function hkdfExpand(prk, info, lengthInBytes) {
let result = CryptoJS.lib.WordArray.create();
let previousBlock = CryptoJS.lib.WordArray.create();
let counter = 1;
while (result.sigBytes < lengthInBytes) {
const data = previousBlock
.concat(CryptoJS.enc.Utf8.parse(info))
.concat(CryptoJS.lib.WordArray.create([counter]));
previousBlock = CryptoJS.HmacSHA256(data, prk);
result = result.concat(previousBlock);
counter++;
}
result.sigBytes = lengthInBytes;
return result;
}
const encKey = hkdfExpand(prk, "encryption", 16);
const macKey = hkdfExpand(prk, "authentication", 32);
const ivKey = hkdfExpand(prk, "iv-generation", 16);
Иногда применяется гибридная схема:
const master = CryptoJS.PBKDF2(password, salt, {
keySize: 8,
iterations: 150000
});
function derive(master, label) {
return CryptoJS.HmacSHA256(label, master);
}
const encKey = derive(master, "enc");
const macKey = derive(master, "mac");
keySize: 4 // = 128 бит
Любое преобразование требует явного encoder:
CryptoJS.enc.Hex.stringify(wordArray);
a.concat(b)
создаёт новый WordArray, не изменяя a физически.
PBKDF2(password, "salt")
PBKDF2(password, "salt") // для разных ключей
Это приводит к корреляции ключей.
Нарушает принцип key separation и открывает возможность атак на целостность.
Малые значения (например, 1000) делают систему уязвимой к перебору.
Наиболее устойчивая структура обычно выглядит так:
PBKDF2 → master key
HKDF → производные ключи:
Это обеспечивает изоляцию доменов и предсказуемую масштабируемость криптографической архитектуры.