HMAC (Hash-based Message Authentication Code) используется для подтверждения того, что сообщение не было изменено и действительно создано обладателем общего секретного ключа. В отличие от обычных хеш-функций, HMAC защищён от подмены входных данных благодаря использованию криптографического ключа.
Основная идея: одинаковые входные данные дают одинаковый HMAC только при наличии одного и того же секретного ключа.
При передаче данных через API, WebSocket или любые открытые каналы возникают типовые угрозы:
HMAC решает задачу проверки:
Библиотека jsrsasign предоставляет криптографические примитивы для работы в JavaScript как в браузере, так и в Node.js. Она включает поддержку:
Для HMAC используется модуль KJUR.crypto.Mac.
npm install jsrsasign
Использование в Node.js:
const jsrsasign = require("jsrsasign");
В браузере (через bundle):
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.8.6/jsrsasign-all-min.js"></script>
Основная операция — генерация MAC (message authentication code):
const { KJUR } = require("jsrsasign");
const secretKey = "super_secret_key";
const message = "user_id=42&role=admin";
const hmac = new KJUR.crypto.Mac({
alg: "HmacSHA256",
pass: secretKey
});
hmac.updateString(message);
const signatureHex = hmac.doFinal();
console.log(signatureHex);
Jsrsasign по умолчанию возвращает значение в HEX. Однако в API чаще используют Base64.
const hex = signatureHex;
const base64 = Buffer.from(hex, "hex").toString("base64");
console.log(base64);
Для браузера:
const base64 = KJUR.crypto.Util.hex2b64(signatureHex);
При работе с API важно сохранять детерминированность строки.
Проблема: JSON не гарантирует порядок ключей.
Решение: формирование канонической строки.
function canonicalString(obj) {
return Object.keys(obj)
.sort()
.map(key => `${key}=${obj[key]}`)
.join("&");
}
const payload = {
user_id: 42,
role: "admin",
timestamp: 1714650000
};
const message = canonicalString(payload);
Далее вычисляется HMAC:
const mac = new KJUR.crypto.Mac({
alg: "HmacSHA256",
pass: secretKey
});
mac.updateString(message);
const signature = mac.doFinal();
Проверка заключается в повторном вычислении подписи и сравнении значений.
function verifyHmac(message, receivedSignatureHex, secretKey) {
const mac = new KJUR.crypto.Mac({
alg: "HmacSHA256",
pass: secretKey
});
mac.updateString(message);
const expected = mac.doFinal();
return expected === receivedSignatureHex;
}
Прямое сравнение строк может быть уязвимо к timing attack.
Более корректный подход — побайтовое сравнение:
function constantTimeEqual(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
Типичный сценарий:
Клиент формирует запрос:
const payload = {
user_id: 42,
action: "update_profile",
timestamp: Date.now()
};
const message = canonicalString(payload);
const mac = new KJUR.crypto.Mac({
alg: "HmacSHA256",
pass: secretKey
});
mac.updateString(message);
const signature = mac.doFinal();
Отправка:
fetch("/api/profile", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Signature": signature
},
body: JSON.stringify(payload)
});
app.post("/api/profile", (req, res) => {
const signature = req.headers["x-signature"];
const message = canonicalString(req.body);
const mac = new KJUR.crypto.Mac({
alg: "HmacSHA256",
pass: secretKey
});
mac.updateString(message);
const expected = mac.doFinal();
if (!constantTimeEqual(expected, signature)) {
return res.status(401).send("Invalid signature");
}
res.send("OK");
});
HMAC не защищает от повторной отправки запроса. Для этого добавляется временная метка:
const payload = {
user_id: 42,
timestamp: Date.now()
};
На сервере:
1. Разный порядок параметров Изменение порядка ключей ломает подпись.
2. Разные кодировки UTF-8 vs ASCII приводит к разным результатам.
mac.updateString(message); // всегда строка, не Buffer
3. Несогласованный формат подписи HEX на одной стороне, Base64 на другой.
4. Использование нестабильного JSON
JSON.stringify(obj) // опасно без сортировки ключей
Jsrsasign позволяет работать не только со строками:
mac.updateHex("deadbeef");
или:
mac.updateString("binary-safe-string");
Но в большинстве API сценариев предпочтительнее работать со строками UTF-8.
Jsrsasign поддерживает:
Пример смены алгоритма:
const mac = new KJUR.crypto.Mac({
alg: "HmacSHA512",
pass: secretKey
});
Выбор зависит от требований безопасности. SHA256 является стандартом для большинства API.
Ключ HMAC — критически важный элемент безопасности:
Типичная промышленная схема включает:
Такая комбинация обеспечивает устойчивость к основным типам атак на целостность данных в прикладных API.