Потоковая обработка в веб-приложениях строится на идее поэтапного потребления данных без необходимости загружать весь объём в память. В контексте криптографии это особенно важно: шифрование больших файлов, медиа или сетевых потоков становится возможным без резких скачков потребления памяти и без блокировки основного потока выполнения.
Ключевая связка в современном браузере формируется из трёх компонентов:
Web Streams API предоставляет абстракцию потокового чтения данных, где информация поступает чанками (chunks), а не единым массивом.
Базовая модель:
Пример создания простого потока:
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("Hello"));
controller.enqueue(new TextEncoder().encode(" world"));
controller.close();
}
});
Чанки поступают последовательно, что позволяет обрабатывать данные без ожидания завершения передачи всего объёма.
TransformStream соединяет входной и выходной потоки и позволяет модифицировать данные на лету.
Архитектура:
Простейшая форма:
const transform = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk);
}
});
В контексте криптографии именно TransformStream становится местом, где выполняется шифрование или расшифрование каждого чанка.
Web Crypto API предоставляет интерфейс crypto.subtle,
однако он не поддерживает нативное потоковое шифрование.
Основные особенности:
ArrayBuffer целикомЭто означает, что потоковая криптография реализуется как надстройка:
На практике используется режим AES-GCM, обеспечивающий:
Ключ:
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
Основная идея: каждый чанк шифруется отдельно.
Проблема: AES-GCM требует уникального IV.
Решение: детерминированная генерация nonce.
Пример схемы:
function createEncryptStream(key, baseIv) {
let counter = 0;
return new TransformStream({
async transform(chunk, controller) {
const iv = new Uint8Array(baseIv);
iv[11] = counter & 0xff;
iv[10] = (counter >> 8) & 0xff;
const encrypted = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
key,
chunk
);
counter++;
controller.enqueue(new Uint8Array(encrypted));
}
});
}
Каждый блок обрабатывается независимо, что позволяет сохранять потоковую природу.
Расшифрование симметрично:
function createDecryptStream(key, baseIv) {
let counter = 0;
return new TransformStream({
async transform(chunk, controller) {
const iv = new Uint8Array(baseIv);
iv[11] = counter & 0xff;
iv[10] = (counter >> 8) & 0xff;
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
key,
chunk
);
counter++;
controller.enqueue(new Uint8Array(decrypted));
}
});
}
Полная цепочка:
inputStream
.pipeThrough(createEncryptStream(key, iv))
.pipeTo(outputStream);
И обратная операция:
inputStream
.pipeThrough(createDecryptStream(key, iv))
.pipeTo(outputStream);
Критический аспект потокового шифрования — уникальность nonce.
Ошибки приводят к:
Подходы:
Недопустимо:
Размер чанка влияет на:
Практика:
Важно учитывать, что AES-GCM добавляет overhead (auth tag), увеличивая размер выходного потока.
Web Streams API автоматически управляет backpressure через writable stream.
Если потребитель не успевает обрабатывать данные:
Это критично при шифровании больших потоков, например:
crypto.subtle операции асинхронны, что приводит к:
Оптимизации:
AES-GCM обеспечивает проверку целостности, но потоковая модель требует дополнительного контроля:
Пример обработки:
new TransformStream({
async transform(chunk, controller) {
try {
const encrypted = await crypto.subtle.encrypt(...);
controller.enqueue(encrypted);
} catch (e) {
controller.error(e);
}
}
});
Типичная схема:
или:
Ключевой принцип — отсутствие буферизации всего объекта.
Подобная модель применяется в:
Главное преимущество — постоянное потребление памяти вне зависимости от размера данных.
При необходимости восстановления состояния (например, при прерывании загрузки) требуется:
Это делает потоковую криптографию не только технической задачей, но и задачей протокольного уровня.