Skip to content

Time-File/browser-file-crypto

Repository files navigation

@time-file/browser-file-crypto

browser-file-crypto

Encryption Flow

Zero-dependency file encryption for browsers using Web Crypto API.

npm version bundle size stars License: MIT

English | 한국어

Features

Features

  • 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

Why?

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.

Comparison

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 ❌ ✅ - ✅

Install

# npm
npm install @time-file/browser-file-crypto

# pnpm
pnpm add @time-file/browser-file-crypto

# yarn
yarn add @time-file/browser-file-crypto

Quick Start

import { 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'
});

API

encryptFile(file, options)

const encrypted = await encryptFile(file, {
  password: 'secret',        // OR
  keyData: keyFile.key,      // use keyfile
  onProgress: (p) => {}      // optional
});

decryptFile(encrypted, options)

const decrypted = await decryptFile(encrypted, {
  password: 'secret',        // OR
  keyData: keyFile.key,
  onProgress: (p) => {}
});

Hybrid Encryption (v1.1.1+)

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' });

Streaming Encryption (v1.1.0+)

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();

Keyfile Mode

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 });
}

Utilities

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!...'

Download & Decrypt

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`)
});

Error Handling

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'
  }
}

Security

Spec

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

File Format

File Format

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]

Notes

  • 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 Support

Browser Version
Chrome 80+
Firefox 74+
Safari 14+
Edge 80+

Also works in Node.js 18+, Deno, Cloudflare Workers.

TypeScript

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';

Links

License

MIT


Made by timefile.co