В криптографических протоколах проверка MAC (Message Authentication Code) является одной из наиболее чувствительных операций с точки зрения утечек по времени выполнения. Любая разница в скорости обработки двух вариантов данных может стать каналом для атаки, позволяющей восстановить секретный ключ или подделать подпись.
MAC в контексте JavaScript-криптографии, в том числе в Stanford JS Crypto Library, чаще всего реализуется через HMAC (например, HMAC-SHA256). Схема выглядит следующим образом:
tag = HMAC(key, message)tag' = HMAC(key, message)tag === tag'Ключевой момент заключается в том, что именно операция сравнения становится потенциальной точкой утечки.
Типичная реализация проверки в JavaScript может выглядеть так:
if (tag1 === tag2) {
return true;
}
return false;
На первый взгляд это безопасно, но в реальности сравнение строк или массивов байтов часто реализуется с ранним выходом при первом несовпадении символов. Это означает:
Атакующий, измеряя миллионы запросов с разными вариантами MAC, может восстановить правильное значение по микроскопическим различиям времени ответа.
Это классическая timing-атака.
Безопасное сравнение MAC должно удовлетворять нескольким условиям:
Фактически требуется константное время выполнения (constant-time comparison).
В Stanford JS Crypto Library работа с бинарными данными строится
вокруг bitArray. MAC обычно представлен как массив
32-битных слов.
SJCL предоставляет операции, которые позволяют избегать небезопасных строковых сравнений. Вместо этого используется побитовое сравнение.
Ключевая идея: сравниваются не строки, а структуры фиксированного типа, и результат накапливается через побитовые операции.
Пример логики сравнения в стиле SJCL:
function safeEqual(a, b) {
if (a.length !== b.length) return false;
var diff = 0;
for (var i = 0; i < a.length; i++) {
diff |= (a[i] ^ b[i]);
}
return diff === 0;
}
Здесь важно:
return внутри циклаДаже если элементы различаются в самом начале, цикл всё равно выполняется до конца.
В SJCL данные MAC обычно представлены через
sjcl.bitArray. Проверка может выглядеть концептуально
так:
var ok = sjcl.bitArray.equal(mac1, mac2);
Такая функция реализует сравнение без ветвлений, зависящих от содержимого данных. Она работает через побитовые операции и аккумулирует различия.
Важно понимать, что даже если функция возвращает boolean, внутренняя реализация должна избегать раннего выхода.
Одна из самых распространённых уязвимостей возникает при следующем подходе:
var ok = sjcl.codec.hex.fromBits(mac1) === sjcl.codec.hex.fromBits(mac2);
Проблема здесь многослойная:
Это создаёт утечки не только по времени, но и по поведению сборщика мусора.
Корректная схема проверки должна выглядеть так:
function verifyMac(key, message, macToCheck) {
var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);
var computed = hmac.encrypt(message);
return sjcl.bitArray.equal(computed, macToCheck);
}
Ключевые свойства этого подхода:
Даже при constant-time сравнении содержимого остаётся ещё один канал утечки — длина данных.
Если проверка выглядит так:
if (a.length !== b.length) return false;
то длина уже может дать атакующему полезную информацию.
В криптографических протоколах часто предполагается, что длина MAC фиксирована (например, всегда 256 бит). В таком случае проверка длины не создаёт утечки, потому что она константна по определению.
Если же длина переменная, требуется нормализация:
Ошибочный вариант часто выглядит так:
if (computedMac.length !== receivedMac.length) {
return false;
}
for (var i = 0; i < computedMac.length; i++) {
if (computedMac[i] !== receivedMac[i]) {
return false;
}
}
return true;
Проблемы:
return при первом несовпаденииЭто делает систему уязвимой даже при использовании сильного HMAC.
Безопасная проверка MAC должна опираться на одно правило: все байты обрабатываются всегда одинаково.
В JavaScript это достигается через:
Важно учитывать особенности JavaScript-движков (V8, SpiderMonkey, JavaScriptCore):
Поэтому криптографический код в SJCL сознательно избегает использования высокоуровневых строковых сравнений.
Обобщённая модель проверки MAC в стиле SJCL выглядит так:
function constantTimeEqual(a, b) {
var len = a.length;
var diff = len ^ b.length;
for (var i = 0; i < len; i++) {
diff |= a[i] ^ b[i];
}
return diff === 0;
}
Особенность этой модели:
Любая MAC-проверка в JavaScript-криптографии должна рассматриваться как потенциальная точка утечки информации. Даже микросекундные различия времени выполнения могут быть измерены при достаточном количестве запросов.
Поэтому библиотека SJCL и подобные ей системы строятся вокруг следующих принципов: