Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 119 additions & 9 deletions js/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}