JavaScript-оператор строгого равенства === выполняет
сравнение с оптимизациями движка V8 и других JS-движков,
ориентированными на скорость, а не на безопасность. В криптографическом
контексте это приводит к фундаментальной проблеме: время выполнения
сравнения может зависеть от содержимого сравниваемых данных.
При обычной логике приложения важно лишь, совпадают ли значения. В криптографии важно ещё и то, как именно происходит это сравнение.
Оператор === в типичном движке Jav * aScript:
Это означает, что время выполнения становится функцией от секретных данных.
Если сравнивается секрет (ключ, подпись, HMAC, токен), то злоумышленник может:
Даже если различия кажутся ничтожными, накопление статистики делает атаку реальной.
Классическая проблема выглядит так:
if (userInput === secretToken) {
// доступ разрешён
}
Такой код раскрывает информацию о secretToken через
время отклика, потому что сравнение прекращается на первом
несовпадении.
В криптографических системах сравнение используется в ключевых местах:
Любая утечка даже одного байта может резко сузить пространство поиска.
Большинство реализаций стандартного сравнения устроено как:
для каждого байта:
если байты различаются:
вернуть false
вернуть true
Ранний выход делает время выполнения переменным.
В оптимизированном JS это усиливается тем, что:
===Библиотеки уровня TweetNaCl.js и nacl.js исходят из принципа: любые операции с секретами должны выполняться в константное время.
Поэтому там используется сравнение без ветвлений, зависящих от данных.
В TweetNaCl.js применяется функция вида «verify», которая реализует побайтовое сравнение через накопление результата, а не через ранний выход:
import nacl from "tweetnacl";
const a = new Uint8Array([1,2,3]);
const b = new Uint8Array([1,2,3]);
const ok = nacl.verify(a, b);
Идея такого подхода:
Безопасное сравнение строится на XOR-аккумуляции:
function constantTimeEqual(a, b) {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a[i] ^ b[i];
}
return diff === 0;
}
Особенность:
В Node.js проблема признана на уровне платформы:
import crypto from "crypto";
crypto.timingSafeEqual(buf1, buf2);
Эта функция специально реализована на уровне C++ так, чтобы:
Распространённая ошибка — считать, что небольшие различия не важны. Однако в реальности:
Даже если разница в 1–2 байта, статистически она извлекается.
Строки в JS особенно опасны:
Поэтому криптографические операции всегда переводят данные в
Uint8Array, избегая строк.
if (signature === expectedSignature) {
// никогда так не делать
}
Проблема не в синтаксисе, а в модели выполнения: сравнение не контролируется разработчиком на уровне таймингов.
Для криптографии сравнение должно рассматриваться не как логическая операция, а как функция:
Любое отклонение от этой модели создаёт побочный канал.
В библиотеках семейства NaCl принцип постоянного времени встроен в архитектуру:
Именно поэтому использование === внутри
криптографических проверок полностью противоречит модели безопасности
этих библиотек.