Даже при использовании высокоуровневого API браузера
криптографические операции не существуют в изоляции от побочных каналов.
Один из наиболее значимых — различия во времени выполнения. В контексте
Web Crypto API это особенно важно, поскольку JavaScript-код,
взаимодействующий через SubtleCrypto, часто воспринимается
как безопасный слой абстракции, скрывающий низкоуровневые детали
реализации.
T_{op} = T_{base} + T_{branch} + T_{cache} + T_{cpu_state}
Любая зависимость времени выполнения от входных данных создаёт потенциальный канал утечки информации. Даже микроскопические различия, измеряемые в наносекундах, могут быть агрегированы атакующим при многократных измерениях.
В Web Crypto API прямой доступ к криптографическим примитивам
осуществляется через crypto.subtle, однако реализация
находится внутри браузера и часто использует нативные библиотеки
(OpenSSL, BoringSSL, NSS). Несмотря на то что они написаны с учётом
защиты от timing leaks, ошибки интеграции на уровне JavaScript-обвязки
или различия в обработке ошибок могут разрушить эту гарантию.
Пример типичного небезопасного сравнения в пользовательском коде:
function unsafeCompare(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;
}
Такой код кажется безобидным, но время выхода из функции зависит от позиции первого несовпадения. При многократных измерениях атакующий способен восстановить секрет побитово.
Корректный подход требует константного времени сравнения:
function constantTimeCompare(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
В криптографических реализациях внутри Web Crypto подобные операции
уже оптимизированы, однако проблема возникает на границах системы:
сериализация данных, преобразование типов (ArrayBuffer,
Uint8Array), обработка ошибок catch, а также
различия в поведении браузеров.
Особенно опасны сценарии, где ошибка криптооперации зависит от содержимого данных:
try {
await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext
);
} catch (e) {
// различие между "invalid tag" и "decryption failed"
}
Если поведение различается по времени или типу исключения, возникает побочный канал.
Padding oracle возникает в режимах блочного шифрования, где
используется дополнение (padding), например PKCS#7 в AES-CBC. Несмотря
на то что Web Crypto API предоставляет безопасный интерфейс,
неправильное использование AES-CBC может привести к утечке
информации через различия в ошибках расшифрования.
Суть атаки заключается в том, что система непреднамеренно сообщает атакующему, корректно ли было заполнение блока после расшифрования. Даже бинарный сигнал «успех/ошибка» достаточен для восстановления открытого текста.
Типичный небезопасный сценарий:
async function decryptMessage(key, iv, data) {
try {
return await crypto.subtle.decrypt(
{ name: "AES-CBC", iv },
key,
data
);
} catch (e) {
throw new Error("Decryption failed");
}
}
На первый взгляд, информация скрыта. Однако если разные типы ошибок приводят к разным задержкам или сообщениям, возникает oracle.
В классической модели padding oracle атакующий многократно отправляет модифицированные ciphertext и наблюдает, проходит ли операция успешно. Это позволяет восстановить блоки plaintext без знания ключа.
P() f()
Web Crypto API снижает вероятность таких атак за счёт унифицированного поведения ошибок, однако полностью устранить риск на уровне приложения невозможно, если:
Современная практика требует отказа от CBC в пользу AEAD режимов, таких как AES-GCM, где целостность и конфиденциальность объединены.
В режиме AEAD, например AES-GCM, nonce (или IV) должен быть уникальным для каждого шифрования под одним ключом. Нарушение этого требования приводит к катастрофическим последствиям, вплоть до полного восстановления ключевого потока и раскрытия сообщений.
Web Crypto API явно не предотвращает повтор nonce — ответственность лежит на разработчике.
Пример корректного использования:
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
key,
data
);
Проблемный сценарий:
const iv = new Uint8Array(12); // всегда нули
await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
data1
);
await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
data2
);
Повтор nonce в GCM приводит к повторному использованию гаммы:
C_1 C_2 = (P_1 K) (P_2 K) = P_1 P_2
Из этого соотношения следует, что XOR двух ciphertext раскрывает XOR исходных сообщений. При наличии частичной информации о plaintext атака становится тривиальной.
В Web Crypto API особенно опасны следующие ошибки:
Math.random() вместо
crypto.getRandomValues;Дополнительно важно учитывать конкурентные сценарии в асинхронной среде JavaScript. При параллельных вызовах функции шифрования возможно случайное совпадение nonce, если он генерируется вне атомарного контекста.
Web Crypto API изолирует криптографические операции от JavaScript-движка, однако граница между ними остаётся уязвимой зоной:
ArrayBuffer;Даже при корректной криптографии утечки возможны через поведение интерфейса:
const start = performance.now();
await crypto.subtle.decrypt(params, key, data);
const end = performance.now();
Если end - start статистически зависит от содержимого
data, появляется возможность дифференциального анализа.
Одна из системных проблем заключается в том, что
SubtleCrypto предоставляет «безопасные» операции, но не
безопасные композиции. Комбинация корректных примитивов может привести к
небезопасному протоколу.
Например:
Подобные ошибки не являются багами Web Crypto API, но именно они формируют поверхность атак.
Комбинация timing leaks, padding oracle и nonce reuse приводит к различным уровням компрометации:
Особенно опасны веб-приложения, где криптография используется для:
Даже без изменения API-уровня Web Crypto можно минимизировать поверхность атак:
crypto.getRandomValues;Ключевой принцип — отсутствие зависимости observable-поведения от секретных данных.