Проверка целостности данных с помощью HMAC

HMAC (Hash-based Message Authentication Code) используется для проверки целостности и подлинности данных на основе криптографических хеш-функций и секретного ключа. В отличие от обычного хеша, который лишь фиксирует состояние данных, HMAC добавляет фактор секретности, что позволяет не только обнаруживать изменения, но и подтверждать источник сообщения.

В основе HMAC лежит комбинация:

  • криптографической хеш-функции (например, SHA-256)
  • секретного ключа, известного только отправителю и получателю

Любое изменение входных данных или ключа приводит к совершенно другому значению HMAC, что делает подделку практически невозможной без знания ключа.


Реализация HMAC в Crypto-js

Библиотека Crypto-js предоставляет готовые инструменты для работы с HMAC без необходимости вручную реализовывать алгоритм.

Подключение базового набора:

import CryptoJS from "crypto-js";

или в Node.js:

const CryptoJS = require("crypto-js");

Основной метод:

CryptoJS.HmacSHA256(message, secretKey)

Где:

  • message — исходные данные
  • secretKey — секретный ключ
  • HmacSHA256 — алгоритм хеширования

Базовое создание HMAC

Простейший пример формирования кода аутентификации сообщения:

const message = "transfer=100&to=account123";
const secretKey = "super_secret_key";

const hmac = CryptoJS.HmacSHA256(message, secretKey).toString();

console.log(hmac);

Результатом будет строка фиксированной длины, зависящая от входных данных и ключа.

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


Выбор алгоритма хеширования

Crypto-js поддерживает несколько вариантов:

CryptoJS.HmacMD5(message, key)
CryptoJS.HmacSHA1(message, key)
CryptoJS.HmacSHA256(message, key)
CryptoJS.HmacSHA512(message, key)

На практике чаще всего используется SHA-256 и выше, поскольку MD5 и SHA-1 считаются криптографически слабыми.


Кодирование результата

По умолчанию Crypto-js возвращает объект WordArray, который необходимо преобразовать в строку:

const hmacHex = CryptoJS.HmacSHA256(message, secretKey).toString(CryptoJS.enc.Hex);

Другие варианты кодирования:

.toString(CryptoJS.enc.Base64)

Hex удобен для логов и API, Base64 — для компактной передачи данных.


Проверка целостности данных

Основной сценарий использования HMAC — проверка того, что данные не были изменены.

Формирование подписи на стороне отправителя:

const payload = JSON.stringify({
  userId: 42,
  amount: 500
});

const secret = "shared_secret";

const signature = CryptoJS.HmacSHA256(payload, secret).toString();

Проверка на стороне получателя:

const receivedPayload = JSON.stringify({
  userId: 42,
  amount: 500
});

const receivedSignature = "полученная_подпись";

const expectedSignature = CryptoJS.HmacSHA256(receivedPayload, secret).toString();

const isValid = expectedSignature === receivedSignature;

Если данные изменены хотя бы на один символ, подписи не совпадут.


Работа с JSON и нормализация данных

Критический аспект использования HMAC — стабильность входных данных. JSON-строки могут отличаться по порядку ключей, что приведёт к разным HMAC.

Пример проблемы:

{ "a": 1, "b": 2 }
{ "b": 2, "a": 1 }

Хотя объекты логически одинаковы, строки различны.

Решение — нормализация:

function stableStringify(obj) {
  return JSON.stringify(obj, Object.keys(obj).sort());
}

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

const payload = stableStringify(data);
const hmac = CryptoJS.HmacSHA256(payload, secret).toString();

Использование HMAC в API-запросах

Типичный сценарий — подпись HTTP-запросов.

const timestamp = Date.now();

const body = JSON.stringify({ action: "pay", sum: 100 });

const dataToSign = `${timestamp}.${body}`;

const signature = CryptoJS.HmacSHA256(dataToSign, secretKey).toString();

Передача:

fetch("/api/payment", {
  method: "POST",
  headers: {
    "X-Signature": signature,
    "X-Timestamp": timestamp
  },
  body
});

На сервере выполняется идентичное вычисление и сравнение.


Защита от повторной отправки (Replay Attack)

HMAC сам по себе не защищает от повторного использования валидной подписи. Для этого добавляется временная метка:

const allowedDrift = 5 * 60 * 1000; // 5 минут

Проверка:

if (Math.abs(Date.now() - timestamp) > allowedDrift) {
  // отклонить запрос
}

Комбинация HMAC + timestamp значительно повышает устойчивость системы.


Секретный ключ и его хранение

Безопасность HMAC полностью зависит от секретного ключа. При его компрометации защита теряется.

Практики хранения:

  • переменные окружения
  • secret manager (в серверных системах)
  • исключение ключа из клиентского кода

Пример:

const secretKey = process.env.HMAC_SECRET;

Недопустимо:

  • хранение ключа в frontend-коде
  • передача ключа по сети
  • логирование ключа

Частые ошибки при использовании HMAC

1. Использование нестабильных данных

  • разные пробелы
  • различный порядок JSON-ключей
  • разные кодировки

2. Разные алгоритмы на клиенте и сервере

  • SHA256 на одной стороне и SHA1 на другой

3. Неправильное кодирование результата

  • сравнение Hex с Base64 без преобразования

4. Использование слабых ключей

  • короткие строки
  • предсказуемые значения

Производительность вычислений

Crypto-js реализует алгоритмы на JavaScript, что делает его удобным, но не самым быстрым решением для больших объёмов данных.

Особенности:

  • подходит для браузера и небольших серверных задач
  • при высоких нагрузках лучше использовать нативные реализации (Node.js crypto)

Сравнение с нативным Node.js crypto

Аналог HMAC в Node.js:

const crypto = require("crypto");

const hmac = crypto
  .createHmac("sha256", secretKey)
  .update(message)
  .digest("hex");

Crypto-js:

CryptoJS.HmacSHA256(message, secretKey).toString();

Crypto-js удобнее для универсального кода (browser + Node.js), но Node.js crypto предпочтительнее для серверных высоконагруженных систем.


Применение в реальных системах

HMAC активно используется в:

  • API подписи запросов (платёжные системы)
  • webhook верификации
  • защите сессий
  • проверке целостности конфигураций
  • обмене данными между микросервисами

Типовой паттерн — shared secret между двумя доверенными сторонами, где HMAC выступает гарантом неизменности сообщения.


Типизация и структурирование кода

При работе с большими проектами удобно выделять функцию подписи:

function signPayload(payload, secret) {
  const normalized = typeof payload === "string"
    ? payload
    : JSON.stringify(payload);

  return CryptoJS.HmacSHA256(normalized, secret).toString();
}

И функцию проверки:

function verifyPayload(payload, signature, secret) {
  const expected = signPayload(payload, secret);
  return expected === signature;
}

Такой подход уменьшает вероятность ошибок и упрощает сопровождение кода.