Introducing browser-file-crypto.

We're open-sourcing the client-side file encryption module we built for TimeFile's secure sharing feature.

Mingyu Lee
Mingyu Lee
January 2, 2026
4min read
Introducing browser-file-crypto.

We're open-sourcing the encryption code from TimeFile's secure sharing feature.

Encrypting files in the browser is more of a hassle than you'd expect. If you use the Web Crypto API directly, you have to generate a salt, create an IV, derive a key... and you end up writing the same 100+ lines of boilerplate every time. TimeFile built an internal module to handle this. We've cleaned it up and open-sourced it, hoping it'll be useful to others.


Installation

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

Usage

TypeScript
1import { encryptFile, decryptFile } from '@time-file/browser-file-crypto';
2
3// Encrypt
4const encrypted = await encryptFile(file, { password: 'secret' });
5
6// Decrypt
7const decrypted = await decryptFile(encrypted, { password: 'secret' });

Why we built this

TimeFile has a secure sharing feature. When you upload a file with a password, only the encrypted version is stored on our servers. Only people who know the password can download and decrypt it. To make this work, the file has to be encrypted on the client. The server should never see the plaintext. We first looked at crypto-js, but it's no longer maintained. It also uses Math.random() internally, which isn't secure. We checked out aws-crypto too, but it has Safari compatibility issues and the bundle is over 200KB. That felt like overkill for just encrypting files. So we ended up building our own using the Web Crypto API.

What raw Web Crypto API looks like

For reference, here's what you'd have to write using the Web Crypto API directly:

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// Not done yet - you need to prepend salt and IV for decryption later
32const result = new Uint8Array(1 + 16 + 12 + ciphertext.byteLength);
33result[0] = 0x01; // marker
34result.set(salt, 1);
35result.set(iv, 17);
36result.set(new Uint8Array(ciphertext), 29);

That's just for encryption. Decryption is another chunk of code. With browser-file-crypto, this becomes two lines.


Features

Password encryption

The most basic usage. Pass a password, and it derives a key using PBKDF2, then encrypts with AES-256-GCM.

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

Keyfile encryption

You can also use a 256-bit random key instead of a password. This is more secure than passwords.

TypeScript
1import { generateKeyFile, encryptFile } from '@time-file/browser-file-crypto';
2
3const keyFile = generateKeyFile();
4// keyFile.key contains the base64-encoded 256-bit key
5
6const encrypted = await encryptFile(file, { keyData: keyFile.key });

At TimeFile, we let users download this key as a .tfkey file, then upload it later for decryption.

Progress callback

Want to show a progress bar when encrypting large files? Use onProgress.

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

Encryption type detection

You can check whether a file was encrypted with a password or a keyfile.

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

TimeFile uses this to show different decryption UIs - a password input for password-encrypted files, or a keyfile upload button for keyfile-encrypted ones.

Error handling

When the password is wrong or the file is corrupted, you'll get a 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("Wrong password");
9 }
10 }
11}

Error codes include INVALID_PASSWORD, INVALID_KEYFILE, INVALID_ENCRYPTED_DATA, and more.


Technical details

Encryption spec

  • Algorithm: AES-256-GCM
  • Key derivation: PBKDF2 (100,000 iterations, SHA-256)
  • Salt: 16 bytes (randomly generated each time)
  • IV: 12 bytes (randomly generated each time)

AES-GCM is authenticated encryption, so it can detect tampering. If someone modifies the encrypted file, decryption will fail.

File format

Here's what the encrypted file structure looks like:

Encrypted file structure
Keyfile encryption doesn't need key derivation, so there's no salt. The header is 16 bytes shorter.

Bundle size

About 5KB gzipped. It's lightweight because there are no external dependencies - just the Web Crypto API. For comparison, crypto-js is 50KB and aws-crypto is over 200KB.

Browser support

Works in any browser that supports the Web Crypto API. Chrome, Firefox, Safari, Edge - all good. Also works in Node.js 18+ since it has webcrypto built in.


What's next

Right now, the entire file is loaded into memory for encryption. Small files are fine, but multi-GB files might run into browser memory limits. We're planning to add streaming encryption in the next version. We're also considering Web Worker support. Currently encryption runs on the main thread, so the UI can freeze momentarily when processing large files.


- npm: https://www.npmjs.com/package/@time-file/browser-file-crypto - GitHub: https://github.com/Time-File/browser-file-crypto Feedback and bug reports are welcome on GitHub Issues.


Features
Footer