Работа с File API и ArrayBuffer

Работа в браузере с криптографическими библиотеками почти всегда упирается в корректную обработку бинарных данных. В случае CryptoJS ключевым внутренним форматом является WordArray, тогда как веб-API оперирует ArrayBuffer, Blob и File. Эти представления необходимо приводить друг к другу, иначе любые операции хеширования, шифрования или подписи файлов будут либо невозможны, либо некорректны по результату.

В браузере файлы, полученные через <input type="file">, drag-and-drop или fetch, представлены объектами File и Blob. Основной способ доступа к их содержимому — чтение в виде ArrayBuffer.

Классический поток работы выглядит так:

  • пользователь выбирает файл
  • браузер предоставляет объект File
  • данные считываются через FileReader
  • результатом становится ArrayBuffer
const fileInput = document.querySelector("input[type='file']");

fileInput.addEventListener("change", (event) => {
    const file = event.target.files[0];

    const reader = new FileReader();

    reader.onl oad = function(e) {
        const arrayBuffer = e.target.result;
        console.log(arrayBuffer);
    };

    reader.readAsArrayBuffer(file);
});

На этом этапе данные уже находятся в бинарном виде, но CryptoJS не работает напрямую с ArrayBuffer. Он использует собственную структуру WordArray.

WordArray как внутренняя модель CryptoJS

CryptoJS хранит данные в виде массива 32-битных слов. Это позволяет эффективно выполнять криптографические операции в JavaScript без работы с сырыми байтами на каждом шаге.

Основная проблема интеграции с Web API заключается в необходимости преобразования:

ArrayBuffer → WordArray

Это ключевой мост между браузерным миром и CryptoJS.

Преобразование ArrayBuffer в WordArray

Типичная реализация конвертации выглядит следующим образом:

function arrayBufferToWordArray(arrayBuffer) {
    const uint8Array = new Uint8Array(arrayBuffer);
    const words = [];

    for (let i = 0; i < uint8Array.length; i++) {
        words[i >>> 2] |= uint8Array[i] << (24 - (i % 4) * 8);
    }

    return CryptoJS.lib.WordArray.create(words, uint8Array.length);
}

Здесь происходит побайтовая упаковка данных в 32-битные слова:

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

Хеширование файлов через CryptoJS

После преобразования данных можно применять любые алгоритмы: SHA-1, SHA-256, MD5 и другие.

Пример вычисления SHA-256 для файла:

function hashFile(file) {
    const reader = new FileReader();

    reader.onl oad = function(e) {
        const arrayBuffer = e.target.result;
        const wordArray = arrayBufferToWordArray(arrayBuffer);

        const hash = CryptoJS.SHA256(wordArray).toString();

        console.log(hash);
    };

    reader.readAsArrayBuffer(file);
}

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

Ограничения FileReader и работа с большими файлами

FileReader.readAsArrayBuffer загружает файл целиком. Это приводит к нескольким ограничениям:

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

CryptoJS сам по себе не поддерживает потоковое хеширование в классическом смысле (как Web Crypto API), поэтому приходится использовать обходные стратегии.

Разбиение файла на чанки

Для работы с большими файлами используется разбиение на блоки (chunking). Каждый блок последовательно добавляется в хеш.

Принцип:

  • читается часть файла
  • преобразуется в WordArray
  • обновляется промежуточное состояние

Однако CryptoJS не предоставляет полноценного streaming API для всех алгоритмов, поэтому часто применяется накопительный подход.

function hashFileByChunks(file, chunkSize = 1024 * 1024) {
    const reader = new FileReader();
    let offset = 0;

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

    reader.onl oad = function(e) {
        const arrayBuffer = e.target.result;
        const wordArray = arrayBufferToWordArray(arrayBuffer);

        sha256.update(wordArray);

        offset += chunkSize;

        if (offset < file.size) {
            readNextChunk();
        } else {
            const hash = sha256.finalize().toString();
            console.log(hash);
        }
    };

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

    readNextChunk();
}

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

Работа с Blob и URL-объектами

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

fetch("/file.bin")
    .then(response => response.blob())
    .then(blob => blob.arrayBuffer())
    .then(arrayBuffer => {
        const wordArray = arrayBufferToWordArray(arrayBuffer);
        const hash = CryptoJS.MD5(wordArray).toString();

        console.log(hash);
    });

Blob предоставляет удобный промежуточный слой, особенно при работе с потоками данных.

Кодирование и текстовые данные внутри ArrayBuffer

Не все данные являются бинарными файлами. Иногда требуется обработка текстов, сериализованных в UTF-8.

В таких случаях используется TextEncoder:

const encoder = new TextEncoder();
const data = encoder.encode("пример строки");

const wordArray = CryptoJS.lib.WordArray.create(data);
const hash = CryptoJS.SHA1(wordArray).toString();

Этот путь обходится без File API, но сохраняет совместимость с CryptoJS.

Типичные ошибки при работе с ArrayBuffer и CryptoJS

Одной из наиболее распространённых проблем является неправильное преобразование байтов:

  • потеря порядка байтов (endianness)
  • неверное смещение при упаковке в слова
  • игнорирование длины исходного массива
  • попытка передать ArrayBuffer напрямую в CryptoJS
// некорректно
CryptoJS.SHA256(arrayBuffer)

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

Сравнение подходов: CryptoJS и Web Crypto API

В контексте File API и ArrayBuffer важно учитывать альтернативу — SubtleCrypto.

  • Web Crypto API работает нативно с ArrayBuffer
  • поддерживает потоковые операции
  • использует аппаратное ускорение
  • требует асинхронного API

CryptoJS:

  • полностью синхронный
  • удобен в чистом JavaScript без зависимостей от браузерных API
  • требует ручного преобразования данных
  • менее эффективен на больших объёмах данных

Пример Web Crypto API:

const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);

Этот вариант значительно проще, но не всегда доступен в старых окружениях или в некоторых ограниченных средах.

Оптимизация работы с бинарными данными

При интенсивной работе с файлами критично учитывать:

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

Особенно важно избегать лишних преобразований:

Blob → ArrayBuffer → Uint8Array → WordArray

Каждый дополнительный шаг увеличивает нагрузку на GC и снижает производительность.

Практический сценарий: проверка целостности загруженного файла

Типичный кейс — проверка контрольной суммы файла перед загрузкой на сервер:

async function verifyFile(file, expectedHash) {
    const buffer = await file.arrayBuffer();
    const wordArray = arrayBufferToWordArray(buffer);

    const hash = CryptoJS.SHA256(wordArray).toString();

    return hash === expectedHash;
}

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