152 lines
4.5 KiB
JavaScript
152 lines
4.5 KiB
JavaScript
|
|
(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);
|
||
|
|
};
|
||
|
|
})();
|