Linux OpenSSL + Browser Web Crypto interop

2 min read Original article ↗
const opensslPrefix = new TextEncoder().encode("Salted__"); async function encryptBuffer(data: Uint8Array, password: string) { const salt = crypto.getRandomValues(new Uint8Array(8)); const { iv, key } = await passwordToKeyAndIV(password, salt); const encrypted = await crypto.subtle.encrypt( { name: "AES-CBC", iv: iv }, key, data ); const encryptedArray = new Uint8Array(encrypted); const buffer = new Uint8Array( opensslPrefix.length + salt.length + encryptedArray.length ); buffer.set(opensslPrefix, 0); buffer.set(salt, opensslPrefix.length); buffer.set(encryptedArray, opensslPrefix.length + salt.length); return encodeBase64(buffer); } async function decryptBuffer(workingBuffer: Uint8Array, password: string) { const encryptedBuffer = workingBuffer.slice(opensslPrefix.length); const salt = encryptedBuffer.slice(0, 8); const encrypted = encryptedBuffer.slice(8); const { key, iv } = await passwordToKeyAndIV(password, salt); const decrypted = await crypto.subtle.decrypt( { name: "AES-CBC", iv: iv }, key, encrypted ); return new TextDecoder().decode(decrypted); } async function passwordToKeyAndIV(password: string, salt: Uint8Array) { const baseKey = await crypto.subtle.importKey( "raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, ["deriveBits"] ); // AES-256-CBC which requires a 256-bit key (32 bytes) + 128-bit IV (16 bytes) const keyLength = 32 * 8; // 32 bytes for AES-256 key const ivLength = 16 * 8; // 16 bytes for AES IV const totalLength = keyLength + ivLength; // Total bits needed const derivedBits = await crypto.subtle.deriveBits( { name: "PBKDF2", salt: salt, iterations: 100_000, hash: "SHA-256", }, baseKey, totalLength ); const keyBuffer = derivedBits.slice(0, 32); // First 32 bytes for the key const ivBuffer = derivedBits.slice(32, 48); // Next 16 bytes for the IV const key = await crypto.subtle.importKey( "raw", keyBuffer, { name: "AES-CBC", length: 256 }, false, ["encrypt", "decrypt"] ); return { key, iv: new Uint8Array(ivBuffer) }; } async function encryptText(plaintext: string, password: string) { const encodedText = new TextEncoder().encode(plaintext); return encryptBuffer(encodedText, password); } async function decryptText(encrypted: string, password: string) { return decryptBuffer(decodeBase64(encrypted), password); } function encodeBase64(bytes: Uint8Array): string { return btoa(String.fromCharCode.apply(null, Array.from(bytes))); } function decodeBase64(base64String: string): Uint8Array { return Uint8Array.from(atob(base64String), (c) => c.charCodeAt(0)); }