Zero-dependency file encryption for browsers using Web Crypto API.
English | 한ęµě–´
- Zero-Knowledge - Client-side encryption, server never sees plaintext
- Zero-Dependency - Native Web Crypto API only
- AES-256-GCM - Industry-standard authenticated encryption
- Password & Keyfile - Two modes for different use cases
- Streaming Support - Memory-efficient large file handling (v1.1.0+)
- Progress Callbacks - Track encryption/decryption progress
- TypeScript - Full type definitions
- Tiny - < 5KB gzipped
Web Crypto API is powerful but verbose. You need ~100 lines of boilerplate just to encrypt a file, and it's easy to make critical mistakes.
- ❌ Reusing IV (catastrophic for security)
- ❌ Low PBKDF2 iterations (brute-forceable)
- ❌ Missing salt/IV in output (can't decrypt later)
- ❌ Wrong ArrayBuffer slicing (corrupted data)
This library handles it all.
// ❌ Before - Raw Web Crypto API
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const keyMaterial = await crypto.subtle.importKey(
'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']
);
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const arrayBuffer = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
arrayBuffer
);
const result = new Uint8Array(1 + salt.length + iv.length + ciphertext.byteLength);
result.set([0x01], 0);
result.set(salt, 1);
result.set(iv, 17);
result.set(new Uint8Array(ciphertext), 29);
// ... and decryption is another 30 lines// âś… After - With this library
const encrypted = await encryptFile(file, { password: 'secret' });
const decrypted = await decryptFile(encrypted, { password: 'secret' });Done.
| Feature | crypto-js | @aws-crypto | Web Crypto (direct) | browser-file-crypto |
|---|---|---|---|---|
| Maintained | ❌ Deprecated | ✅ | - | ✅ |
| Bundle size | ~50KB | ~200KB+ | 0 | < 5KB |
| Dependencies | Many | Many | None | None |
| File-focused API | ❌ | ❌ | ✅ | |
| Progress callbacks | ❌ | ❌ | ❌ | ✅ |
| Keyfile mode | ❌ | ❌ | ❌ | ✅ |
| Type detection | ❌ | ❌ | ❌ | ✅ |
| TypeScript | ❌ | ✅ | - | ✅ |
# npm
npm install @time-file/browser-file-crypto
# pnpm
pnpm add @time-file/browser-file-crypto
# yarn
yarn add @time-file/browser-file-cryptoimport { encryptFile, decryptFile } from '@time-file/browser-file-crypto';
// Encrypt
const file = document.querySelector('input[type="file"]').files[0];
const encrypted = await encryptFile(file, {
password: 'my-secret-password',
onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
});
// Decrypt
const decrypted = await decryptFile(encrypted, {
password: 'my-secret-password'
});const encrypted = await encryptFile(file, {
password: 'secret', // OR
keyData: keyFile.key, // use keyfile
onProgress: (p) => {} // optional
});const decrypted = await decryptFile(encrypted, {
password: 'secret', // OR
keyData: keyFile.key,
onProgress: (p) => {}
});Auto-switch between standard and streaming encryption based on file size:
import { encryptFileAuto } from '@time-file/browser-file-crypto';
// Automatically uses streaming for files > 100MB
const encrypted = await encryptFileAuto(file, {
password: 'secret',
autoStreaming: true,
streamingThreshold: 100 * 1024 * 1024, // 100MB (default)
chunkSize: 1024 * 1024, // 1MB chunks for streaming
onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
});
// decryptFile automatically handles both formats!
const decrypted = await decryptFile(encrypted, { password: 'secret' });For large files that don't fit in memory:
import { encryptFileStream, decryptFileStream } from '@time-file/browser-file-crypto';
// Encrypt large file (memory-efficient)
const encryptedStream = await encryptFileStream(largeFile, {
password: 'secret',
chunkSize: 1024 * 1024, // 1MB chunks (default: 64KB)
onProgress: ({ processedBytes, totalBytes }) => {
console.log(`${Math.round(processedBytes / totalBytes * 100)}%`);
}
});
// Convert stream to Blob
const response = new Response(encryptedStream);
const encryptedBlob = await response.blob();
// Decrypt - decryptFile auto-detects streaming format!
const decrypted = await decryptFile(encryptedBlob, { password: 'secret' });
// Or use streaming decryption directly
const decryptedStream = decryptFileStream(encryptedBlob, { password: 'secret' });
const decryptResponse = new Response(decryptedStream);
const decryptedBlob = await decryptResponse.blob();No password to remember:
import { generateKeyFile, downloadKeyFile, parseKeyFile } from '@time-file/browser-file-crypto';
const keyFile = generateKeyFile();
downloadKeyFile(keyFile.key, 'my-secret-key'); // saves .key file (default)
// custom extension
downloadKeyFile(keyFile.key, 'my-secret-key', 'tfkey'); // saves .tfkey file
const encrypted = await encryptFile(file, { keyData: keyFile.key });
// Later, load and use
const content = await uploadedFile.text();
const loaded = parseKeyFile(content);
if (loaded) {
const decrypted = await decryptFile(encrypted, { keyData: loaded.key });
}import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
await getEncryptionType(blob); // 'password' | 'keyfile' | 'password-stream' | 'keyfile-stream' | 'unknown'
await isEncryptedFile(blob); // true | false
generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'import { downloadAndDecrypt, downloadAndDecryptStream } from '@time-file/browser-file-crypto';
// Standard download & decrypt
await downloadAndDecrypt('https://example.com/secret.enc', {
password: 'secret',
fileName: 'document.pdf',
onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
});
// Streaming download & decrypt (v1.1.1+) - for large files
await downloadAndDecryptStream('https://example.com/large-file.enc', {
password: 'secret',
fileName: 'video.mp4',
onProgress: ({ phase, processedBytes }) => console.log(`${phase}: ${processedBytes} bytes`)
});import { isCryptoError } from '@time-file/browser-file-crypto';
try {
await decryptFile(encrypted, { password: 'wrong' });
} catch (error) {
if (isCryptoError(error)) {
// error.code: 'INVALID_PASSWORD' | 'INVALID_KEYFILE' | 'INVALID_ENCRYPTED_DATA'
}
}| Component | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key Derivation | PBKDF2 (SHA-256, 100k iterations) |
| Salt | 16 bytes (random per encryption) |
| IV | 12 bytes (random per encryption) |
| Auth Tag | 16 bytes |
Password-encrypted
=> [0x01] + [salt:16] + [iv:12] + [ciphertext + auth_tag:16]
Keyfile-encrypted
=> [0x02] + [iv:12] + [ciphertext + auth_tag:16]
Password-encrypted (streaming)
=> [0x11] + [version:1] + [chunkSize:4] + [salt:16] + [baseIV:12] + [chunks...]
Keyfile-encrypted (streaming)
=> [0x12] + [version:1] + [chunkSize:4] + [baseIV:12] + [chunks...]
Each streaming chunk:
=> [chunkLength:4] + [ciphertext + auth_tag:16]
- Keys are non-extractable (
extractable: false) - Random IV/salt per encryption = no identical ciphertexts
- AES-GCM = authenticated encryption (tamper detection)
- 100k PBKDF2 iterations = brute-force resistant
| Browser | Version |
|---|---|
| Chrome | 80+ |
| Firefox | 74+ |
| Safari | 14+ |
| Edge | 80+ |
Also works in Node.js 18+, Deno, Cloudflare Workers.
import type {
EncryptOptions,
DecryptOptions,
Progress,
KeyFile,
EncryptionType,
CryptoErrorCode,
// Streaming types (v1.1.0+)
StreamEncryptOptions,
StreamDecryptOptions,
StreamProgress,
// v1.1.1+
AutoEncryptOptions,
DownloadDecryptStreamOptions
} from '@time-file/browser-file-crypto';