/** * MAILCRIPTY — primitive crittografiche end-to-end (Web Crypto API). * Tutto gira nel browser: il server vede solo blob cifrati. */ export type EncryptedBlob = { salt: string; iv: string; data: string; }; export type EncryptedMessage = { iv: string; ciphertext: string; encryptedAesKey: string; }; const buf2b64 = (buf: ArrayBuffer | Uint8Array): string => { const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf); let binary = ""; for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]!); return btoa(binary); }; const toArrayBuffer = (u: Uint8Array): ArrayBuffer => { const out = new ArrayBuffer(u.byteLength); new Uint8Array(out).set(u); return out; }; const b642buf = (b64: string): ArrayBuffer => toArrayBuffer(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))); const str2buf = (str: string): ArrayBuffer => toArrayBuffer(new TextEncoder().encode(str)); const buf2str = (buf: ArrayBuffer): string => new TextDecoder().decode(buf); const subtle = () => { if (typeof window === "undefined" || !window.crypto?.subtle) { throw new Error("Crittografia non disponibile in questo contesto"); } return window.crypto.subtle; }; export async function generateKeyPair(): Promise { return subtle().generateKey( { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["encrypt", "decrypt"], ) as Promise; } export async function exportKey(key: CryptoKey): Promise { return JSON.stringify(await subtle().exportKey("jwk", key)); } export async function importPublicKey(jwkStr: string): Promise { return subtle().importKey( "jwk", JSON.parse(jwkStr) as JsonWebKey, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["encrypt"], ); } export async function importPrivateKey(jwkStr: string): Promise { return subtle().importKey( "jwk", JSON.parse(jwkStr) as JsonWebKey, { name: "RSA-OAEP", hash: "SHA-256" }, true, ["decrypt"], ); } async function deriveKeyFromPassword(password: string, salt: BufferSource): Promise { const keyMaterial = await subtle().importKey("raw", str2buf(password), { name: "PBKDF2" }, false, [ "deriveKey", ]); return subtle().deriveKey( { name: "PBKDF2", salt, iterations: 250_000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"], ); } /** Cifra la chiave privata con la Master Password (zero-knowledge). */ export async function encryptPrivateKey( privateKey: CryptoKey, password: string, ): Promise { const salt = window.crypto.getRandomValues(new Uint8Array(16)); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const wrappingKey = await deriveKeyFromPassword(password, salt); const data = str2buf(await exportKey(privateKey)); const encrypted = await subtle().encrypt( { name: "AES-GCM", iv: iv }, wrappingKey, data, ); return { salt: buf2b64(salt), iv: buf2b64(iv), data: buf2b64(encrypted) }; } export async function decryptPrivateKey( blob: EncryptedBlob, password: string, ): Promise { const wrappingKey = await deriveKeyFromPassword(password, b642buf(blob.salt)); const decrypted = await subtle().decrypt( { name: "AES-GCM", iv: b642buf(blob.iv) }, wrappingKey, b642buf(blob.data), ); return importPrivateKey(buf2str(decrypted)); } /** Cifratura ibrida: AES-GCM per il testo, RSA-OAEP per la chiave AES. */ export async function encryptMessage( plaintext: string, recipientPublicKeyJwk: string, ): Promise { const recipientPublicKey = await importPublicKey(recipientPublicKeyJwk); const aesKey = await subtle().generateKey({ name: "AES-GCM", length: 256 }, true, [ "encrypt", "decrypt", ]); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await subtle().encrypt( { name: "AES-GCM", iv: iv }, aesKey, str2buf(plaintext), ); const exportedAesKey = await subtle().exportKey("raw", aesKey); const encryptedAesKey = await subtle().encrypt( { name: "RSA-OAEP" }, recipientPublicKey, exportedAesKey, ); return { iv: buf2b64(iv), ciphertext: buf2b64(ciphertext), encryptedAesKey: buf2b64(encryptedAesKey), }; } export async function decryptMessage( message: EncryptedMessage, privateKey: CryptoKey, ): Promise { const rawAesKey = await subtle().decrypt( { name: "RSA-OAEP" }, privateKey, b642buf(message.encryptedAesKey), ); const aesKey = await subtle().importKey("raw", rawAesKey, { name: "AES-GCM" }, false, [ "decrypt", ]); const plaintext = await subtle().decrypt( { name: "AES-GCM", iv: b642buf(message.iv) }, aesKey, b642buf(message.ciphertext), ); return buf2str(plaintext); } /** Cifra dati binari (allegati) con AES-GCM; la chiave AES viene protetta con RSA per ogni destinatario. */ export async function encryptBytes( data: ArrayBuffer, publicKeysJwk: string[], ): Promise<{ bytes: ArrayBuffer; iv: string; wrappedKeys: string[] }> { const aesKey = await subtle().generateKey({ name: "AES-GCM", length: 256 }, true, [ "encrypt", "decrypt", ]); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const bytes = await subtle().encrypt({ name: "AES-GCM", iv: iv }, aesKey, data); const rawAes = await subtle().exportKey("raw", aesKey); const wrappedKeys: string[] = []; for (const jwk of publicKeysJwk) { const pub = await importPublicKey(jwk); wrappedKeys.push(buf2b64(await subtle().encrypt({ name: "RSA-OAEP" }, pub, rawAes))); } return { bytes, iv: buf2b64(iv), wrappedKeys }; } export async function decryptBytes( bytes: ArrayBuffer, iv: string, wrappedKey: string, privateKey: CryptoKey, ): Promise { const rawAes = await subtle().decrypt({ name: "RSA-OAEP" }, privateKey, b642buf(wrappedKey)); const aesKey = await subtle().importKey("raw", rawAes, { name: "AES-GCM" }, false, ["decrypt"]); return subtle().decrypt({ name: "AES-GCM", iv: b642buf(iv) }, aesKey, bytes); } export function fingerprint(publicKeyJwk: string): string { const jwk = JSON.parse(publicKeyJwk) as { n?: string }; const n = jwk.n ?? ""; return (n.slice(0, 4) + n.slice(-8)) .replace(/[^a-zA-Z0-9]/g, "") .toUpperCase() .match(/.{1,4}/g) ?.join(" ") ?? "—"; }