97 lines
3 KiB
JavaScript
97 lines
3 KiB
JavaScript
/**
|
|
* Transient.chat - Encryption First Engine
|
|
* Zero-Knowledge End-to-End Encryption (E2EE)
|
|
* All chat payloads (text & voice audio) are encrypted client-side using AES-256-GCM.
|
|
* Server memory stores strictly ephemeral encrypted ciphertext and IVs.
|
|
*/
|
|
(function() {
|
|
// Helper: string to ArrayBuffer (UTF-8)
|
|
function strToBuf(str) {
|
|
return new TextEncoder().encode(str);
|
|
}
|
|
|
|
// Helper: ArrayBuffer to string (UTF-8)
|
|
function bufToStr(buf) {
|
|
return new TextDecoder().decode(buf);
|
|
}
|
|
|
|
// Helper: ArrayBuffer to hex string
|
|
function bufToHex(buf) {
|
|
return Array.from(new Uint8Array(buf))
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
// Helper: hex string to ArrayBuffer
|
|
function hexToBuf(hex) {
|
|
if (!hex) return new Uint8Array(0).buffer;
|
|
const bytes = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
}
|
|
return bytes.buffer;
|
|
}
|
|
|
|
/**
|
|
* Derives a 256-bit AES-GCM key from the room password and room name.
|
|
* Uses PBKDF2 with SHA-256 and 100,000 iterations with a room-bound salt.
|
|
*/
|
|
window.deriveRoomKey = async function(password, roomName) {
|
|
const passwordBuffer = strToBuf(password);
|
|
const saltBuffer = strToBuf("transient.chat.room." + roomName);
|
|
|
|
const baseKey = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
passwordBuffer,
|
|
"PBKDF2",
|
|
false,
|
|
["deriveKey"]
|
|
);
|
|
|
|
return await window.crypto.subtle.deriveKey(
|
|
{
|
|
name: "PBKDF2",
|
|
salt: saltBuffer,
|
|
iterations: 100000,
|
|
hash: "SHA-256"
|
|
},
|
|
baseKey,
|
|
{ name: "AES-GCM", length: 256 },
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Encrypts a message payload object (text + optional audio) using the derived room key.
|
|
* Returns hex-encoded ciphertext and 96-bit (12-byte) initialization vector (IV).
|
|
*/
|
|
window.encryptRoomPayload = async function(cryptoKey, payloadObj) {
|
|
const jsonStr = JSON.stringify(payloadObj);
|
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
|
{ name: "AES-GCM", iv: iv },
|
|
cryptoKey,
|
|
strToBuf(jsonStr)
|
|
);
|
|
|
|
return {
|
|
ciphertext: bufToHex(encryptedBuffer),
|
|
iv: bufToHex(iv)
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Decrypts ciphertext and IV hex strings back to the original payload object.
|
|
*/
|
|
window.decryptRoomPayload = async function(cryptoKey, ciphertextHex, ivHex) {
|
|
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
{ name: "AES-GCM", iv: hexToBuf(ivHex) },
|
|
cryptoKey,
|
|
hexToBuf(ciphertextHex)
|
|
);
|
|
|
|
return JSON.parse(bufToStr(decryptedBuffer));
|
|
};
|
|
})();
|