Тайминговые атаки относятся к классу побочных каналов (side-channel attacks), при которых злоумышленник извлекает информацию не из самих данных, а из времени выполнения операций. Даже если криптографический алгоритм математически устойчив, его реализация может раскрывать сведения о секретах через микроскопические различия во времени обработки.
Основная идея заключается в том, что разные входные данные могут приводить к различному количеству вычислительных шагов. В результате измерение времени выполнения становится источником информации о скрытых значениях, таких как ключи подписи, HMAC-секреты или токены.
В контексте JavaScript это особенно критично из-за:
Даже минимальные различия, измеряемые в наносекундах, могут быть агрегированы через большое количество запросов и привести к восстановлению секретных данных.
Одним из наиболее уязвимых мест криптографических систем является сравнение подписей или токенов.
Наивная реализация:
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 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;
}
Здесь:
return внутри цикла,^) для выявления различий,|=),Даже если строки различаются в первом символе, цикл всё равно выполняется полностью.
В JavaScript оператор === и == не
предназначены для криптографической безопасности.
Причины:
Даже встроенные методы вроде
String.prototype.localeCompare категорически непригодны для
криптографических задач.
В Node.js существует встроенный механизм для безопасного сравнения буферов:
import crypto from 'crypto';
const a = Buffer.from('secret1');
const b = Buffer.from('secret2');
crypto.timingSafeEqual(a, b);
Этот метод реализован на уровне C++ и обеспечивает:
Ключевое ограничение: длины буферов должны совпадать, иначе будет выброшена ошибка.
В криптографических библиотеках, работающих с JWS, JWE и JWT, проверка подписи является критическим этапом.
В экосистеме jose проверка подписи включает сравнение
вычисленной подписи с переданной.
Типичная логика:
Уязвимая реализация могла бы выглядеть так:
if (signature === expectedSignature) {
return payload;
}
Безопасная реализация использует константное сравнение через
Uint8Array:
import { timingSafeEqual } from 'crypto';
function safeCompare(a, b) {
const bufA = new Uint8Array(a);
const bufB = new Uint8Array(b);
if (bufA.length !== bufB.length) {
return false;
}
return timingSafeEqual(bufA, bufB);
}
Несмотря на наличие простого паттерна, на практике часто встречаются ошибки:
if (a.length !== b.length) return false;
Хотя это допустимо, сама проверка длины уже даёт утечку информации о структуре данных. В некоторых моделях угроз это считается нежелательным.
Строки в JavaScript — абстракция, и их побайтовое сравнение может быть непредсказуемым.
return a.slice(0, 10) === b.slice(0, 10);
Такой подход разрушает криптографическую стойкость полностью.
every или somea.every((v, i) => v === b[i]);
Метод every может прерваться при первом несовпадении,
что снова создаёт тайминговую утечку.
Движок V8 (используемый в Node.js и Chrome) активно оптимизирует горячие пути выполнения:
Это означает, что даже корректно выглядящий код может стать небезопасным после оптимизации.
Пример опасного поведения:
Безопасная реализация должна следовать нескольким принципам:
Обобщённая схема:
Формально:
В системах авторизации JWT слабое сравнение может привести к:
Особенно критично это в микросервисной архитектуре, где токены проверяются часто и автоматически.
Тайминговая атака не требует прямого доступа к данным. Достаточно:
При недостаточно защищённом сравнении даже современные криптографические алгоритмы становятся уязвимыми не из-за математики, а из-за реализации.
Константное сравнение является одной из базовых, но критически важных защитных мер, обеспечивающих устойчивость криптографических систем в JavaScript-окружении.