HKDF: HMAC-based Key Derivation Function

HKDF (HMAC-based Key Derivation Function) представляет собой стандартный алгоритм для получения криптографически стойких ключей из исходного секретного материала. В контексте Web Crypto API он используется через deriveKey и deriveBits с алгоритмом HKDF, обеспечивая безопасное расширение или разделение ключевого материала для разных целей.

HKDF строится поверх HMAC и включает два этапа:

  • Extract (извлечение энтропии) — преобразование исходного ключевого материала в псевдослучайный ключ фиксированной длины
  • Expand (расширение) — генерация необходимого количества ключевого материала с учётом контекста применения

Базовая структура HKDF в Web Crypto API

В Web Crypto API HKDF задаётся через объект алгоритма:

{
  name: "HKDF",
  hash: "SHA-256",
  salt: Uint8Array,
  info: Uint8Array
}

Параметры алгоритма

hash

Определяет хеш-функцию, используемую внутри HMAC. Поддерживаются:

  • SHA-1 (не рекомендуется)
  • SHA-256
  • SHA-384
  • SHA-512

На практике чаще всего используется SHA-256 как баланс безопасности и производительности.


salt

Случайная или фиксированная соль, используемая на этапе extract.

Ключевые свойства:

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

info

Контекстная строка, которая “привязывает” получаемые ключи к конкретному назначению.

Примеры использования:

  • "encryption"
  • "authentication"
  • "session key"
  • "api v1 key"

Важно: изменение info приводит к полностью другому ключу.


Импорт исходного ключевого материала

HKDF работает с базовым секретом, который импортируется как CryptoKey.

Обычно используется тип raw:

const baseKeyMaterial = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode("initial-secret"),
  "HKDF",
  false,
  ["deriveKey", "deriveBits"]
);

Исходный материал может быть:

  • пароль пользователя (после предварительной обработки)
  • случайный секрет
  • результат ECDH-обмена
  • master key из другого протокола

Генерация ключа через deriveKey

Основной сценарий использования HKDF — получение симметричного ключа.

Пример: получение AES-GCM ключа

const derivedKey = await crypto.subtle.deriveKey(
  {
    name: "HKDF",
    hash: "SHA-256",
    salt: new TextEncoder().encode("unique-salt"),
    info: new TextEncoder().encode("encryption-key")
  },
  baseKeyMaterial,
  {
    name: "AES-GCM",
    length: 256
  },
  false,
  ["encrypt", "decrypt"]
);

Результат — полноценный CryptoKey, пригодный для шифрования.


Генерация битов через deriveBits

Если требуется не ключ, а произвольный байтовый массив:

const derivedBits = await crypto.subtle.deriveBits(
  {
    name: "HKDF",
    hash: "SHA-256",
    salt: new TextEncoder().encode("unique-salt"),
    info: new TextEncoder().encode("session-id")
  },
  baseKeyMaterial,
  256
);

Результат:

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

Внутренний процесс HKDF

Extract этап

Исходный ключевой материал проходит через HMAC:

PRK = HMAC(salt, IKM)

где:

  • IKM — input key material
  • PRK — pseudorandom key

Если salt не задан, используется массив нулей длиной хеша.


Expand этап

Из PRK генерируются блоки:

T(1) = HMAC(PRK, info | 0x01)
T(2) = HMAC(PRK, T(1) | info | 0x02)
...

И так далее до нужной длины.


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

Одно из наиболее распространённых применений — постобработка результата ECDH.

Схема:

  1. Обмен публичными ключами
  2. Получение shared secret
  3. Прогон через HKDF
  4. Получение ключей для шифрования и аутентификации

Пример:

const sharedSecret = await crypto.subtle.deriveBits(
  {
    name: "ECDH",
    public: serverPublicKey
  },
  clientPrivateKey,
  256
);

const baseKey = await crypto.subtle.importKey(
  "raw",
  sharedSecret,
  "HKDF",
  false,
  ["deriveKey"]
);

Разделение ключей по назначению

HKDF позволяет из одного master key получать несколько независимых ключей.

Пример разделения:

const encKey = await crypto.subtle.deriveKey(
  {
    name: "HKDF",
    hash: "SHA-256",
    salt,
    info: new TextEncoder().encode("enc")
  },
  baseKeyMaterial,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

const macKey = await crypto.subtle.deriveKey(
  {
    name: "HKDF",
    hash: "SHA-256",
    salt,
    info: new TextEncoder().encode("mac")
  },
  baseKeyMaterial,
  { name: "HMAC", hash: "SHA-256" },
  false,
  ["sign", "verify"]
);

Ключевое свойство: при одинаковом base key различие только в info полностью изолирует ключи.


Роль salt и info в безопасности

Salt

  • защищает от атак с заранее вычисленными таблицами
  • предотвращает повторное использование PRK в разных системах
  • особенно важен при паролях

Info

  • обеспечивает domain separation
  • предотвращает использование одного ключа в разных контекстах
  • служит “меткой назначения”

Типичная ошибка — использование одинакового info для разных задач.


Практические паттерны использования

1. Сессионные ключи

info: "session-key:" + sessionId

Каждая сессия получает уникальный ключевой материал.


2. Версионирование протокола

info: "api-v2-encryption"

Позволяет менять схему без изменения базового секрета.


3. Многоуровневая деривация

masterKey → HKDF → intermediateKey → HKDF → finalKeys

Используется в сложных протоколах (например, мессенджеры, VPN-подобные схемы).


Ограничения Web Crypto API HKDF

Отсутствие потоковой обработки

HKDF работает только с буферами, нет streaming API.


Невозможность кастомного HMAC

Алгоритм строго фиксирован внутри браузера.


Ограниченный контроль над extract/expand

Разработчик не может вмешаться в промежуточные шаги.


Безопасные практики применения

Использование криптостойкого salt

crypto.getRandomValues(new Uint8Array(16))

Изоляция контекстов через info

Каждый тип ключа должен иметь уникальный namespace:

  • "enc"
  • "auth"
  • "sign"
  • "backup"

Избегание повторного использования base key

HKDF допускает повторное использование, но только при строгом контроле salt и info.


Фиксация параметров протокола

Любое изменение:

  • hash
  • salt
  • info

приводит к несовместимости ключей, поэтому параметры должны версионироваться.


Ошибочные сценарии

Использование пустого info

Приводит к слабому разделению контекста и потенциальному повторному использованию ключей.


Статический salt без необходимости

Уменьшает стойкость при массовых атаках.


Прямое использование пароля без предварительной обработки

HKDF не заменяет PBKDF2 или Argon2 для паролей, если нет дополнительного этапа усиления.


Комбинация с другими примитивами Web Crypto

HKDF часто используется вместе с:

  • ECDH — для получения shared secret
  • AES-GCM — для шифрования данных
  • HMAC — для аутентификации сообщений

Типичный pipeline:

ECDH → HKDF → AES-GCM key + HMAC key

Структура безопасного ключевого конвейера

  1. Генерация/получение исходного секрета
  2. Применение HKDF с уникальным salt
  3. Разделение ключей по назначению через info
  4. Использование специализированных ключей в криптографических операциях

Особенности поведения в браузерах

  • операции выполняются асинхронно через Promise
  • доступ только в secure context (HTTPS)
  • ключи могут быть non-extractable
  • реализация полностью зависит от браузерного движка, но соответствует WebCrypto spec

Типовые ошибки реализации

  • повторное использование info для разных типов ключей
  • отсутствие salt или использование фиксированной строки
  • попытка использовать HKDF как замену KDF для паролей
  • смешивание ключевых доменов (например, encryption и authentication без разделения)

Масштабирование схемы деривации

В сложных системах HKDF используется как центральный элемент иерархии ключей:

Root Key
  ├── HKDF → Device Key
  │         ├── HKDF → Session Key
  │         └── HKDF → Backup Key
  └── HKDF → Application Key
            ├── HKDF → Encryption Key
            └── HKDF → Signing Key

Такой подход позволяет:

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