Шифрование файлов на JavaScript: пошаговый учебник
Шифруйте файлы в браузере с помощью JavaScript. Практический учебник по AES-шифрованию, выводу ключей и безопасной работе с файлами.
JavaScript позволяет шифровать файлы полностью в браузере через Web Crypto API без какого-либо участия сервера. Стандартный конвейер: читаем файл как ArrayBuffer, выводим 256-битный ключ AES-GCM из пароля через PBKDF2-SHA256 (600 000 итераций), шифруем со случайным 12-байтовым IV, упаковываем соль, IV и шифртекст в скачиваемый Blob. Все основные браузеры поддерживают это нативно через window.crypto.subtle, и для файлов до 2–3 ГБ весь процесс занимает менее десяти секунд на современном ноутбуке без сторонних библиотек.
Выбор алгоритмов, который имеет значение
Используйте AES-GCM, а не AES-CBC. GCM даёт аутентифицированное шифрование за один проход с 128-битным тегом аутентичности, тогда как CBC требует отдельного шага HMAC, который большинство руководств реализуют неправильно. Используйте 256-битный ключ — разница в производительности против 128-битного ничтожна на железе с AES-NI. Выбирайте PBKDF2-SHA256 для вывода ключа из пароля, если не можете поставить Argon2id через WebAssembly — он сильнее, но добавляет 50 КБ к загрузке.
Избегайте: режима ECB (фундаментально сломан), самодельного дополнения (десятилетие атак CBC padding oracle), MD5 и SHA-1 (атаки на коллизии), и пакета crypto-js без понимания, что он по умолчанию использует CBC с PKCS7.
Чтение файла в память
API File предлагает три способа получить байты:
const buf = await file.arrayBuffer(); // весь файл
const stream = file.stream(); // потоковое чтение
const text = await file.text(); // декодирование UTF-8
Для файлов выше 500 МБ arrayBuffer() часто падает в мобильном Safari. Используйте потоки:
async function* chunks(file, size = 4 * 1024 * 1024) {
for (let off = 0; off < file.size; off += size) {
yield new Uint8Array(await file.slice(off, off + size).arrayBuffer());
}
}
Каждый срез читается лениво с диска, поэтому пиковое потребление памяти остаётся ограниченным.
Вывод ключа из пользовательского пароля
Никогда не передавайте сырой пароль в encrypt. Сначала выводите ключ:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
Генерируйте свежую 16-байтовую соль для каждого файла через crypto.getRandomValues(new Uint8Array(16)). Храните соль вместе с шифртекстом — повторное использование соли между файлами нивелирует смысл PBKDF2. Декабрьская 2026 года памятка OWASP по хранению паролей рекомендует 600 000 итераций для PBKDF2-SHA256, что соответствует примерно 500 мс вывода ключа на среднем телефоне.
Шифрование файла
Имея ключ, шифрование — один вызов subtle.encrypt на буфер:
async function encryptBuffer(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
GCM катастрофически ломается при повторном использовании пары (iv, key) — коллизия потока ключей раскрывает оба открытых текста. 96-битный случайный IV даёт ~2^48 безопасных шифрований одним ключом, что достаточно для шифрования файлов. Если вы шифруете много чанков одним ключом, выводите IV из счётчика плюс случайный 32-битный префикс.
Упаковка результата
Дешифратору нужны соль, IV и шифртекст. Упакуйте их в единый blob с небольшим заголовком:
function pack(salt, iv, ciphertext) {
const magic = new TextEncoder().encode('ENC1');
return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}
Версионируйте заголовок (ENC1, ENC2…), чтобы впоследствии можно было мигрировать алгоритмы без поломки старых файлов. Предложите скачивание через:
const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);
Дешифрование файлов
Дешифрование обращает процесс и бросает OperationError, если пароль неверный или файл был подделан:
async function decryptFile(blob, password) {
const buf = await blob.arrayBuffer();
const view = new Uint8Array(buf);
const magic = new TextDecoder().decode(view.slice(0, 4));
if (magic !== 'ENC1') throw new Error('Unknown format');
const salt = view.slice(4, 20);
const iv = view.slice(20, 32);
const ct = view.slice(32);
const key = await deriveKey(password, salt);
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
return new Blob([pt]);
}
Показывайте одно сообщение об ошибке — «не удалось расшифровать: неверный пароль или повреждённый файл» — без различения между сбоем тега и структурным сбоем. Это устраняет утечку информации атакующим, аналогичную padding-oracle.
Шифрование больших файлов без исчерпания RAM
Для всего выше 500 МБ не вызывайте arrayBuffer() на весь файл. Шифруйте чанки независимо с уникальными IV, выведенными из счётчика:
function ivForChunk(baseIV, index) {
const iv = new Uint8Array(baseIV);
const view = new DataView(iv.buffer);
view.setUint32(8, index, false);
return iv;
}
Прогоняйте файл через TransformStream, шифруйте каждый чанк по 4 МБ и записывайте результаты в WritableStream, указывающий на диск через File System Access API. Пиковое потребление памяти остаётся около 10 МБ даже для файла в 20 ГБ. Чанкованный формат должен записывать размер и количество чанков в заголовке, чтобы дешифратор мог корректно пересобрать данные.
Типичные ошибки, доходящие до продакшена
Три паттерна встречаются в реальных код-ревью JavaScript-крипто:
Во-первых, хранение сырого ключа в localStorage для удобства. localStorage синхронный, ограничен origin и читается любым XSS. Используйте вместо этого неизвлекаемый CryptoKey в IndexedDB.
Во-вторых, использование Math.random() для IV или соли. Math.random() предсказуем; всегда используйте crypto.getRandomValues.
В-третьих, предположение, что subtle.encrypt работает за константное время. В нативных реализациях браузера — да, но любая JavaScript-обёртка вокруг него почти наверняка нет. Держите свой код вне горячего пути.
HexaTransfer применяет именно этот конвейер — PBKDF2 при 600к итерациях, AES-256-GCM, потоковые чанки — так что сервер хранит только непрозрачный шифртекст. Попробуйте на hexatransfer.com — бесплатно, без регистрации, до 10 ГБ.
Тестирование реализации
Напишите тестовый стенд, выполняющий round-trip случайного 10 МБ blob, мутирующий байт и проверяющий, что дешифрование бросает исключение. Добавьте фаззинг против искажённых заголовков и усечённых шифртекстов — типичная ошибка — отсутствие проверок границ в slice(), приводящее к крашу вместо отклонения. Делайте замеры на iPhone SE, среднем Android и Chromebook; всё, что занимает более 2 секунд на вывод ключа, слишком медленно для мобильных пользователей. Перед выводом в продакшен проверьте реализацию с помощью второй пары глаз — криптокод выглядит просто и ломается тонкими способами, которые тесты пропускают.
Безопасная отправка больших файлов со сквозным шифрованием
Передавайте файлы до 10 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.
Отправить файл