Тестирование криптографического кода

Криптографические операции в Web Crypto API построены вокруг асинхронной модели и работают через интерфейс crypto.subtle, что сразу усложняет тестирование: отсутствуют детерминированные входы, результаты зависят от случайности (IV, nonce, ключи), а часть операций невозможно корректно воспроизвести без управления источником энтропии.

Большинство криптографических примитивов в Web Crypto API изначально недетерминированы:

  • AES-GCM требует уникального IV для каждого шифрования
  • RSA-OAEP зависит от случайного паддинга
  • ECDSA использует случайное значение k при подписи
  • генерация ключей полностью опирается на системный CSPRNG

Это означает, что прямое сравнение выходных данных в тестах часто невозможно. Вместо этого проверяется:

  • корректность обратимости (encrypt → decrypt)
  • соответствие стандартным тест-векторам
  • структурная корректность результата (например, длины буферов)
  • стабильность интерфейса и ошибок

Использование тест-векторов

Наиболее надёжный способ тестирования криптографии — фиксированные векторы из стандартов:

  • NIST SP 800-38A (AES)
  • RFC 3394 (key wrapping)
  • RFC 7518 (JOSE)
  • FIPS 180-4 (SHA)

Пример теста AES-GCM с фиксированными значениями:

const keyBytes = new Uint8Array([
  0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
  0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
]);

const iv = new Uint8Array(12);
const plaintext = new TextEncoder().encode("test message");

const key = await crypto.subtle.importKey(
  "raw",
  keyBytes,
  { name: "AES-GCM" },
  false,
  ["encrypt", "decrypt"]
);

const encrypted = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  key,
  plaintext
);

В тесте проверяется не “точное совпадение ciphertext”, а:

  • успешное расшифрование
  • совпадение исходного текста
const decrypted = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv },
  key,
  encrypted
);

expect(new TextDecoder().decode(decrypted))
  .toBe("test message");

Асинхронность и организация тестов

Web Crypto API полностью асинхронен, что влияет на структуру тестов. В Jest / Vitest важно:

  • использовать async/await
  • избегать гонок
  • контролировать завершение промисов

Типичный паттерн:

test("AES-GCM roundtrip", async () => {
  const encrypted = await encryptData();
  const decrypted = await decryptData(encrypted);

  expect(decrypted).toEqual(originalData);
});

Ошибкой является использование синхронных обёрток или отсутствие await, что приводит к ложноположительным результатам.

Проблема случайности и стратегии её обхода

Так как API использует криптографически стойкую случайность, тесты должны либо:

  1. фиксировать входные параметры
  2. заменять источник случайности
  3. тестировать только свойства результата

Подход с фиксированным IV

Для AES-GCM допустимо задавать IV вручную в тестах:

const iv = new Uint8Array(12).fill(1);

Это не нарушает безопасность теста, так как выполняется только в контролируемой среде.

Негативный пример

Попытка сравнивать ciphertext напрямую:

expect(encrypted).toEqual(expectedCiphertext);

Такой подход почти всегда нестабилен и ломается при изменении реализации браузера.

Моки Web Crypto API

В Node.js окружении часто отсутствует полноценный crypto.subtle, либо он отличается от браузерного. Поэтому применяется мокирование.

Пример подмены crypto.subtle.encrypt:

global.crypto = {
  subtle: {
    encrypt: jest.fn(async () => new ArrayBuffer(16)),
    decrypt: jest.fn(async () => new TextEncoder().encode("mock")),
  }
};

Однако мокирование криптографии требует осторожности:

  • нельзя тестировать безопасность через моки
  • можно тестировать только бизнес-логику вокруг API
  • моки не должны подменять криптографическую корректность

Проверка совместимости браузеров

Web Crypto API имеет различия между реализациями:

  • Chrome / Chromium
  • Firefox
  • Safari

Различаются:

  • поддержка алгоритмов
  • ограничения ключей
  • формат экспортируемых ключей

Для проверки используется слой абстракции:

async function generateKey() {
  return crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
}

Тестируется не конкретный браузер, а контракт функции:

  • ключ можно экспортировать
  • ключ можно импортировать обратно
  • операции работают симметрично

Тестирование генерации ключей

Генерация ключей не должна проверяться на “правильность содержимого”, так как результат случайный. Проверяются свойства:

test("key generation", async () => {
  const key = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );

  expect(key.algorithm.name).toBe("AES-GCM");
  expect(key.extractable).toBe(true);
});

Дополнительно проверяется возможность использования:

const exported = await crypto.subtle.exportKey("raw", key);
expect(exported.byteLength).toBe(32);

PBKDF2 и тестирование производных ключей

Функции деривации ключей особенно чувствительны к параметрам:

  • salt
  • iterations
  • hash

Тестирование строится на фиксированных значениях:

const baseKey = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode("password"),
  "PBKDF2",
  false,
  ["deriveBits"]
);

const derived = await crypto.subtle.deriveBits(
  {
    name: "PBKDF2",
    salt: new TextEncoder().encode("salt"),
    iterations: 1000,
    hash: "SHA-256"
  },
  baseKey,
  256
);

Проверяется:

  • длина результата
  • стабильность при одинаковых входах
  • отличие при изменении salt

Тестирование ошибок и негативных сценариев

Криптографический API активно генерирует ошибки:

  • неверный ключ
  • неподдерживаемый алгоритм
  • неправильный размер IV
  • попытка decrypt с другим ключом

Пример:

await expect(
  crypto.subtle.decrypt(
    { name: "AES-GCM", iv },
    wrongKey,
    data
  )
).rejects.toThrow();

Особенность: важно тестировать не только успех, но и гарантированное падение.

Проверка целостности данных

Для режимов AEAD (например AES-GCM) важно проверять:

  • любое изменение ciphertext приводит к ошибке
  • изменение tag делает расшифрование невозможным
const corrupted = new Uint8Array(encrypted);
corrupted[0] ^= 1;

await expect(
  crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, corrupted)
).rejects.toThrow();

Тайминги и нестабильность CI

Криптографические тесты иногда нестабильны в CI из-за:

  • различий реализации CSPRNG
  • нагрузки на систему
  • sandbox-ограничений

Решения:

  • увеличение таймаутов тестов
  • отказ от сравнения “точных байтов”
  • минимизация параллельных крипто-тестов
  • изоляция тестов ключей

Тестирование потоков данных (streaming crypto)

В некоторых реализациях используется потоковое шифрование через Web Streams API. Здесь тестирование усложняется:

  • данные приходят чанками
  • порядок и буферизация важны
  • возможны race conditions

Проверяется:

  • целостность итогового результата
  • независимость от разбиения на чанки

Абстракция над crypto.subtle

В реальных проектах создаётся слой-обёртка:

export async function encryptText(key, text) {
  const iv = crypto.getRandomValues(new Uint8Array(12));

  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    new TextEncoder().encode(text)
  );

  return { iv, encrypted };
}

Тестируется уже эта абстракция, а не низкоуровневый API, что позволяет:

  • изолировать сложность Web Crypto API
  • стабилизировать тесты
  • упростить замену реализации

Граничные случаи

Особое внимание уделяется:

  • пустым входным данным
  • очень длинным буферам
  • Unicode-строкам (эмодзи, суррогаты)
  • выравниванию блоков

Пример:

const text = "?".repeat(1000);

Проверяется, что шифрование не ломается на нестандартных строках.

Совместное тестирование с Node.js crypto

Иногда требуется сверять Web Crypto API с Node.js crypto:

  • сравнение AES-GCM результатов
  • проверка PBKDF2
  • сверка SHA-хэшей

Важно учитывать:

  • разные форматы буферов
  • различия в API
  • endian-особенности

Такой подход используется как кросс-проверка реализации, а не как источник истины.

Структура надёжного криптотеста

Хорошо организованный тест криптографического кода обычно включает:

  • фиксированные входные данные
  • контроль параметров (IV, salt)
  • проверку обратимости
  • проверку ошибок
  • отсутствие зависимости от случайного вывода
  • минимизацию сравнения raw ciphertext

Эта модель позволяет тестам оставаться стабильными даже при изменениях браузеров и реализаций Web Crypto API.