From 7145e17c9528cbb2dca40895b5493405675a6b49 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:17:47 +0530 Subject: [PATCH] feat: implement hybrid encryption and compress public key URLs (Fixes #13) --- js/crypto.js | 128 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/js/crypto.js b/js/crypto.js index 82e1379..1cf5948 100644 --- a/js/crypto.js +++ b/js/crypto.js @@ -2,12 +2,44 @@ const ALGO_NAME = 'RSA-OAEP' const ALGO_HASH = 'SHA-256' const ALGO_KEY_LENGTH = 2048 +function bufferToBase64(buffer) { + const bytes = new Uint8Array(buffer); + let binary = ''; + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +function base64ToBuffer(base64) { + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + function serialize_key(key) { + if (key.kty === 'RSA' && key.d === undefined) { + return key.n; + } return btoa(JSON.stringify(key)) } function deserialize_key(str) { - return JSON.parse(atob(str)) + if (str.startsWith('ey')) { + return JSON.parse(atob(str)) + } + return { + kty: "RSA", + n: str, + e: "AQAB", + alg: "RSA-OAEP-256", + ext: true + }; } async function generateKeyPair() { @@ -69,34 +101,112 @@ async function load_key(serialized_key, usage) { } async function load_private_key(public_key_ser) { - return await importJWKey(deserialize_key(localStorage.getItem(pub_key_ser)), 'decrypt') + return await importJWKey(deserialize_key(localStorage.getItem(public_key_ser)), 'decrypt') } async function encryptString(publicKey, plaintext) { + // 1. Generate ephemeral AES key + const aesKey = await window.crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + // 2. Encrypt plaintext with AES-GCM + const iv = window.crypto.getRandomValues(new Uint8Array(12)); const textBuffer = new TextEncoder().encode(plaintext); + const encryptedSecretBuffer = await window.crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: iv, + }, + aesKey, + textBuffer + ); + + // 3. Export raw AES key + const rawAesKey = await window.crypto.subtle.exportKey('raw', aesKey); - const encryptedBuffer = await window.crypto.subtle.encrypt( + // 4. Encrypt raw AES key with RSA-OAEP public key + const encryptedAesKeyBuffer = await window.crypto.subtle.encrypt( { name: ALGO_NAME, }, publicKey, - textBuffer + rawAesKey + ); + + // 5. Pack everything: [RSA Encrypted AES Key (256 bytes) | IV (12 bytes) | AES Ciphertext] + const combinedBuffer = new Uint8Array( + encryptedAesKeyBuffer.byteLength + iv.length + encryptedSecretBuffer.byteLength ); + combinedBuffer.set(new Uint8Array(encryptedAesKeyBuffer), 0); + combinedBuffer.set(iv, encryptedAesKeyBuffer.byteLength); + combinedBuffer.set(new Uint8Array(encryptedSecretBuffer), encryptedAesKeyBuffer.byteLength + iv.length); - return btoa(String.fromCharCode(...new Uint8Array(encryptedBuffer))); + // 6. Base64 encode the combined buffer + return bufferToBase64(combinedBuffer); } async function decryptString(privateKey, encryptedBase64) { - const encryptedBuffer = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0)); + const combinedBytes = new Uint8Array(base64ToBuffer(encryptedBase64)); + + // Backward compatibility check: + // Standard 2048-bit RSA-OAEP ciphertext is exactly 256 bytes. + if (combinedBytes.length === 256) { + const decryptedBuffer = await window.crypto.subtle.decrypt( + { + name: ALGO_NAME, + }, + privateKey, + combinedBytes + ); + return new TextDecoder().decode(decryptedBuffer); + } - const decryptedBuffer = await window.crypto.subtle.decrypt( + if (combinedBytes.length < 284) { + throw new Error('Invalid encrypted payload size'); + } + + const rsaKeyLengthBytes = 256; + const ivLengthBytes = 12; + + const encryptedAesKey = combinedBytes.slice(0, rsaKeyLengthBytes); + const iv = combinedBytes.slice(rsaKeyLengthBytes, rsaKeyLengthBytes + ivLengthBytes); + const encryptedSecret = combinedBytes.slice(rsaKeyLengthBytes + ivLengthBytes); + + // 1. Decrypt raw AES key using RSA private key + const rawAesKey = await window.crypto.subtle.decrypt( { name: ALGO_NAME, }, privateKey, - encryptedBuffer + encryptedAesKey + ); + + // 2. Import raw AES key + const aesKey = await window.crypto.subtle.importKey( + 'raw', + rawAesKey, + { + name: 'AES-GCM', + }, + true, + ['decrypt'] + ); + + // 3. Decrypt the secret payload using AES-GCM + const decryptedBuffer = await window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: iv, + }, + aesKey, + encryptedSecret ); - // Convert the decrypted buffer to a string return new TextDecoder().decode(decryptedBuffer); } \ No newline at end of file