Skip to content

File Encryptor — TypeScript source

Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// File Encryptor - password-based AES-256-GCM file encryption via the Web
// Crypto API. 100% client-side: no data leaves the browser.
//
// Wire format:  salt (16 B) || IV (12 B) || AES-256-GCM ciphertext + tag.
// The 256-bit key is derived from the password with PBKDF2-SHA256 and a fresh
// random salt per encryption, so the same file + password never encrypts to
// the same bytes, and the password itself is never stored or derivable from
// the output.

export const SALT_LENGTH = 16;
export const IV_LENGTH = 12;
export const ITERATIONS = 100_000;

// GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
// encrypted payload is therefore salt + IV + tag = 44 bytes.
const TAG_LENGTH = 16;
const MIN_LENGTH = SALT_LENGTH + IV_LENGTH + TAG_LENGTH;

function subtle(): SubtleCrypto {
  const c = globalThis.crypto;
  if (!c?.subtle) {
    throw new Error('Web Crypto is not available in this browser context.');
  }
  return c.subtle;
}

async function deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
  const material = await subtle().importKey(
    'raw',
    new TextEncoder().encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );
  return subtle().deriveKey(
    { name: 'PBKDF2', salt, iterations: ITERATIONS, hash: 'SHA-256' },
    material,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
}

/** Encrypt `data` under `password`. Returns salt || IV || ciphertext+tag. */
export async function encryptFile(data: ArrayBuffer, password: string): Promise<ArrayBuffer> {
  if (!password) {
    throw new Error('Password must not be empty.');
  }
  if (data.byteLength === 0) {
    throw new Error('Input data is empty - nothing to encrypt.');
  }
  const salt = globalThis.crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_LENGTH));
  const key = await deriveKey(password, salt);
  const ciphertext = new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv }, key, data));
  const out = new Uint8Array(SALT_LENGTH + IV_LENGTH + ciphertext.length);
  out.set(salt, 0);
  out.set(iv, SALT_LENGTH);
  out.set(ciphertext, SALT_LENGTH + IV_LENGTH);
  return out.buffer;
}

/**
 * Decrypt a payload produced by `encryptFile`. Throws when the password is
 * wrong or the payload was corrupted/tampered (GCM auth-tag failure).
 */
export async function decryptFile(data: ArrayBuffer, password: string): Promise<ArrayBuffer> {
  if (!password) {
    throw new Error('Password must not be empty.');
  }
  if (data.byteLength < MIN_LENGTH) {
    throw new Error(
      `Input is too short to be an encrypted file (needs at least ${MIN_LENGTH} bytes: salt + IV + auth tag).`
    );
  }
  const bytes = new Uint8Array(data);
  const salt = bytes.slice(0, SALT_LENGTH);
  const iv = bytes.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
  const ciphertext = bytes.slice(SALT_LENGTH + IV_LENGTH);
  const key = await deriveKey(password, salt);
  try {
    return await subtle().decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
  } catch {
    // A GCM auth-tag failure means the key did not match (wrong password) or
    // the payload was modified after encryption.
    throw new Error('Decryption failed: wrong password or corrupted file.');
  }
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →