Шифрование больших объёмов данных: разбивка на чанки

Web Crypto API не предоставляет потокового шифрования в классическом смысле. Все операции crypto.subtle.encrypt() и crypto.subtle.decrypt() работают с целостными буферами данных (ArrayBuffer), что создаёт фундаментальное ограничение при работе с файлами, объёмами в сотни мегабайт и выше.

Основная проблема заключается в том, что невозможно «дополнить» уже начатую операцию шифрования новыми данными. Каждый вызов криптографической функции обрабатывает только тот блок, который был передан на вход. Это делает необходимым ручное разбиение данных на части — чанки.

При работе с большими объёмами данных появляются ключевые требования:

  • минимизация потребления памяти
  • отсутствие блокировки основного потока
  • корректная обработка целостности данных
  • уникальность параметров шифрования для каждого фрагмента

Разбиение данных на чанки как базовая модель обработки

Чанкирование — это стратегия, при которой исходный массив данных разбивается на фиксированные или динамические блоки, каждый из которых шифруется отдельно.

Типичная схема:

  • исходный файл → ArrayBuffer
  • разбиение на N частей фиксированного размера
  • шифрование каждого чанка отдельно
  • сохранение метаданных для восстановления

Простейшая модель разбиения:

function chunkArrayBuffer(buffer, chunkSize) {
  const chunks = [];
  for (let i = 0; i < buffer.byteLength; i += chunkSize) {
    chunks.push(buffer.slice(i, i + chunkSize));
  }
  return chunks;
}

Чанк размером 64–256 KB часто является компромиссом между производительностью и нагрузкой на память.

Выбор режима шифрования для чанков

При шифровании чанков критически важно выбрать корректный режим, так как не все режимы одинаково безопасны при раздельной обработке данных.

AES-GCM

Наиболее безопасный вариант для чанков — AES-GCM. Он обеспечивает:

  • конфиденциальность
  • целостность (authentication tag)
  • защиту от подмены данных

Каждый чанк должен иметь уникальный IV.

AES-CTR

Потоковый режим, удобный для чанков, но без встроенной проверки целостности. Требует дополнительного HMAC.

AES-CBC

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

Генерация уникальных IV для каждого чанка

Ключевая ошибка при чанкировании — повторное использование IV. Для AES-GCM это критическая уязвимость.

Подходы к генерации IV:

1. Случайный IV для каждого чанка

function createIV() {
  return crypto.getRandomValues(new Uint8Array(12));
}

Недостаток — необходимость хранения IV для каждого блока.

2. Базовый IV + индекс чанка

Более контролируемый подход:

function deriveIV(baseIV, index) {
  const iv = new Uint8Array(baseIV);
  iv[11] ^= index & 0xff;
  iv[10] ^= (index >> 8) & 0xff;
  return iv;
}

Преимущество — воспроизводимость.

Структура зашифрованного чанка

Каждый зашифрованный блок должен содержать:

  • индекс чанка
  • IV
  • ciphertext
  • authentication tag (для AES-GCM)

Типичная структура:

[chunkIndex | iv | ciphertext | tag]

Для хранения часто используют бинарный формат Uint8Array.

Шифрование чанков через Web Crypto API

Базовый процесс шифрования:

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

  return new Uint8Array(encrypted);
}

Полный процесс обработки файла:

async function encryptLargeData(key, buffer, chunkSize = 64 * 1024) {
  const chunks = chunkArrayBuffer(buffer, chunkSize);
  const result = [];

  for (let i = 0; i < chunks.length; i++) {
    const iv = deriveIV(new Uint8Array(12), i);

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

    result.push({
      index: i,
      iv,
      data: new Uint8Array(encrypted)
    });
  }

  return result;
}

Дешифрование чанков и восстановление данных

Дешифрование требует строгого соблюдения порядка чанков и идентичности параметров шифрования.

async function decryptChunk(key, chunk, iv) {
  const decrypted = await crypto.subtle.decrypt(
    {
      name: "AES-GCM",
      iv
    },
    key,
    chunk
  );

  return new Uint8Array(decrypted);
}

Сборка данных:

async function decryptLargeData(key, encryptedChunks) {
  encryptedChunks.sort((a, b) => a.index - b.index);

  const buffers = [];

  for (const chunk of encryptedChunks) {
    const decrypted = await crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: chunk.iv
      },
      key,
      chunk.data
    );

    buffers.push(new Uint8Array(decrypted));
  }

  return mergeBuffers(buffers);
}

Слияние:

function mergeBuffers(buffers) {
  const totalLength = buffers.reduce((acc, b) => acc + b.length, 0);
  const result = new Uint8Array(totalLength);

  let offset = 0;
  for (const buf of buffers) {
    result.set(buf, offset);
    offset += buf.length;
  }

  return result;
}

Использование Streams API для оптимизации

При работе с большими файлами предпочтительно использовать ReadableStream и TransformStream, чтобы избежать загрузки всего файла в память.

Архитектура:

  • поток чтения файла
  • трансформация чанков
  • асинхронное шифрование
  • поток записи результата

Пример трансформирующего потока:

class EncryptStream extends TransformStream {
  constructor(key, chunkSize = 64 * 1024) {
    let index = 0;
    const ivBase = crypto.getRandomValues(new Uint8Array(12));

    super({
      async transform(chunk, controller) {
        const iv = deriveIV(ivBase, index++);

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

        controller.enqueue(new Uint8Array(encrypted));
      }
    });
  }
}

Такой подход позволяет обрабатывать данные без накопления в памяти.

Проблема границ чанков

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

Ошибки возникают при:

  • потоковом шифровании JSON
  • бинарных форматах с заголовками
  • сжатых данных (ZIP, GZIP)

В таких случаях требуется:

  • либо шифровать после компрессии
  • либо использовать потоковые форматы до криптографии
  • либо внедрять framing слой

Метаданные и контроль целостности

Для восстановления данных недостаточно только ciphertext. Необходима метаинформация:

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

Пример метаданных:

{
  "algorithm": "AES-GCM",
  "chunkSize": 65536,
  "chunks": [
    { "index": 0, "iv": "...", "length": 65536 },
    { "index": 1, "iv": "...", "length": 65536 }
  ]
}

Ошибки при реализации чанкового шифрования

На практике наиболее распространены следующие проблемы:

Повторное использование IV

Даже один повтор IV в AES-GCM компрометирует ключ.

Потеря порядка чанков

Без индексов восстановление становится невозможным.

Несогласованность размеров

Последний чанк часто меньше остальных и требует отдельной обработки.

Игнорирование authentication tag

Удаление tag делает GCM режим фактически уязвимым.

Оптимизация производительности

Для ускорения обработки больших данных применяются:

  • параллельное шифрование чанков (Web Workers)
  • буферизация потоков
  • предварительное выделение памяти
  • батчинг операций encrypt

Пример параллельной обработки:

const promises = chunks.map(async (chunk, i) => {
  const iv = deriveIV(baseIV, i);

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

  return { i, encrypted, iv };
});

Сочетание чанков и криптографических протоколов

Чанкирование в Web Crypto API фактически является пользовательским уровнем поверх криптографических primitives. Это означает, что:

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

В более сложных системах чанки часто комбинируются с:

  • HKDF для производных ключей
  • HMAC для дополнительной проверки целостности
  • nonce derivation functions

Такая комбинация позволяет приблизиться к поведению потокового шифрования без его нативной поддержки в Web Crypto API