Практика: шифрование файла в браузере

Работа с файлами в браузере опирается на объектную модель File API и бинарные структуры ArrayBuffer/TypedArray, поскольку Web Crypto API не работает со строками напрямую и требует строго бинарного представления данных.

Любой файл, полученный через <input type="file">, представляет собой объект File, который является наследником Blob. Для криптографических операций файл необходимо преобразовать в ArrayBuffer.

Чаще всего используется FileReader:

const file = input.files[0];

const reader = new FileReader();
reader.onl oad = function () {
    const arrayBuffer = reader.result;
};
reader.readAsArrayBuffer(file);

Альтернативный современный подход — использование file.arrayBuffer():

const arrayBuffer = await file.arrayBuffer();

Этот вариант предпочтителен, так как избавляет от колбэков и упрощает поток обработки.

Выбор алгоритма шифрования

Web Crypto API предоставляет ограниченный набор безопасных алгоритмов. Для шифрования файлов в браузере стандартным решением выступает AES-GCM:

  • симметричное шифрование
  • высокая производительность
  • встроенная аутентификация данных
  • защита от подмены (tamper detection)

Основной интерфейс — crypto.subtle.

Генерация ключа шифрования

Ключ может быть сгенерирован автоматически:

const key = await crypto.subtle.generateKey(
    {
        name: "AES-GCM",
        length: 256
    },
    true,
    ["encrypt", "decrypt"]
);
  • length: 256 — рекомендуемый уровень стойкости
  • extractable: true позволяет экспортировать ключ при необходимости
  • ["encrypt", "decrypt"] задаёт разрешённые операции

Инициализирующий вектор (IV)

AES-GCM требует уникальный IV для каждого шифрования. Его нельзя повторно использовать с одним ключом.

Обычно применяется 12-байтовый случайный вектор:

const iv = crypto.getRandomValues(new Uint8Array(12));

IV не является секретом, но должен сохраняться вместе с зашифрованными данными.

Шифрование файла

После подготовки данных выполняется шифрование через crypto.subtle.encrypt:

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

Результатом является ArrayBuffer, содержащий зашифрованные данные.

Формирование итогового контейнера данных

Поскольку для расшифрования необходим IV, он обычно сохраняется вместе с шифротекстом.

Простейшая схема упаковки:

[ IV (12 байт) ][ encrypted data ]

Реализация:

const ivAndData = new Uint8Array(iv.byteLength + encryptedBuffer.byteLength);

ivAndData.set(iv, 0);
ivAndData.set(new Uint8Array(encryptedBuffer), iv.byteLength);

Скачивание зашифрованного файла

Для сохранения результата используется Blob:

const blob = new Blob([ivAndData], { type: "application/octet-stream" });

const url = URL.createObjectURL(blob);

const a = document.createElement("a");
a.href = url;
a.download = "encrypted.bin";
a.click();

URL.revokeObjectURL(url);

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

Расшифрование файла

При обратной операции сначала извлекается IV и данные:

const combined = new Uint8Array(await encryptedFile.arrayBuffer());

const iv = combined.slice(0, 12);
const data = combined.slice(12);

Затем выполняется расшифрование:

const decryptedBuffer = await crypto.subtle.decrypt(
    {
        name: "AES-GCM",
        iv: iv
    },
    key,
    data
);

Результат снова представляет собой ArrayBuffer, который можно преобразовать обратно в Blob:

const blob = new Blob([decryptedBuffer]);

Хранение ключа и проблема его устойчивости

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

  • экспорт ключа и сохранение (IndexedDB)
  • генерация ключа из пароля пользователя
  • использование PBKDF2

Экспорт ключа:

const exported = await crypto.subtle.exportKey("raw", key);

Получение ключа из пароля (PBKDF2)

Для пользовательских паролей применяется PBKDF2 с солью:

const encoder = new TextEncoder();

const baseKey = await crypto.subtle.importKey(
    "raw",
    encoder.encode(password),
    "PBKDF2",
    false,
    ["deriveKey"]
);

Генерация соли:

const salt = crypto.getRandomValues(new Uint8Array(16));

Производный ключ:

const derivedKey = await crypto.subtle.deriveKey(
    {
        name: "PBKDF2",
        salt: salt,
        iterations: 100000,
        hash: "SHA-256"
    },
    baseKey,
    {
        name: "AES-GCM",
        length: 256
    },
    false,
    ["encrypt", "decrypt"]
);

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

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

Web Crypto API работает с целыми буферами, поэтому большие файлы требуют внимания:

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

Практический подход:

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

Типовые ошибки при работе с Web Crypto API

Криптографические операции часто приводят к ошибкам, связанным с типами данных:

  1. Передача строки вместо ArrayBuffer Все входные данные должны быть бинарными.

  2. Повторное использование IV Нарушает безопасность AES-GCM и может привести к компрометации ключа.

  3. Неверная длина ключа AES-GCM требует строго 128/192/256 бит.

  4. Потеря IV при хранении Без IV расшифрование невозможно.

  5. Использование неподдерживаемых алгоритмов Web Crypto API работает только с ограниченным набором проверенных алгоритмов.

Организация полного цикла обработки файла

Типовой поток выглядит как последовательность операций:

  1. выбор файла через input
  2. чтение в ArrayBuffer
  3. генерация IV
  4. шифрование через AES-GCM
  5. упаковка IV + ciphertext
  6. сохранение Blob
  7. при расшифровании — обратное разделение и decrypt

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