transient.chat/static/assets/js/crypto.js

152 lines
4.5 KiB
JavaScript
Raw Permalink Normal View History

(function() {
let keyring = {}; // { keyId: { passphrase, key } }
let activeKeyId = null;
// Helper: string to array buffer
function strToBuf(str) {
return new TextEncoder().encode(str);
}
// Helper: array buffer to string
function bufToStr(buf) {
return new TextDecoder().decode(buf);
}
// Helper: array buffer 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 array buffer
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;
}
// Short hash of passphrase to identify the key (non-reversible identifier)
async function computeKeyId(passphrase) {
const msgUint8 = strToBuf(passphrase);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').substring(0, 8);
}
// Derive PBKDF2 key from passphrase
async function deriveKey(passphrase, roomName) {
const passwordBuffer = strToBuf(passphrase);
const saltBuffer = strToBuf("transient.chat.salt." + 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"]
);
}
// Add a passphrase to the keyring
window.addKeyToRing = async function(passphrase, roomName) {
try {
const keyId = await computeKeyId(passphrase);
const cryptoKey = await deriveKey(passphrase, roomName);
keyring[keyId] = {
passphrase: passphrase,
key: cryptoKey
};
if (!activeKeyId) {
activeKeyId = keyId;
}
return keyId;
} catch (e) {
console.error("Failed to add key to ring:", e);
return null;
}
};
window.removeKeyFromRing = function(keyId) {
delete keyring[keyId];
if (activeKeyId === keyId) {
const remaining = Object.keys(keyring);
activeKeyId = remaining.length > 0 ? remaining[0] : null;
}
};
window.setActiveKey = function(keyId) {
if (keyring[keyId]) {
activeKeyId = keyId;
return true;
}
return false;
};
window.getActiveKeyId = function() {
return activeKeyId;
};
window.getKeyringIds = function() {
return Object.keys(keyring);
};
window.getKeyring = function() {
// Return passive list of { keyId, passphrase }
const list = [];
for (const [id, data] of Object.entries(keyring)) {
list.push({ keyId: id, passphrase: data.passphrase });
}
return list;
};
window.isCryptoEnabled = function() {
return activeKeyId !== null;
};
window.encryptData = async function(plaintext, keyId = activeKeyId) {
if (!keyId || !keyring[keyId]) {
throw new Error("Key not found in keyring");
}
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
keyring[keyId].key,
strToBuf(plaintext)
);
return {
ciphertext: bufToHex(encrypted),
iv: bufToHex(iv),
keyId: keyId
};
};
window.decryptData = async function(ciphertextHex, ivHex, keyId) {
if (!keyId || !keyring[keyId]) {
throw new Error("Key not found in keyring");
}
const decrypted = await window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: hexToBuf(ivHex) },
keyring[keyId].key,
hexToBuf(ciphertextHex)
);
return bufToStr(decrypted);
};
})();