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

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

В Web Crypto API вычисление контрольных сумм реализовано через метод subtle.digest, который поддерживает алгоритмы семейства SHA.


Поддерживаемые алгоритмы хеширования

Web Crypto API предоставляет доступ к следующим алгоритмам:

  • SHA-1 — устаревший, не рекомендуется для безопасности
  • SHA-256 — стандартный и широко используемый
  • SHA-384
  • SHA-512 — более длинный и устойчивый

Для проверки целостности файлов чаще всего используется SHA-256.


Базовый пример вычисления хеша

Метод crypto.subtle.digest принимает:

  • название алгоритма
  • данные в виде ArrayBuffer или TypedArray
async function hashData(data) {
  const encoder = new TextEncoder();
  const buffer = encoder.encode(data);

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

  return bufferToHex(hashBuffer);
}

function bufferToHex(buffer) {
  const bytes = new Uint8Array(buffer);
  return [...bytes]
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");
}

Работа с файлами через input

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

<input type="file" id="fileInput">

Чтение файла:

const input = document.getElementById("fileInput");

input.addEventListener("change", async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  const arrayBuffer = await file.arrayBuffer();

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

  const hashHex = bufferToHex(hashBuffer);

  console.log("SHA-256:", hashHex);
});

Особенности работы с большими файлами

Метод digest работает только с целым массивом данных, что означает:

  • файл полностью загружается в память
  • нет потоковой обработки (streaming)

Это создаёт ограничения:

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

Подход с чанками (частями)

Хотя Web Crypto API не поддерживает инкрементальное хеширование напрямую, можно реализовать обработку файла по частям:

  1. Читать файл кусками (Blob.slice)
  2. Конкатенировать данные
  3. Передавать итоговый буфер в digest

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

Альтернативный подход — использовать сторонние реализации (например, pure JS или WASM), поддерживающие потоковое хеширование.


Преобразование результата

Результат digest — это ArrayBuffer. Его нужно преобразовать:

В hex-строку

function bufferToHex(buffer) {
  return [...new Uint8Array(buffer)]
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");
}

В Base64

function bufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = "";

  bytes.forEach(b => binary += String.fromCharCode(b));

  return btoa(binary);
}

Проверка целостности файла

Сравнение контрольной суммы:

async function verifyFile(file, expectedHash) {
  const buffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
  const hash = bufferToHex(hashBuffer);

  return hash === expectedHash;
}

Использование в реальных сценариях

Проверка скачанных файлов

  • сравнение с заранее известным хешем
  • защита от повреждений и подмены

Контроль версий

  • определение, изменился ли файл
  • кеширование на клиенте

Безопасность

  • проверка integrity (например, при загрузке конфигураций)
  • защита от атак через изменённые ресурсы

Работа с Drag & Drop

const dropZone = document.getElementById("dropZone");

dropZone.addEventListener("drop", async (event) => {
  event.preventDefault();

  const file = event.dataTransfer.files[0];
  const buffer = await file.arrayBuffer();

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

  console.log(bufferToHex(hashBuffer));
});

Ограничения и нюансы

1. Асинхронность Все операции выполняются через Promise, что важно учитывать при проектировании UI.

2. Отсутствие потоковой обработки Нельзя обрабатывать данные по мере поступления.

3. Ограничения памяти Браузер может ограничить размер доступного буфера.

4. Поддержка браузерами Web Crypto API поддерживается большинством современных браузеров, но требует HTTPS.


Производительность

Факторы, влияющие на скорость:

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

SHA-256 обычно является хорошим балансом между скоростью и безопасностью.


Интеграция с UI

Пример с отображением прогресса:

async function hashFileWithProgress(file, onProgress) {
  const chunkSize = 1024 * 1024 * 2; // 2MB
  let offset = 0;
  let chunks = [];

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

    offset += chunkSize;
    onProgress(offset / file.size);
  }

  const fullBuffer = concatenateBuffers(chunks);

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

  return bufferToHex(hashBuffer);
}

function concatenateBuffers(buffers) {
  let totalLength = buffers.reduce((sum, b) => sum + b.byteLength, 0);

  let result = new Uint8Array(totalLength);
  let offset = 0;

  buffers.forEach(buffer => {
    result.set(new Uint8Array(buffer), offset);
    offset += buffer.byteLength;
  });

  return result.buffer;
}

Альтернативы для потокового хеширования

Когда требуется обработка очень больших файлов:

  • WebAssembly-библиотеки (например, SHA-256 реализация)
  • сторонние JS-библиотеки с incremental API
  • использование Worker-потоков для разгрузки UI

Использование Web Workers

Вычисление хеша может блокировать основной поток. Перенос в Worker:

// main.js
const worker = new Worker("worker.js");

worker.postMessage(file);

worker.onmess age = (e) => {
  console.log("Hash:", e.data);
};
// worker.js
self.onmess age = async (e) => {
  const file = e.data;

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

  const hash = [...new Uint8Array(hashBuffer)]
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");

  self.postMessage(hash);
};

Практические рекомендации

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

Связь с другими механизмами безопасности

Контрольные суммы дополняют:

  • Subresource Integrity (SRI)
  • цифровые подписи
  • TLS

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


Расширенные сценарии

Хеширование нескольких файлов

async function hashMultiple(files) {
  const results = [];

  for (const file of files) {
    const buffer = await file.arrayBuffer();
    const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);

    results.push({
      name: file.name,
      hash: bufferToHex(hashBuffer)
    });
  }

  return results;
}

Создание карты файлов

const fileMap = new Map();

fileMap.set(hash, file.name);

Диагностика ошибок

Возможные проблемы:

  • NotSupportedError — неподдерживаемый алгоритм
  • DataError — некорректные данные
  • ошибки памяти при больших файлах

Проверка:

if (!crypto.subtle) {
  throw new Error("Web Crypto API не поддерживается");
}

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

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

  1. Получение файла (input / drag&drop)
  2. Преобразование в ArrayBuffer
  3. Вычисление хеша через subtle.digest
  4. Преобразование результата
  5. Сравнение или отображение

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