Атаки на реализации: timing, padding oracle, nonce reuse

Даже при использовании высокоуровневого 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 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 снижает вероятность таких атак за счёт унифицированного поведения ошибок, однако полностью устранить риск на уровне приложения невозможно, если:

  • различается текст ошибок;
  • различается время ответа;
  • присутствуют side-channel эффекты в бизнес-логике;
  • используется CBC вместо AEAD режимов.

Современная практика требует отказа от CBC в пользу AEAD режимов, таких как AES-GCM, где целостность и конфиденциальность объединены.


Повтор использования nonce (nonce reuse)

В режиме 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 особенно опасны следующие ошибки:

  • использование фиксированного IV;
  • использование timestamp как IV без контроля уникальности;
  • генерация IV через Math.random() вместо crypto.getRandomValues;
  • повторное использование состояния при потоковой обработке данных.

Дополнительно важно учитывать конкурентные сценарии в асинхронной среде JavaScript. При параллельных вызовах функции шифрования возможно случайное совпадение nonce, если он генерируется вне атомарного контекста.


Связь побочных каналов с архитектурой браузера

Web Crypto API изолирует криптографические операции от JavaScript-движка, однако граница между ними остаётся уязвимой зоной:

  • данные проходят через сериализацию ArrayBuffer;
  • ошибки возвращаются в JS-область;
  • планирование задач зависит от event loop;
  • тайминги зависят от загрузки процесса и оптимизаций JIT.

Даже при корректной криптографии утечки возможны через поведение интерфейса:

const start = performance.now();

await crypto.subtle.decrypt(params, key, data);

const end = performance.now();

Если end - start статистически зависит от содержимого data, появляется возможность дифференциального анализа.


Ошибки абстракции при использовании SubtleCrypto

Одна из системных проблем заключается в том, что SubtleCrypto предоставляет «безопасные» операции, но не безопасные композиции. Комбинация корректных примитивов может привести к небезопасному протоколу.

Например:

  • AES-GCM используется без проверки уникальности IV;
  • RSA-OAEP применяется без контроля padding consistency на уровне приложения;
  • AES-CBC используется без HMAC (отсутствие authenticate-then-encrypt);
  • ошибки дешифрования обрабатываются по-разному в зависимости от типа исключения.

Подобные ошибки не являются багами Web Crypto API, но именно они формируют поверхность атак.


Практические последствия компрометации

Комбинация timing leaks, padding oracle и nonce reuse приводит к различным уровням компрометации:

  • восстановление отдельных байтов plaintext;
  • восстановление полного сообщения;
  • извлечение keystream в потоковых режимах;
  • восстановление ключа при повторе nonce в GCM;
  • построение адаптивных атак на протоколы поверх Web Crypto.

Особенно опасны веб-приложения, где криптография используется для:

  • авторизации токенов;
  • шифрования локального хранилища;
  • защиты API-запросов;
  • end-to-end обмена сообщениями.

Системные меры снижения рисков

Даже без изменения API-уровня Web Crypto можно минимизировать поверхность атак:

  • использование AEAD (AES-GCM) вместо CBC;
  • генерация nonce исключительно через crypto.getRandomValues;
  • унификация обработки ошибок без различий по типу;
  • исключение синхронных ветвлений по секретным данным;
  • отказ от ручной реализации криптографических примитивов;
  • контроль конкурентного доступа к состоянию шифрования.

Ключевой принцип — отсутствие зависимости observable-поведения от секретных данных.