Руководство Web Crypto API: нативное шифрование браузера
Освойте Web Crypto API для создания приложений шифрованной передачи. Полное руководство по AES-GCM, RSA-OAEP и управлению ключами.
Web Crypto API (описан в рекомендации W3C Web Cryptography API, доступен через window.crypto.subtle) — это встроенный в браузер способ выполнять криптографические операции без загрузки сторонних библиотек. Поддерживаются AES-GCM, AES-CBC, AES-CTR, AES-KW, HMAC, RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDH, ECDSA, HKDF и PBKDF2 во всех современных браузерах (Chrome 37+, Firefox 34+, Safari 10.1+, Edge 79+). Для приложений передачи файлов это важно: каждый байт шифротекста формируется на стороне клиента ещё до загрузки на сервер, а браузер предоставляет постоянную по времени, проверенную реализацию. Данное руководство охватывает ключевые примитивы для зашифрованной передачи файлов и типичные ошибки первых реализаций.
SubtleCrypto работает асинхронно через Promise
Каждый метод crypto.subtle возвращает Promise. Это сделано намеренно: криптографические операции могут быть перенесены на аппаратный уровень или в фоновый поток, поэтому принудительная асинхронность не позволяет API заблокировать основной поток. Пример кода:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true, // extractable
["encrypt", "decrypt"]
);
Второй аргумент (true) помечает ключ как извлекаемый — его можно экспортировать через crypto.subtle.exportKey(). Для долгоживущих ключей установите значение false, чтобы сырые байты оставались недоступны из JavaScript. Для ключей, которые нужно сериализовать во фрагмент URL (паттерн HexaTransfer), используйте true.
Третий аргумент — массив разрешённых операций. Ключ, созданный с ["encrypt"], нельзя использовать для дешифрования, даже несмотря на симметричность AES-GCM. Такое разделение предотвращает злоупотребление скомпрометированным потоком шифрования для расшифровки ранее накопленных данных.
AES-GCM для симметричного шифрования файлов
AES-GCM — основной алгоритм для содержимого файлов. Он обеспечивает аутентифицированное шифрование со связанными данными (AEAD): шифротекст плюс тег аутентификации плюс опциональные связанные данные, которые аутентифицируются, но не шифруются. Для передачи файлов используйте 256-битный ключ и 96-битный nonce согласно рекомендациям NIST SP 800-38D.
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-битный nonce
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
Результат включает 128-битный тег аутентификации GCM, добавленный к шифротексту. Дешифрование автоматически проверяет тег и выбрасывает исключение при несоответствии. Никогда не повторяйте nonce с тем же ключом: безопасность GCM катастрофически нарушается при повторном использовании nonce — атакующий может восстановить ключ аутентификации. При передаче файлов, где каждый файл получает свежий ключ, случайные nonce безопасны; для долгоживущих ключей используйте счётчик.
PBKDF2 для ключей на основе пароля
Когда пользователь вводит пароль для защиты файла, нельзя использовать пароль напрямую как AES-ключ. Сначала пропустите его через PBKDF2:
const passwordKey = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveKey"]
);
const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 600000,
hash: "SHA-256",
},
passwordKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
Рекомендации OWASP 2023 по хэшированию паролей предписывают 600 000 итераций для PBKDF2-SHA-256. Значения ниже 310 000 не соответствуют актуальным стандартам. Соль должна быть случайной и храниться рядом с шифротекстом (не является секретом, но должна быть уникальной).
Для нового кода в 2026 году стоит рассмотреть Argon2id вместо PBKDF2. Argon2 пока не входит в Web Crypto API, однако библиотеки argon2-browser и @noble/hashes предоставляют реализации на JavaScript/WASM. Argon2id значительно лучше противостоит GPU-атакам по сравнению с PBKDF2.
RSA-OAEP для оборачивания ключей
Для сценариев, где нужно зашифровать AES-ключ файла открытым ключом получателя, используйте RSA-OAEP. Генерация ключевой пары:
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]), // 65537
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
);
Используйте modulusLength 4096 для новых ключей; 2048 ещё допустимо, но начинает устаревать по мере уточнения квантовых угроз. RSA-OAEP шифрует только небольшие полезные нагрузки (не более modulusLength/8 - 2*hashLength - 2 байт), поэтому оборачивайте 256-битный AES-ключ, а не содержимое файла напрямую.
Для производительных приложений ECDH с P-256 или P-384 является лучшей альтернативой RSA. Генерация ключей на порядок быстрее, а размеры ключей значительно меньше.
Потоковая обработка больших файлов
Файл размером 2 ГБ не помещается комфортно в ArrayBuffer браузера. Chrome, Firefox и Safari позволяют читать файлы через File.stream(), возвращающий ReadableStream, с последующей обработкой по частям. Web Crypto API пока не имеет методов потокового шифрования/дешифрования (это пробел в спецификации), поэтому используются два обходных пути:
- Разбивка на чанки (64 КБ или 1 МБ) с шифрованием каждого уникальным nonce. Получатель конкатенирует их по порядку. Это нарушает истинный AEAD для всего файла, но подходит для большинства случаев.
- WASM-библиотека crypto (libsodium.js, @noble/ciphers с WASM-бэкендом), поддерживающая потоковые AEAD-режимы, такие как XChaCha20-Poly1305 или AES-GCM-SIV.
Для передач до нескольких сотен мегабайт буферизованный AES-GCM работает отлично и значительно проще. Свыше этого объёма потоковая обработка становится необходимостью из-за давления на память.
Экспорт, импорт ключей и фрагменты URL
Для потоков в стиле HexaTransfer, где ключ передаётся во фрагменте URL:
const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// Поделитесь ссылкой вида https://example.com/file/abc123#key=keyBase64
Фрагменты URL никогда не отправляются на серверы в HTTP-запросах (браузер их отрезает). Это сохраняет ключ на стороне клиента даже при использовании публичной ссылки. На стороне получателя:
const keyBase64 = window.location.hash.slice(5); // убираем "#key="
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
"raw", rawKey, "AES-GCM", false, ["decrypt"]
);
Используйте кодировку base64url (замените + на -, / на _, уберите дополнение), чтобы избежать проблем с URL-кодированием.
Типичные ошибки
Использование Math.random() для солей или IV. Math.random() криптографически небезопасен. Всегда используйте crypto.getRandomValues().
Повторное использование IV с тем же ключом. Свойства безопасности GCM полностью нарушаются при повторении nonce. Случайные 96-битные nonce сталкиваются примерно после 2^48 шифрований под одним ключом (граница парадокса дней рождений). При передаче файлов с уникальным ключом на файл — безопасно; для долгоживущих ключей используйте счётчик.
Забыть об HTTPS. crypto.subtle доступен только в защищённых контекстах (HTTPS или localhost). В незащищённом источнике crypto.subtle равен undefined.
Хранение извлекаемых ключей в IndexedDB без защиты. Если нужно сохранять ключи, сначала оберните их (например, ключом, производным от парольной фразы). Никогда не храните сырые AES-ключи в localStorage, доступном любому скрипту в источнике.
Доверие паролям без PBKDF2. Сырой пароль в кодировке UTF-8 — это не 256-битный ключ. Всегда выполняйте деривацию.
Пропуск проверки тегов аутентификации. crypto.subtle.decrypt() делает это автоматически для AES-GCM, но при реализации собственных протоколов не пропускайте проверку.
Нюансы поддержки браузерами
Все основные браузеры поддерживают Web Crypto по HTTPS. Некоторые особенности:
- PBKDF2 в Safari был медленнее, чем в Chrome/Firefox на протяжении многих лет; разрыв закрылся в Safari 15.
- Firefox применяет более строгую валидацию входных данных; код, работающий в Chrome, может выбрасывать
OperationErrorв Firefox. Тестируйте в обоих браузерах. - Web Crypto в service workers работает, но требует, чтобы область регистрации была по HTTPS.
- Node.js предоставляет
require("crypto").webcryptoс совместимым API начиная с Node 15, что полезно для изоморфного криптографического кода.
Когда стоит использовать библиотеку
Web Crypto хорошо покрывает основы, но не поддерживает современные примитивы: ChaCha20-Poly1305, Argon2, X25519 и Ed25519 (хотя Ed25519 уже появляется). Для них libsodium.js (через WASM) или @noble/ciphers / @noble/curves (чистый JavaScript, прошедший аудит) — ведущие варианты. HexaTransfer использует примитивы Web Crypto напрямую для AES-GCM и PBKDF2, поскольку они покрывают путь передачи файлов без дополнительных зависимостей.
Для полного потока зашифрованной передачи Web Crypto справляется менее чем за 100 строк кода: генерация AES-ключа, шифрование файла, загрузка шифротекста, публикация ссылки с ключом во фрагменте, импорт ключа получателем и дешифрование. Именно столько и нужно.
Попробуйте на hexatransfer.com — бесплатно, без регистрации, до 10 ГБ.
Безопасная отправка больших файлов со сквозным шифрованием
Передавайте файлы до 10 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.
Отправить файл