Представляем browser-file-crypto.

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

Минкю Ли
Минкю Ли
January 2, 2026
3мин чтения
Представляем browser-file-crypto.

Мы открываем исходный код шифрования из функции безопасного обмена TimeFile.

Шифрование файлов в браузере оказывается куда более хлопотным, чем кажется. Если использовать Web Crypto API напрямую, нужно генерировать salt, создавать IV, выводить ключ... и каждый раз писать одинаковые 100+ строк шаблонного кода. В TimeFile мы сделали для этого внутренний модуль. Затем привели его в порядок и выложили в open source, чтобы он был полезен и другим.


Установка

npm
Bash
1npm install @time-file/browser-file-crypto
pnpm
Bash
1pnpm add @time-file/browser-file-crypto
yarn
Bash
1yarn add @time-file/browser-file-crypto

Использование

TypeScript
1import { encryptFile, decryptFile } from '@time-file/browser-file-crypto';
2
3// Шифрование
4const encrypted = await encryptFile(file, { password: 'secret' });
5
6// Расшифровка
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:

JavaScript
1const salt = crypto.getRandomValues(new Uint8Array(16));
2const iv = crypto.getRandomValues(new Uint8Array(12));
3
4const keyMaterial = await crypto.subtle.importKey(
5 "raw",
6 new TextEncoder().encode(password),
7 "PBKDF2",
8 false,
9 ["deriveKey"],
10);
11
12const 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);
24
25const ciphertext = await crypto.subtle.encrypt(
26 { name: "AES-GCM", iv },
27 key,
28 data,
29);
30
31// Этого недостаточно: для последующей расшифровки нужно добавить salt и IV
32const 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.

TypeScript
1const encrypted = await encryptFile(file, { password: 'my-password' });
2const decrypted = await decryptFile(encrypted, { password: 'my-password' });

Шифрование с keyfile

Вместо пароля можно использовать случайный 256-битный ключ. Это безопаснее, чем пароль.

TypeScript
1import { generateKeyFile, encryptFile } from '@time-file/browser-file-crypto';
2
3const keyFile = generateKeyFile();
4// keyFile.key содержит 256-битный ключ, закодированный в base64
5
6const encrypted = await encryptFile(file, { keyData: keyFile.key });

В TimeFile этот ключ можно скачать как файл .tfkey и позже загрузить для расшифровки.

Колбэк прогресса

Хотите показывать progress bar при шифровании больших файлов? Используйте onProgress.

TypeScript
1await encryptFile(file, {
2 password: "secret",
3 onProgress: ({ phase, progress }) => {
4 // phase: 'deriving_key' | 'encrypting' | 'decrypting' | 'complete'
5 updateProgressBar(progress);
6 },
7});

Определение типа шифрования

Можно определить, был ли файл зашифрован паролем или keyfile.

TypeScript
1import { getEncryptionType } from "@time-file/browser-file-crypto";
2
3const type = getEncryptionType(encryptedBlob);
4// 'password' | 'keyfile' | 'unknown'

В TimeFile мы используем это для показа разного UI расшифровки: поле пароля для файлов с паролем или кнопку загрузки keyfile для файлов, зашифрованных ключом.

Обработка ошибок

Если пароль неверный или файл поврежден, вы получите CryptoError.

TypeScript
1import { CryptoError } from "@time-file/browser-file-crypto";
2
3try {
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 — это аутентифицированное шифрование, поэтому оно позволяет обнаруживать подмену данных. Если кто-то изменит зашифрованный файл, расшифровка завершится ошибкой.

Формат файла

Вот как выглядит структура зашифрованного файла:

Структура зашифрованного файла
Для шифрования с keyfile не нужен вывод ключа, поэтому salt отсутствует. Из-за этого заголовок короче на 16 байт.

Размер бандла

Около 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.


Возможности
Подвал