Выработка нескольких ключей из одного пароля

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

В экосистеме JavaScript и библиотеки CryptoJS эта задача решается через корректное применение функций KDF (Key Derivation Function) и принципов доменного разделения.


Базовые свойства CryptoJS WordArray

В CryptoJS все бинарные данные представлены в виде WordArray — массива 32-битных слов.

const wordArray = CryptoJS.enc.Hex.parse("0011223344556677");

Ключевые особенности:

  • 1 слово = 4 байта
  • размер задаётся в словах, а не байтах
  • операции concat, clamp, slice позволяют управлять фрагментами ключей

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


Подход 1: единый мастер-ключ с разбиением

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

Генерация большого ключа

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

Особенности подхода

  • высокая производительность (один PBKDF2 вызов)
  • все ключи статистически связаны
  • отсутствие доменного разделения на уровне KDF

Используется в системах, где требуется баланс между скоростью и безопасностью, но не максимальная криптографическая строгость.


Подход 2: независимые ключи через разные соли

Более безопасная модель — генерация каждого ключа отдельно с разными 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
});

Усиленный вариант с комбинированным salt

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)
  • устойчивость к анализу повторного использования материала

Подход 3: реализация HKDF-подобной схемы

PBKDF2 не поддерживает контекстное разделение напрямую, поэтому используется конструкция, аналогичная HKDF.

Структура HKDF

  1. Extract — получение псевдослучайного ключа (PRK)
  2. Expand — генерация нескольких ключей через HMAC

Шаг 1: Extract

const passwordWA = CryptoJS.enc.Utf8.parse(password);
const saltWA = CryptoJS.enc.Utf8.parse("hkdf-salt");

const prk = CryptoJS.HmacSHA256(passwordWA, saltWA);

Шаг 2: Expand с контекстами

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);

Почему HKDF-подход предпочтительнее

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

Подход 4: комбинированный PBKDF2 + расширение

Иногда применяется гибридная схема:

  1. PBKDF2 создаёт master key
  2. HMAC расширяет ключи
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");

Сравнение подходов

Единый ключ с разбиением

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

PBKDF2 с разными salt

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

HKDF-подобная схема

  • криптографически корректная
  • масштабируемая
  • оптимальный баланс

Гибрид

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

Важные нюансы CryptoJS

1. keySize измеряется в 32-битных словах

keySize: 4 // = 128 бит

2. WordArray нельзя трактовать как строку

Любое преобразование требует явного encoder:

CryptoJS.enc.Hex.stringify(wordArray);

3. concat изменяет структуру данных

a.concat(b)

создаёт новый WordArray, не изменяя a физически.


Ошибки, которые часто встречаются

Повторное использование salt

PBKDF2(password, "salt")
PBKDF2(password, "salt") // для разных ключей

Это приводит к корреляции ключей.


Использование одного ключа для шифрования и MAC

Нарушает принцип key separation и открывает возможность атак на целостность.


Недооценка итераций PBKDF2

Малые значения (например, 1000) делают систему уязвимой к перебору.


Практическая схема для приложений

Наиболее устойчивая структура обычно выглядит так:

  • PBKDF2 → master key

  • HKDF → производные ключи:

    • encryption key
    • authentication key
    • IV key
    • session key

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