Класс атак, основанных на анализе времени выполнения криптографических операций, относится к 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;
}
На первый взгляд функция корректна, но фактически она прекращает работу сразу при первом несовпадении. Это означает, что время выполнения зависит от позиции первой ошибки. Если атакующий многократно отправляет запросы и измеряет задержку ответа, он может определить, насколько «далеко» он находится от правильного значения.
В реальных системах это может привести к восстановлению:
Безопасное сравнение строк должно иметь константное время выполнения (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 и побитовое ИЛИ гарантируют, что каждый символ влияет на итог, но не влияет на время выполнения.
При аутентификации часто сравнивается:
Наивный подход:
if (inputPassword === storedHash) {
// доступ разрешён
}
Проблема в том, что:
Библиотека bcrypt.js специально спроектирована так, чтобы минимизировать утечки информации через время выполнения.
Ключевой элемент — функция сравнения:
bcrypt.compare(password, hash, callback)
или в Promise-версии:
await bcrypt.compare(password, hash)
Внутри compare происходит несколько этапов:
bcrypt-хэш содержит:
Формат:
$2a$10$......................hash
bcrypt повторно вычисляет хэш из введённого пароля с тем же salt и параметрами:
Важно: эта операция занимает примерно одинаковое время независимо от совпадения пароля.
После генерации хэша выполняется сравнение двух бинарных значений.
Ключевой момент: сравнение реализовано так, чтобы избежать 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:
Это устраняет возможность различать “быстро неверный” и “медленно неверный”.
В отличие от строковых сравнений, bcrypt.compare:
Хотя абсолютное время зависит от 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;
};
Если такая логика используется в системе:
Атакующий:
Пример логики восстановления:
Даже различия в микросекундах могут быть статистически значимы при большом количестве запросов.
Хотя bcrypt.compare уже защищён от timing attacks, в реальных системах применяются дополнительные меры:
Даже при ошибке система возвращает одинаковую структуру ответа без различий в логике выполнения.
Не различаются причины отказа:
bcrypt.js реализует алгоритм на JavaScript и использует:
Важный момент: несмотря на JavaScript-реализацию, библиотека сохраняет свойства constant-time поведения на уровне алгоритма сравнения.
При использовании:
bcrypt.compare(password, hash)
гарантируется:
Но не гарантируется:
Cost factor влияет на:
Но не влияет на возможность timing attack напрямую, потому что:
Однако слишком низкий cost factor:
Безопасная схема должна обеспечивать:
bcrypt.compare реализует эти требования за счёт комбинации: