Представляем browser-file-crypto.
Мы открываем исходный код клиентского модуля шифрования файлов, который разработали для функции безопасного обмена в TimeFile.

Мы открываем исходный код шифрования из функции безопасного обмена TimeFile.
Шифрование файлов в браузере оказывается куда более хлопотным, чем кажется. Если использовать Web Crypto API напрямую, нужно генерировать salt, создавать IV, выводить ключ... и каждый раз писать одинаковые 100+ строк шаблонного кода. В TimeFile мы сделали для этого внутренний модуль. Затем привели его в порядок и выложили в open source, чтобы он был полезен и другим.
Установка
1npm install @time-file/browser-file-crypto
1pnpm add @time-file/browser-file-crypto
1yarn add @time-file/browser-file-crypto
Использование
1import { encryptFile, decryptFile } from '@time-file/browser-file-crypto';23// Шифрование4const encrypted = await encryptFile(file, { password: 'secret' });56// Расшифровка7const decrypted = await decryptFile(encrypted, { password: 'secret' });
Почему мы это сделали
В TimeFile есть функция безопасного обмена. Когда вы загружаете файл с паролем, на наших серверах хранится только зашифрованная версия. Скачать и расшифровать файл может только тот, кто знает пароль. Чтобы это работало, файл должен шифроваться на клиенте. Сервер никогда не должен видеть открытый текст. Сначала мы посмотрели на crypto-js, но проект больше не поддерживается. К тому же внутри используется Math.random(), что небезопасно. Проверили и aws-crypto, но там проблемы с совместимостью в Safari, а размер бандла больше 200KB. Для простой задачи шифрования файлов это избыточно. В итоге мы реализовали свое решение на Web Crypto API.
Как выглядит "сырой" Web Crypto API
Для справки, вот какой код нужно писать при прямом использовании Web Crypto API:
1const salt = crypto.getRandomValues(new Uint8Array(16));2const iv = crypto.getRandomValues(new Uint8Array(12));34const keyMaterial = await crypto.subtle.importKey(5 "raw",6 new TextEncoder().encode(password),7 "PBKDF2",8 false,9 ["deriveKey"],10);1112const key = await crypto.subtle.deriveKey(13 {14 name: "PBKDF2",15 salt: salt.buffer,16 iterations: 100000,17 hash: "SHA-256",18 },19 keyMaterial,20 { name: "AES-GCM", length: 256 },21 false,22 ["encrypt"],23);2425const ciphertext = await crypto.subtle.encrypt(26 { name: "AES-GCM", iv },27 key,28 data,29);3031// Этого недостаточно: для последующей расшифровки нужно добавить salt и IV32const result = new Uint8Array(1 + 16 + 12 + ciphertext.byteLength);33result[0] = 0x01; // маркер34result.set(salt, 1);35result.set(iv, 17);36result.set(new Uint8Array(ciphertext), 29);
И это только шифрование. Расшифровка потребует еще один отдельный кусок кода. С browser-file-crypto все сводится к двум строкам.
Возможности
Шифрование паролем
Базовый сценарий. Вы передаете пароль, библиотека выводит ключ через PBKDF2 и шифрует данные с AES-256-GCM.
1const encrypted = await encryptFile(file, { password: 'my-password' });2const decrypted = await decryptFile(encrypted, { password: 'my-password' });
Шифрование с keyfile
Вместо пароля можно использовать случайный 256-битный ключ. Это безопаснее, чем пароль.
1import { generateKeyFile, encryptFile } from '@time-file/browser-file-crypto';23const keyFile = generateKeyFile();4// keyFile.key содержит 256-битный ключ, закодированный в base6456const encrypted = await encryptFile(file, { keyData: keyFile.key });
В TimeFile этот ключ можно скачать как файл .tfkey и позже загрузить для расшифровки.
Колбэк прогресса
Хотите показывать progress bar при шифровании больших файлов? Используйте onProgress.
1await encryptFile(file, {2 password: "secret",3 onProgress: ({ phase, progress }) => {4 // phase: 'deriving_key' | 'encrypting' | 'decrypting' | 'complete'5 updateProgressBar(progress);6 },7});
Определение типа шифрования
Можно определить, был ли файл зашифрован паролем или keyfile.
1import { getEncryptionType } from "@time-file/browser-file-crypto";23const type = getEncryptionType(encryptedBlob);4// 'password' | 'keyfile' | 'unknown'
В TimeFile мы используем это для показа разного UI расшифровки: поле пароля для файлов с паролем или кнопку загрузки keyfile для файлов, зашифрованных ключом.
Обработка ошибок
Если пароль неверный или файл поврежден, вы получите CryptoError.
1import { CryptoError } from "@time-file/browser-file-crypto";23try {4 await decryptFile(encrypted, { password: "wrong" });5} catch (error) {6 if (error instanceof CryptoError) {7 if (error.code === "INVALID_PASSWORD") {8 alert("Неверный пароль");9 }10 }11}
Среди кодов ошибок: INVALID_PASSWORD, INVALID_KEYFILE, INVALID_ENCRYPTED_DATA и другие.
Технические детали
Спецификация шифрования
- Алгоритм: AES-256-GCM
- Вывод ключа: PBKDF2 (100 000 итераций, SHA-256)
- Salt: 16 байт (генерируется случайно каждый раз)
- IV: 12 байт (генерируется случайно каждый раз)
AES-GCM — это аутентифицированное шифрование, поэтому оно позволяет обнаруживать подмену данных. Если кто-то изменит зашифрованный файл, расшифровка завершится ошибкой.
Формат файла
Вот как выглядит структура зашифрованного файла:

Размер бандла
Около 5KB в gzip. Библиотека легковесная, потому что не имеет внешних зависимостей и использует только Web Crypto API. Для сравнения: crypto-js — 50KB, а aws-crypto — более 200KB.
Поддержка браузеров
Работает в любом браузере с поддержкой Web Crypto API: Chrome, Firefox, Safari, Edge. Также работает в Node.js 18+, где встроен webcrypto.
Что дальше
Сейчас для шифрования весь файл загружается в память. Для небольших файлов это нормально, но для файлов в несколько гигабайт можно упереться в лимиты памяти браузера. В следующей версии мы планируем добавить потоковое шифрование. Также рассматриваем поддержку Web Worker. Сейчас шифрование идет в основном потоке, поэтому при обработке больших файлов интерфейс может на короткое время зависать.
Ссылки
- npm: https://www.npmjs.com/package/@time-file/browser-file-crypto - GitHub: https://github.com/Time-File/browser-file-crypto Отзывы и баг-репорты приветствуются в GitHub Issues.


