Codice aperto alla revisione
Modulo crittografico del client
Questo è il codice che cifra e decifra i messaggi nel browser: generazione della coppia di chiavi RSA-OAEP 2048, cifratura ibrida AES-256-GCM, protezione della chiave privata con PBKDF2 a 250.000 iterazioni e cifratura degli allegati. La pagina serve direttamente il file usato in produzione, quindi non può divergere dalla versione in esecuzione: chiunque può verificare che nessuna chiave e nessun testo in chiaro venga inviato al server.
Nota: è pubblicato il modulo crittografico, non l'intera applicazione. Segnalazioni e revisioni a info@mailcripty.com.
/**
* 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<CryptoKeyPair> {
return subtle().generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt", "decrypt"],
) as Promise<CryptoKeyPair>;
}
export async function exportKey(key: CryptoKey): Promise<string> {
return JSON.stringify(await subtle().exportKey("jwk", key));
}
export async function importPublicKey(jwkStr: string): Promise<CryptoKey> {
return subtle().importKey(
"jwk",
JSON.parse(jwkStr) as JsonWebKey,
{ name: "RSA-OAEP", hash: "SHA-256" },
true,
["encrypt"],
);
}
export async function importPrivateKey(jwkStr: string): Promise<CryptoKey> {
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<CryptoKey> {
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<EncryptedBlob> {
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<CryptoKey> {
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<EncryptedMessage> {
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<string> {
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<ArrayBuffer> {
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(" ") ?? "—";
}