Цепочечное и инкрементальное хеширование: обходные пути

Встроенный механизм хеширования в SubtleCrypto предоставляет метод digest, который принимает на вход весь массив данных целиком. Это означает:

  • отсутствие нативной поддержки потокового (streaming) хеширования
  • невозможность «докармливать» данные частями
  • необходимость держать весь буфер в памяти

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


Природа проблемы: отсутствие incremental API

Классические криптографические библиотеки (например, OpenSSL) поддерживают следующий паттерн:

  1. Инициализация контекста
  2. Последовательное обновление (upd ate)
  3. Финализация (final)

В Web Crypto API отсутствует аналогичная модель. Метод crypto.subtle.digest работает как чистая функция:

const hash = await crypto.subtle.digest("SHA-256", data);

Это означает, что:

  • нельзя вызвать update() несколько раз
  • нельзя прервать или продолжить процесс
  • нельзя использовать хеширование «на лету»

Подход 1: Буферизация данных

Самый очевидный обход — накопление всех чанков в один буфер.

async function hashChunks(chunks) {
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
  const combined = new Uint8Array(totalLength);

  let offset = 0;
  for (const chunk of chunks) {
    combined.se t(chunk, offset);
    offset += chunk.length;
  }

  return crypto.subtle.digest("SHA-256", combined);
}

Недостатки:

  • потребление памяти линейно растёт
  • не подходит для потоков и больших файлов
  • снижает эффективность при работе с сетью

Подход 2: Использование ReadableStream + аккумуляция

При работе с потоками (например, fetch) можно собирать данные постепенно:

async function hashStream(stream) {
  const reader = stream.getReader();
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
  }

  return hashChunks(chunks);
}

Фактически это тот же буферизационный подход, но адаптированный под поток.


Подход 3: Использование сторонних библиотек

Для настоящего инкрементального хеширования применяются JavaScript-библиотеки, реализующие алгоритмы вручную:

  • hash.js
  • crypto-js
  • spark-md5

Пример с crypto-js:

const sha256 = CryptoJS.algo.SHA256.create();

sha256.update(CryptoJS.lib.WordArray.create(chunk1));
sha256.update(CryptoJS.lib.WordArray.create(chunk2));

const hash = sha256.finalize();

Преимущества:

  • полноценная поддержка update()
  • работа с потоками
  • гибкость

Недостатки:

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

Подход 4: WebAssembly-решения

Более эффективный вариант — использование WASM-реализаций криптографии:

  • hash-wasm
  • сборки OpenSSL или BoringSSL
  • специализированные SHA-реализации

Пример:

import { createSHA256 } from 'hash-wasm';

const hasher = await createSHA256();

hasher.init();
hasher.update(chunk1);
hasher.update(chunk2);

const hash = hasher.digest();

Преимущества:

  • близкая к нативной производительность
  • поддержка потоковой обработки
  • контроль над процессом

Подход 5: Комбинирование Web Crypto и chunk-хешей

Иногда применяется стратегия «дерева хешей» (Merkle tree):

  1. Хеширование каждого чанка отдельно
  2. Объединение хешей
  3. Повторное хеширование
async function hashChunk(chunk) {
  return new Uint8Array(await crypto.subtle.digest("SHA-256", chunk));
}

async function hashTree(chunks) {
  const hashes = await Promise.all(chunks.map(hashChunk));
  return hashChunks(hashes);
}

Особенности:

  • подходит для параллельной обработки
  • используется в системах распределённого хранения
  • итоговый хеш отличается от стандартного SHA-256 от всего файла

Подход 6: File API и slicing

Для работы с файлами можно читать их по частям:

async function hashFile(file, chunkSize = 1024 * 1024) {
  const chunks = [];
  let offset = 0;

  while (offset < file.size) {
    const slice = file.slice(offset, offset + chunkSize);
    const buffer = await slice.arrayBuffer();
    chunks.push(new Uint8Array(buffer));
    offset += chunkSize;
  }

  return hashChunks(chunks);
}

Это позволяет:

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

Однако финальное хеширование всё равно требует объединения.


Подход 7: Использование TransformStream

Можно построить поток обработки:

function createHashingStream() {
  const chunks = [];

  return new TransformStream({
    transform(chunk, controller) {
      chunks.push(chunk);
      controller.enqueue(chunk);
    },
    async flush() {
      const hash = await hashChunks(chunks);
      console.log(new Uint8Array(hash));
    }
  });
}

Такой подход полезен для:

  • промежуточной обработки данных
  • интеграции с pipeline

Но не решает проблему отсутствия incremental digest.


Сравнение подходов

Подход Потоковость Производительность Память Нативность
Буферизация Нет Высокая Плохо Да
Stream + buffer Частично Средняя Плохо Да
JS-библиотеки Да Ниже Хорошо Нет
WebAssembly Да Высокая Хорошо Частично
Merkle tree Да Высокая Хорошо Да

Практические сценарии выбора

Небольшие данные (до нескольких MB):

  • crypto.subtle.digest без обходных решений

Большие файлы:

  • WASM или JS-библиотеки с incremental API

Потоки (network streaming):

  • WASM-решения или crypto-js

Параллельная обработка:

  • Merkle tree

Критичная безопасность и производительность:

  • WebAssembly + проверенная реализация

Архитектурные ограничения браузера

Причины отсутствия incremental API:

  • безопасность (контроль времени выполнения операций)
  • простота интерфейса
  • асинхронная модель Web Crypto
  • ограничения спецификации W3C

Это приводит к тому, что Web Crypto остаётся:

  • высокоуровневым API
  • не предназначенным для потоковой криптографии
  • ориентированным на завершённые данные

Итоговая архитектурная стратегия

Эффективная работа с хешированием в браузере строится на комбинировании:

  • SubtleCrypto — для финальных операций
  • WASM — для потоковой обработки
  • Streams API — для управления данными
  • Chunking — для контроля памяти

Такой подход позволяет компенсировать ограничения Web Crypto API и реализовать масштабируемую обработку данных без потери производительности и контроля.