Перейти к содержанию
HexaTransfer
Вернуться к блогу
Технические погружения

Web Crypto API: полный учебник по шифрованию файлов

Освойте Web Crypto API для браузерного шифрования файлов. AES-GCM, RSA-OAEP и управление ключами в JavaScript-приложениях.

Web Crypto API позволяет шифровать файлы прямо в браузере через нативные методы window.crypto.subtle без каких-либо внешних библиотек. Для шифрования файлов типичный конвейер: вывод ключа из пароля через PBKDF2 (210 000 итераций, SHA-256), затем шифрование байт файла с AES-GCM с 96-битным IV и 128-битным тегом аутентичности. Публичные ключи используют RSA-OAEP с 4096-битными ключами для оборачивания симметричного ключа. API доступен по HTTPS в каждом современном браузере и выполняется в нативном криптобэкенде, а не в JavaScript.

Почему SubtleCrypto лучше чистых JS-библиотек

window.crypto.subtle вызывает аудированный нативный криптобэкенд браузера — обычно BoringSSL в Chromium или CommonCrypto в Safari. По сравнению с чистыми JS-вариантами вроде CryptoJS или sjcl, SubtleCrypto работает в 30–80 раз быстрее для AES-GCM, избегает временны́х боковых каналов в интерпретаторах JavaScript и доставляет пользователям ноль байт дополнительного кода. Компромисс — Promise-based API, работающий только с объектами ArrayBuffer и CryptoKey, поэтому много времени уходит на переключение между Uint8Array, Blob и ReadableStream. Для файлов свыше 100 МБ этот «сантехнический» код важнее сырой скорости криптографии.

Вывод ключа из пароля через PBKDF2

Никогда не используйте пароль напрямую как AES-ключ. Импортируйте пароль как материал, затем выведите 256-битный ключ:

async function deriveKey(password, salt) {
  const enc = new TextEncoder();
  const material = await crypto.subtle.importKey(
    'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
  );
  return crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
    material,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

Рекомендации OWASP 2026 года требуют минимум 600 000 итераций с SHA-256, хотя 210 000 приемлемо для контекстов низкого риска. Генерируйте свежую 16-байтовую соль для каждого файла через crypto.getRandomValues и храните её вместе с шифртекстом. Argon2id был бы сильнее, но SubtleCrypto его пока не предоставляет.

Шифрование файла с AES-GCM

AES-GCM даёт конфиденциальность и аутентичность за один проход. Критическое правило — никогда не повторять пару (key, IV):

async function encryptFile(file, key) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const plaintext = await file.arrayBuffer();
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv, tagLength: 128 },
    key,
    plaintext
  );
  return { iv, ciphertext };
}

Для файла в 2 ГБ file.arrayBuffer() выделит весь буфер, что часто вызывает краш в мобильном Safari. Разделите файл на чанки по 4 МБ, шифруйте каждый с уникальным IV, выведенным из счётчика с конкатенацией случайного префикса, и добавляйте байт версии и соль в начало, чтобы дешифратор знал, с чем имеет дело.

Потоковая обработка больших файлов через TransformStream

Чтобы избежать переполнения памяти, оберните шифрование в TransformStream и пропустите файл через него:

const chunkSize = 4 * 1024 * 1024;
const encryptor = new TransformStream({
  async transform(chunk, controller) {
    const iv = nextIV(counter++);
    const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, chunk);
    controller.enqueue(new Uint8Array([...iv, ...new Uint8Array(ct)]));
  }
});
await file.stream()
  .pipeThrough(sliceByChunks(chunkSize))
  .pipeThrough(encryptor)
  .pipeTo(uploadSink);

file.stream() возвращает ReadableStream<Uint8Array>, лениво читающий с диска. Слайсер производит чанки фиксированного размера, чтобы теги GCM выравнивались предсказуемо. Пиковое потребление памяти остаётся до 20 МБ даже для загрузки в 10 ГБ.

Оборачивание симметричного ключа с RSA-OAEP

При необходимости передать файл конкретному получателю — сгенерируйте его RSA-keypair один раз и опубликуйте публичный ключ:

const keypair = await crypto.subtle.generateKey(
  { name: 'RSA-OAEP', modulusLength: 4096,
    publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
  true, ['wrapKey', 'unwrapKey']
);

Сгенерируйте AES-GCM ключ для файла, затем оберните его:

const wrapped = await crypto.subtle.wrapKey(
  'raw', fileKey, keypair.publicKey,
  { name: 'RSA-OAEP' }
);

4096-битные RSA-ключи дают примерно 150-битную безопасность до 2030 года согласно NIST SP 800-57. Если нужна прямая секретность или постквантовая стойкость, сочетайте RSA-OAEP с ECDH на P-384 или переходите на ML-KEM (Kyber), когда рабочая группа WebCrypto его включит.

Безопасное хранение ключей в IndexedDB

Объекты CryptoKey по умолчанию неизвлекаемы — это означает, что их можно сохранить в IndexedDB, не раскрывая сырые байты JavaScript:

const db = await openDB('keystore', 1);
await db.put('keys', keypair.privateKey, 'user-signing-key');

Браузеры сериализуют ключ через алгоритм structured clone и хранят реальные байты в криптобэкенде. Скомпрометированный скрипт может вызывать encrypt или decrypt с сохранённым ключом, но не может извлечь его материал. Это реальное укрепление безопасности по сравнению с хранением base64-ключей в localStorage.

Обработка ошибок, которые бросает API

SubtleCrypto выбрасывает OperationError при сбоях аутентифицированного дешифрования — обычно это означает подделку шифртекста, неправильный IV или неверный пароль. DataError выбрасывается при неверной длине входного буфера, NotSupportedError — когда алгоритм не реализован, InvalidAccessError — когда ключ не был импортирован с правильными флагами использования. Всегда оборачивайте дешифрование в try/catch, показывайте нейтральное сообщение «файл не удалось расшифровать» и избегайте утечки информации о том, что именно не так — тег или структура.

Реальные подводные камни

Firefox на Android ограничивает итерации deriveKey примерно 1 миллионом, после чего UI-поток замирает на несколько секунд — запускайте вывод ключа в отдельном Worker. Safari ниже 16.4 не поддерживает crypto.subtle.verify с PSS-заполнением. Chrome throttle-ит вызовы getRandomValues свыше 64 КБ за вызов — используйте цикл, если нужно больше энтропии. Передачи ArrayBuffer через postMessage — без копирования, но отсоединяют оригинал, что многих застаёт врасплох.

HexaTransfer использует именно этот конвейер AES-GCM + PBKDF2 для каждой загрузки: ключи выводятся в Worker, шифртекст стримится в хранилище без того, чтобы сервер когда-либо видел открытый текст. Попробуйте на hexatransfer.com — бесплатно, без регистрации, до 10 ГБ.

Собираем всё вместе

Минимальный зашифрованный поток загрузки: генерация соли и IV через getRandomValues, вывод AES-GCM ключа из пароля пользователя через PBKDF2, потоковая передача файла через TransformStream, шифрующий каждый чанк по 4 МБ, добавление небольшого заголовка с версией, солью и количеством чанков в начало, POST результата на сервер. При скачивании — обратный процесс по чанкам, перехват OperationError как сигнала о неверном пароле или повреждении. Web Crypto API даёт всё необходимое, а нативная реализация браузера превзойдёт любую JavaScript-альтернативу на порядок.

Безопасная отправка больших файлов со сквозным шифрованием

Передавайте файлы до 10 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.

Отправить файл