Деривация нескольких криптографических ключей из одного пароля в JavaScript-библиотеке SJCL строится вокруг идеи преобразования низкоэнтропийного пароля в набор независимых ключевых материалов с помощью стойких функций вывода ключей. Основная цель — исключить прямое использование пароля в криптографических операциях и разделить его на отдельные ключи для шифрования, аутентификации и вспомогательных задач.
SJCL оперирует бинарными данными в формате bitArray. Это не строка и не массив байтов в классическом смысле, а специализированная структура, оптимизированная для побитовых операций. Любая деривация ключа в конечном итоге возвращает bitArray фиксированной длины, пригодный для AES, HMAC или других криптопримитивов.
Пароль в чистом виде сначала преобразуется в битовое представление,
после чего проходит через функцию растяжения ключа (key stretching).
Основная используемая функция — PBKDF2, реализованная в
sjcl.misc.pbkdf2.
PBKDF2 в SJCL используется как основной инструмент увеличения энтропийной стойкости пароля. Его задача — сделать перебор пароля вычислительно дорогим за счёт многократного применения HMAC.
(P, S, c, dkLen) = (P, S)^{c}
Где:
В SJCL вызов выглядит следующим образом:
const password = "secret-password";
const salt = sjcl.random.randomWords(8, 0);
const derivedBits = sjcl.misc.pbkdf2(password, salt, 10000, 256);
Полученный результат — 256-битный ключ, пригодный для AES-256 или дальнейшего разделения на несколько ключей.
Соль в PBKDF2 играет роль уникализатора ключевого пространства. При одинаковом пароле разные соли приводят к разным ключам. В контексте генерации нескольких ключей из одного пароля соль становится инструментом разделения ключевого материала на независимые ветви.
Типовой подход в SJCL — фиксировать базовый пароль и использовать разные соли для разных целей:
Пример:
const encSalt = sjcl.codec.hex.toBits("a1b2c3d4e5f6");
const macSalt = sjcl.codec.hex.toBits("0f1e2d3c4b5a");
const encKey = sjcl.misc.pbkdf2(password, encSalt, 12000, 256);
const macKey = sjcl.misc.pbkdf2(password, macSalt, 12000, 256);
Такой подход обеспечивает криптографическую изоляцию ключей даже при использовании одного исходного пароля.
Альтернативный подход — генерация одного большого ключа и его последующее разбиение на несколько частей. Это часто используется в протоколах, где важно минимизировать количество вызовов PBKDF2.
const masterKey = sjcl.misc.pbkdf2(password, salt, 15000, 512);
const encKey = masterKey.slice(0, 8); // 256 бит
const macKey = masterKey.slice(8, 16); // 256 бит
SJCL работает с массивами 32-битных слов, поэтому slicing происходит по словам, а не байтам. Это важно учитывать при проектировании схемы деривации.
Такой метод быстрее, но требует аккуратного контроля границ разделения, чтобы исключить пересечения ключевого материала.
Хотя SJCL не предоставляет полноценную стандартизированную реализацию HKDF в виде отдельного API, его можно эмулировать через комбинацию HMAC и bitArray операций.
В библиотеке присутствуют примитивы HMAC через
sjcl.misc.hmac, которые позволяют строить конструкцию
извлечения и расширения ключа.
Схема HKDF включает два этапа:
PRK = (, )
OKM = (PRK, | 0) | (PRK, | 1) |
Реализация на SJCL:
const hmac = new sjcl.misc.hmac(salt, sjcl.hash.sha256);
const prk = hmac.encrypt(password);
const info1 = sjcl.codec.utf8String.toBits("encryption");
const info2 = sjcl.codec.utf8String.toBits("authentication");
const hmac1 = new sjcl.misc.hmac(prk, sjcl.hash.sha256);
const encKey = hmac1.encrypt(info1);
const hmac2 = new sjcl.misc.hmac(prk, sjcl.hash.sha256);
const macKey = hmac2.encrypt(info2);
Такой подход обеспечивает логическое разделение ключей без повторного применения PBKDF2, что ускоряет генерацию при сохранении криптостойкости.
В типичных сценариях SJCL применяется полный pipeline:
Пример полной схемы:
const password = "user-password";
const masterSalt = sjcl.random.randomWords(8);
const masterKey = sjcl.misc.pbkdf2(password, masterSalt, 20000, 512);
const encKey = masterKey.slice(0, 8);
const authKey = masterKey.slice(8, 16);
const ivSeed = masterKey.slice(16, 20);
IV часто не хранится напрямую как ключ, а выводится из отдельного сегмента мастер-ключа через хеширование или HMAC:
const ivHmac = new sjcl.misc.hmac(ivSeed, sjcl.hash.sha256);
const iv = ivHmac.encrypt("iv-generation");
Критический аспект деривации нескольких ключей из одного пароля — предотвращение корреляции между ключами. Ошибка проектирования может привести к тому, что компрометация одного ключа частично раскрывает другой.
SJCL не навязывает строгую схему разделения, поэтому ответственность ложится на разработчика:
Domain separation достигается через добавление контекстных строк:
const encInfo = sjcl.codec.utf8String.toBits("enc");
const macInfo = sjcl.codec.utf8String.toBits("mac");
Количество итераций PBKDF2 напрямую влияет на устойчивость к brute force. В SJCL нет фиксированного значения, и выбор зависит от целевой среды.
Рост числа итераций линейно увеличивает время вычисления:
T c
где T — время вычисления, c — количество итераций PBKDF2.
На практике наиболее частые проблемы связаны с неправильным управлением состоянием ключевого материала:
SJCL предоставляет гибкие примитивы, но не защищает от архитектурных ошибок, поэтому схема деривации должна быть явно определена на уровне приложения.
В реальных криптосистемах на базе SJCL часто используется трёхуровневая модель:
Каждый уровень уменьшает прямую зависимость от пароля и увеличивает устойчивость системы к компрометации отдельных компонентов.
Комбинация PBKDF2 и HKDF-подобных конструкций позволяет одновременно обеспечить: