Хэширование потоков и больших объёмов данных

В задачах криптографического хэширования ключевое ограничение классических API — необходимость иметь всё входное сообщение целиком до начала вычисления хэша. Для небольших строк это не проблема, но при работе с файлами, сетевыми потоками, логами или бинарными данными такой подход становится неэффективным и иногда невозможным из-за ограничений памяти.

В SJCL (Stanford JavaScript Crypto Library) хэш-функции построены таким образом, чтобы поддерживать инкрементальную обработку данных, позволяя формировать хэш постепенно, порциями, без необходимости хранить весь объём данных в памяти.


Базовая модель работы хэш-функций

В SJCL хэш-функции реализуют стандартный интерфейс:

  • reset() — сброс состояния
  • update(data) — добавление данных в поток
  • finalize() — получение итогового хэша

Пример для SHA-256:

const hash = new sjcl.hash.sha256();

hash.update("hello ");
hash.update("world");

const result = hash.finalize();

Ключевой момент: метод update можно вызывать сколько угодно раз, постепенно подавая данные.


Представление данных в SJCL: bitArray

SJCL не работает напрямую со строками или Uint8Array как основным типом. Внутренний формат данных — это bitArray, массив 32-битных слов с дополнительной информацией о длине.

Пример преобразования строки:

const bits = sjcl.codec.utf8String.toBits("data");

Обратное преобразование:

const str = sjcl.codec.utf8String.fromBits(bits);

При потоковом хэшировании важно понимать: каждый вызов update преобразует входные данные в bitArray и добавляет их к внутреннему состоянию алгоритма.


Потоковое хэширование больших данных

При обработке больших файлов данные обычно поступают порциями (chunks). SJCL позволяет обрабатывать их без накопления всего содержимого.

Пример имитации потоковой обработки:

const sha = new sjcl.hash.sha256();

function processChunk(chunk) {
    const bits = sjcl.codec.utf8String.toBits(chunk);
    sha.update(bits);
}

// имитация потоков
processChunk("first part of ");
processChunk("large dataset ");
processChunk("streaming input");

const digest = sha.finalize();

Работа с бинарными потоками

Для бинарных данных (например, ArrayBuffer) используется преобразование в bitArray через sjcl.codec.hex или ручное преобразование.

Пример обработки ArrayBuffer:

function arrayBufferToBits(buffer) {
    const bytes = new Uint8Array(buffer);
    let hex = "";

    for (let i = 0; i < bytes.length; i++) {
        hex += bytes[i].toString(16).padStart(2, "0");
    }

    return sjcl.codec.hex.toBits(hex);
}

const sha = new sjcl.hash.sha256();

sha.update(arrayBufferToBits(chunk1));
sha.update(arrayBufferToBits(chunk2));

const digest = sha.finalize();

Этот подход используется при интеграции SJCL с Web APIs, такими как FileReader или Streams API.


Обработка файлов через потоковую модель

При работе с файлами в браузере данные обычно читаются частями через FileReader или ReadableStream.

Пример с FileReader:

const file = input.files[0];
const reader = new FileReader();
const sha = new sjcl.hash.sha256();

let offset = 0;
const chunkSize = 64 * 1024;

reader.onl oad = function(e) {
    const buffer = e.target.result;
    sha.update(arrayBufferToBits(buffer));

    offset += chunkSize;
    readNext();
};

function readNext() {
    const slice = file.slice(offset, offset + chunkSize);
    reader.readAsArrayBuffer(slice);
}

readNext();

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


Особенности внутреннего состояния хэша

Каждая хэш-функция SJCL хранит:

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

Это позволяет корректно продолжать вычисления между вызовами update.

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

  • после finalize() объект нельзя повторно использовать без reset()
  • порядок вызовов update влияет на результат
  • любые преобразования входных данных должны быть детерминированными

Сравнение с однократным хэшированием

Однократный вариант:

const digest = sjcl.hash.sha256.hash("message");

Потоковый вариант:

const sha = new sjcl.hash.sha256();
sha.update(sjcl.codec.utf8String.toBits("me"));
sha.update(sjcl.codec.utf8String.toBits("ssage"));
const digest = sha.finalize();

Оба варианта дают одинаковый результат при идентичной последовательности данных.


Производительность при потоковой обработке

Потоковое хэширование в SJCL выгодно в следующих сценариях:

  • обработка файлов > десятков мегабайт
  • сетевые потоки (WebSocket, fetch streams)
  • генерация контрольных сумм больших структур данных

Однако следует учитывать:

  • преобразование в bitArray добавляет накладные расходы
  • частые маленькие update могут снижать производительность
  • оптимальный размер блока — от 16KB до 256KB

Типичные ошибки при потоковом хэшировании

1. Потеря состояния между chunk-ами

function process(chunk) {
    const sha = new sjcl.hash.sha256(); // ошибка
    sha.update(...);
}

Правильный подход — сохранять экземпляр:

const sha = new sjcl.hash.sha256();

2. Повторный finalize

const h = sha.finalize();
const h2 = sha.finalize(); // некорректно

После финализации состояние фиксируется.


3. Несогласованное кодирование данных

Если часть данных передаётся как UTF-8, а часть как hex без унификации, результат будет некорректным.


Интеграция с Streams API

В современных приложениях SJCL часто используется вместе с потоками:

const sha = new sjcl.hash.sha256();

const stream = file.stream().getReader();

function read() {
    stream.read().then(({ done, value }) => {
        if (done) {
            const digest = sha.finalize();
            return;
        }

        sha.update(arrayBufferToBits(value));
        read();
    });
}

read();

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


Использование HMAC в потоковом режиме

HMAC в SJCL также поддерживает инкрементальное обновление:

const key = sjcl.codec.utf8String.toBits("secret");
const hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);

hmac.update(sjcl.codec.utf8String.toBits("part1"));
hmac.update(sjcl.codec.utf8String.toBits("part2"));

const mac = hmac.digest();

Принцип идентичен хэшированию: данные подаются частями, результат фиксируется в конце.


Обработка нестандартных потоков данных

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

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

SJCL не навязывает транспортный слой, поэтому ответственность за корректную потоковую сборку лежит на уровне приложения.


Роль bitArray в потоковой архитектуре

bitArray выступает промежуточным универсальным форматом:

  • позволяет объединять разные типы данных
  • обеспечивает побитовую точность
  • совместим со всеми хэш-функциями SJCL

Это делает возможной единообразную обработку текста, бинарных данных и структурированных сообщений в одной цепочке вычислений.