Атаки на реализацию: timing attacks и как bcrypt.compare защищает от них

Класс атак, основанных на анализе времени выполнения криптографических операций, относится к side-channel атакам. Их цель — получить информацию о секретных данных не через взлом алгоритма напрямую, а через наблюдение за косвенными утечками, в частности за временем ответа системы.

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

Простейший пример уязвимого сравнения:

function insecureCompare(a, b) {
  if (a.length !== b.length) return false;

  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) return false;
  }

  return true;
}

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

В реальных системах это может привести к восстановлению:

  • токенов доступа
  • API ключей
  • хэшированных паролей (через косвенную корреляцию)

Почему криптографическое сравнение требует постоянного времени

Безопасное сравнение строк должно иметь константное время выполнения (constant-time comparison), то есть не зависеть от содержимого строк.

Идея проста: алгоритм обязан пройтись по всем байтам независимо от того, где находится первое несовпадение.

Общий принцип:

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

Пример безопасного подхода:

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

Здесь операция XOR и побитовое ИЛИ гарантируют, что каждый символ влияет на итог, но не влияет на время выполнения.


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

При аутентификации часто сравнивается:

  • введённый пароль
  • bcrypt-хэш, сохранённый в базе

Наивный подход:

if (inputPassword === storedHash) {
  // доступ разрешён
}

Проблема в том, что:

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

Архитектура bcrypt и защита от side-channel атак

Библиотека bcrypt.js специально спроектирована так, чтобы минимизировать утечки информации через время выполнения.

Ключевой элемент — функция сравнения:

bcrypt.compare(password, hash, callback)

или в Promise-версии:

await bcrypt.compare(password, hash)

Как работает bcrypt.compare

Внутри compare происходит несколько этапов:

1. Парсинг хэша

bcrypt-хэш содержит:

  • cost factor (число итераций)
  • salt
  • сам хэш

Формат:

$2a$10$......................hash

2. Пересчёт хэша

bcrypt повторно вычисляет хэш из введённого пароля с тем же salt и параметрами:

  • используется Blowfish-based key schedule
  • вычисление намеренно медленное (cost factor)

Важно: эта операция занимает примерно одинаковое время независимо от совпадения пароля.


3. Константное сравнение результатов

После генерации хэша выполняется сравнение двух бинарных значений.

Ключевой момент: сравнение реализовано так, чтобы избежать timing leakage.

Принцип:

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

Упрощённая модель:

function constantTimeCompare(a, b) {
  let diff = 0;

  for (let i = 0; i < a.length; i++) {
    diff |= a[i] ^ b[i];
  }

  return diff === 0;
}

Почему bcrypt устойчив к timing attacks

1. Фиксированная сложность вычисления

Даже если пароль неверный, bcrypt:

  • всё равно выполняет полную процедуру хэширования
  • не завершает выполнение раньше

Это устраняет возможность различать “быстро неверный” и “медленно неверный”.


2. Отсутствие раннего выхода

В отличие от строковых сравнений, bcrypt.compare:

  • не прекращает вычисления при первом несовпадении
  • не использует ветвления, зависящие от результата промежуточного сравнения

3. Нормализация времени ответа

Хотя абсолютное время зависит от cost factor, относительные различия между успешной и неуспешной проверкой минимизируются.


Типичная уязвимая альтернатива и её последствия

Рассмотрим ошибочную архитектуру:

const isValid = (input, stored) => {
  if (input.length !== stored.length) return false;

  for (let i = 0; i < input.length; i++) {
    if (input[i] !== stored[i]) return false;
  }

  return true;
};

Если такая логика используется в системе:

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

Практическая модель атаки на timing

Атакующий:

  1. Отправляет множество запросов с разными входными значениями
  2. Измеряет среднее время ответа
  3. Анализирует корреляцию между длиной совпадающего префикса и задержкой

Пример логики восстановления:

  • если первый символ совпал → время чуть выше
  • если первые два совпали → ещё выше
  • и так далее

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


Дополнительные меры защиты вместе с bcrypt

Хотя bcrypt.compare уже защищён от timing attacks, в реальных системах применяются дополнительные меры:

Ограничение скорости запросов

  • rate limiting на уровне API
  • блокировка IP при подозрительной активности

Унификация ответа

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

Изоляция ошибок

Не различаются причины отказа:

  • неверный пароль
  • несуществующий пользователь

Особенности реализации bcrypt.js в Node.js

bcrypt.js реализует алгоритм на JavaScript и использует:

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

Важный момент: несмотря на JavaScript-реализацию, библиотека сохраняет свойства constant-time поведения на уровне алгоритма сравнения.


Что важно понимать при использовании compare

При использовании:

bcrypt.compare(password, hash)

гарантируется:

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

Но не гарантируется:

  • защита от атак на слабые пароли
  • защита от утечек базы данных
  • защита от логических ошибок приложения

Связь cost factor и атак по времени

Cost factor влияет на:

  • время хэширования
  • нагрузку на CPU

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

  • сравнение всегда выполняется после полного вычисления
  • различия между “верно/неверно” сглаживаются

Однако слишком низкий cost factor:

  • ускоряет brute-force атаки
  • снижает общую криптостойкость системы

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

Безопасная схема должна обеспечивать:

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

bcrypt.compare реализует эти требования за счёт комбинации:

  • повторного вычисления хэша
  • константного сравнения результатов
  • фиксированной структуры алгоритма bcrypt