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 →