Криптографические операции в Web Crypto API построены вокруг
асинхронной модели и работают через интерфейс
crypto.subtle, что сразу усложняет тестирование:
отсутствуют детерминированные входы, результаты зависят от случайности
(IV, nonce, ключи), а часть операций невозможно корректно воспроизвести
без управления источником энтропии.
Большинство криптографических примитивов в Web Crypto API изначально недетерминированы:
k при подписиЭто означает, что прямое сравнение выходных данных в тестах часто невозможно. Вместо этого проверяется:
Наиболее надёжный способ тестирования криптографии — фиксированные векторы из стандартов:
Пример теста 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 использует криптографически стойкую случайность, тесты должны либо:
Для AES-GCM допустимо задавать IV вручную в тестах:
const iv = new Uint8Array(12).fill(1);
Это не нарушает безопасность теста, так как выполняется только в контролируемой среде.
Попытка сравнивать ciphertext напрямую:
expect(encrypted).toEqual(expectedCiphertext);
Такой подход почти всегда нестабилен и ломается при изменении реализации браузера.
В 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")),
}
};
Однако мокирование криптографии требует осторожности:
Web Crypto API имеет различия между реализациями:
Различаются:
Для проверки используется слой абстракции:
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);
Функции деривации ключей особенно чувствительны к параметрам:
Тестирование строится на фиксированных значениях:
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
);
Проверяется:
Криптографический API активно генерирует ошибки:
Пример:
await expect(
crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
wrongKey,
data
)
).rejects.toThrow();
Особенность: важно тестировать не только успех, но и гарантированное падение.
Для режимов AEAD (например AES-GCM) важно проверять:
const corrupted = new Uint8Array(encrypted);
corrupted[0] ^= 1;
await expect(
crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, corrupted)
).rejects.toThrow();
Криптографические тесты иногда нестабильны в CI из-за:
Решения:
В некоторых реализациях используется потоковое шифрование через Web Streams API. Здесь тестирование усложняется:
Проверяется:
В реальных проектах создаётся слой-обёртка:
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, что позволяет:
Особое внимание уделяется:
Пример:
const text = "?".repeat(1000);
Проверяется, что шифрование не ломается на нестандартных строках.
Иногда требуется сверять Web Crypto API с Node.js
crypto:
Важно учитывать:
Такой подход используется как кросс-проверка реализации, а не как источник истины.
Хорошо организованный тест криптографического кода обычно включает:
Эта модель позволяет тестам оставаться стабильными даже при изменениях браузеров и реализаций Web Crypto API.