Уязвимости атаки по времени при сравнении хешей

При сравнении хеш-значений в криптографических приложениях критическим фактором становится не только корректность алгоритма, но и способ выполнения самого сравнения. Обычные операции сравнения строк в JavaScript могут приводить к утечке информации через время выполнения, что делает возможными атаки по времени (timing attacks).

Атака по времени основана на измерении различий во времени выполнения операций, зависящих от входных данных. При сравнении двух строк посимвольно стандартные реализации часто завершают проверку при первом несовпадении. Это означает, что чем раньше обнаружено различие, тем быстрее завершается функция.

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

Особенности сравнения в JavaScript

В JavaScript операция строгого равенства для строк:

a === b

не гарантирует одинаковое время выполнения для всех входных значений. Внутренние реализации движков (V8, SpiderMonkey, JavaScriptCore) могут выполнять оптимизации, при которых сравнение прекращается при первом несовпадении символов.

Дополнительно использование библиотек вроде CryptoJS усиливает проблему, поскольку результат хеширования обычно представляется в виде строки (hex или Base64):

const hash = CryptoJS.SHA256("password").toString();

Сравнение таких строк через === или == не защищено от утечки по времени.

Уязвимость при использовании CryptoJS

CryptoJS не предоставляет встроенной функции для константного сравнения хешей. Типичный небезопасный паттерн выглядит следующим образом:

const hashA = CryptoJS.SHA256(inputA).toString();
const hashB = CryptoJS.SHA256(inputB).toString();

if (hashA === hashB) {
    // совпадение
}

Проблема заключается в том, что сравнение строк может завершаться на первом несовпадающем символе, создавая различимую временную характеристику.

Механизм утечки информации

Если рассматривать сравнение двух строк длиной n, то при несовпадении на позиции k время выполнения приблизительно пропорционально k. Это означает, что:

  • совпадение первых 10 символов даёт более длительное выполнение, чем совпадение первых 5
  • полностью совпадающие строки дают максимальное время выполнения

Повторяя запросы и анализируя распределение времени, возможно восстановление хеша по частям.

Константное сравнение как защита

Основной способ устранения уязвимости — использование константного по времени сравнения (constant-time comparison). Его идея заключается в том, что время выполнения не зависит от содержимого строк.

Базовая реализация:

function constantTimeCompare(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;
}

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

Использование Node.js crypto как альтернативы

В среде Node.js предусмотрена встроенная защита:

const crypto = require('crypto');

crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));

Эта функция гарантирует константное время сравнения буферов одинаковой длины. Однако CryptoJS работает в пользовательском пространстве и не интегрирован с данным механизмом.

Ошибки при попытке защиты

Часто применяются псевдозащитные подходы, которые не устраняют проблему:

// небезопасно
return a.localeCompare(b) === 0;

или

// небезопасно
JSON.stringify(a) === JSON.stringify(b);

Обе операции могут иметь раннее завершение и различимое время выполнения.

Хеширование и формат представления

CryptoJS возвращает хеш в виде строки, что увеличивает поверхность атаки. Альтернативный подход — использование бинарных представлений:

const hash = CryptoJS.SHA256("data");
const words = hash.words;

Работа с бинарными массивами снижает накладные расходы на преобразования строк, но не устраняет проблему сравнения без константного времени.

Дополнительные факторы утечки

На практическую реализуемость timing attack влияют:

  • точность измерения времени (высокая в Node.js, ниже в браузерах с шумом)
  • кэширование процессора
  • оптимизации JIT-компилятора
  • сетевые задержки при удалённых вызовах

Тем не менее даже с шумом статистический анализ позволяет восстановление данных при достаточном числе запросов.

Применение HMAC вместо прямого сравнения

Использование HMAC снижает риск подмены данных, но не устраняет необходимость безопасного сравнения результата:

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

Даже в этом случае сравнение двух HMAC-значений требует константного подхода.

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

Комбинированный подход в приложениях с CryptoJS обычно включает:

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

Особое внимание требуется при сравнении токенов доступа, сессионных идентификаторов и подписей запросов.

Особенности браузерной среды

В браузере отсутствует нативный аналог timingSafeEqual, поэтому реализация защиты полностью ложится на JavaScript-код. При этом шум планировщика задач и событийного цикла может частично маскировать различия, но не устраняет саму уязвимость.

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

Итоговая характеристика проблемы

Сравнение криптографических значений через стандартные строковые операции в JavaScript формирует предсказуемый канал утечки информации. CryptoJS, предоставляя удобный интерфейс хеширования, не решает задачу безопасного сравнения, оставляя её на уровне прикладной реализации.

Константное время сравнения становится обязательным элементом любых механизмов проверки криптографических значений, независимо от выбранной библиотеки хеширования.