From 8f28879f18429558ffcdec651ba73a2c9b5a3a34 Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 27 May 2026 23:45:38 +0200 Subject: [PATCH] DM keys!!! --- android/app/src/main/assets/public/index.html | 6 + android/app/src/main/assets/public/main.js | 339 +++++++++++++++++- webroot/index.html | 6 + webroot/main.js | 339 +++++++++++++++++- 4 files changed, 686 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html index b99a1670..b6eb73ec 100644 --- a/android/app/src/main/assets/public/index.html +++ b/android/app/src/main/assets/public/index.html @@ -103,6 +103,12 @@ let res = await Auth(username, password); clearAction("startauth"); if (res.startsWith("success:")) { + try { + await ensureUserKeys(); + } catch (e) { + //if fails continue loading encryption may be broken + console.error(e); + } await refreshDms(); } else { showBlahNotification("error:auth.failed.redirect.to.login"); diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 280befb2..e15f77bd 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -136,6 +136,210 @@ function uint8ToBase64(uint8) { return fixBase64Padding(btoa(String.fromCharCode(...uint8))); } +function concatUint8(a, b) { + const out = new Uint8Array(a.length + b.length); + out.set(a, 0); + out.set(b, a.length); + return out; +} + +async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) { + const enc = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + enc.encode(passwordPlain), + { name: "PBKDF2" }, + false, + ["deriveKey"] + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt, + iterations, + hash: "SHA-256" + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); +} + +async function encryptJsonWithPassword(jsonObj, passwordPlain) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const iterations = 310000; + const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations); + const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj)); + const ciphertextBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext); + const ciphertext = new Uint8Array(ciphertextBuf); + + return JSON.stringify({ + v: 1, + kdf: "PBKDF2-SHA256", + iter: iterations, + alg: "AES-256-GCM", + salt: uint8ToBase64(salt), + iv: uint8ToBase64(iv), + ct: uint8ToBase64(ciphertext) + }); +} + +async function decryptJsonWithPassword(envelopeJson, passwordPlain) { + const env = JSON.parse(envelopeJson); + if (!env || env.v !== 1 || env.kdf !== "PBKDF2-SHA256" || env.alg !== "AES-256-GCM") { + throw new Error("unsupported.key.envelope"); + } + const salt = base64ToUint8(env.salt); + const iv = base64ToUint8(env.iv); + const ct = base64ToUint8(env.ct); + const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter); + const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); + return JSON.parse(new TextDecoder().decode(plainBuf)); +} + +async function sha256Bytes(bytes) { + const hashBuffer = await crypto.subtle.digest("SHA-256", bytes); + return new Uint8Array(hashBuffer); +} + +async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length = 32) { + const info = new TextEncoder().encode(infoString); + const salt = saltBytes ?? new Uint8Array(32); + const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]); + const bits = await crypto.subtle.deriveBits( + { name: "HKDF", hash: "SHA-256", salt, info }, + keyMaterial, + length * 8 + ); + return new Uint8Array(bits); +} + +async function importAesGcmKey(keyBytes) { + return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); +} + +async function encryptAesGcmToBase64(plainText, keyBytes) { + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await importAesGcmKey(keyBytes); + const pt = new TextEncoder().encode(plainText); + const ctBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt); + const combined = new Uint8Array(iv.length + ctBuf.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(ctBuf), iv.length); + return uint8ToBase64(combined); +} + +async function decryptAesGcmFromBase64(cipherBase64, keyBytes) { + const combined = base64ToUint8(cipherBase64); + const iv = combined.slice(0, 12); + const ct = combined.slice(12); + const key = await importAesGcmKey(keyBytes); + const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); + return new TextDecoder().decode(ptBuf); +} + +function randomRoomKey256Chars() { + const bytes = crypto.getRandomValues(new Uint8Array(192)); + return uint8ToBase64(bytes); +} + +async function deriveDmSelfWrapKeyBytes() { + if (!userKeysPrivate) throw new Error("missing.user.private.keys"); + const privX = base64ToUint8(userKeysPrivate.privX25519); + const privM = base64ToUint8(userKeysPrivate.privMlKem); + return await hkdfSha256Bytes(concatUint8(privX, privM), "larpix:dm:selfwrap:v1"); +} + +async function wrapRoomKeyForSelf(roomKey) { + const keyBytes = await deriveDmSelfWrapKeyBytes(); + const enc = await encryptAesGcmToBase64(roomKey, keyBytes); + return `DMK1:${enc}`; +} + +async function unwrapRoomKeyForSelf(wrapped) { + if (!wrapped || !wrapped.startsWith("DMK1:")) throw new Error("unsupported.dm.key.format"); + const keyBytes = await deriveDmSelfWrapKeyBytes(); + return await decryptAesGcmFromBase64(wrapped.substring("DMK1:".length), keyBytes); +} + +async function deriveHybridKeyToUser(targetPublicKeysObj) { + const pubX25519Target = base64ToUint8(targetPublicKeysObj.pubX25519); + const pubMlKemTarget = base64ToUint8(targetPublicKeysObj.pubMlKem); + + const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519); + const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Target); + + const mlkem = new window.MlKem768(); + const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemTarget); + + const combined = concatUint8(secretX25519, secretMlKem); + const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1"); + return { + aesKeyBytes, + ciphertextMlKemBase64: uint8ToBase64(ciphertextMlKem) + }; +} + +async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase64) { + const pubX25519Other = base64ToUint8(otherPublicKeysObj.pubX25519); + const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519); + const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Other); + + const ct = base64ToUint8(ciphertextMlKemBase64); + const privMlKemMine = base64ToUint8(userKeysPrivate.privMlKem); + const mlkem = new window.MlKem768(); + const secretMlKem = await mlkem.decap(ct, privMlKemMine); + + const combined = concatUint8(secretX25519, secretMlKem); + const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1"); + return aesKeyBytes; +} + +async function fetchDmKey(dmId) { + return await fetchEncrypted("dm/key/get", dmId); +} + +async function updateDmKey(dmId, keyValue) { + return await fetchEncrypted( + "dm/key/update", + JSON.stringify({ + string1: dmId, + string2: keyValue + }) + ); +} + +async function ensureDmRoomKey(dmId) { + //returns decrypted roomkey, and converts SETUP key + await ensureUserKeys(); + const raw = await fetchDmKey(dmId); + if (!raw) throw new Error("error:dm.key.missing"); + + if (raw.startsWith("SETUP:")) { + const rest = raw.substring("SETUP:".length); + const parts = rest.split(";"); + if (parts.length < 2) throw new Error("error:dm.key.setup.malformed"); + + const otherPublicJson = parts[0]; + const setupPayloadJson = parts.slice(1).join(";"); + const otherPublic = JSON.parse(otherPublicJson); + const setupPayload = JSON.parse(setupPayloadJson); + + const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem); + const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes); + + const selfWrapped = await wrapRoomKeyForSelf(roomKey); + await updateDmKey(dmId, selfWrapped); + return roomKey; + } + + return await unwrapRoomKeyForSelf(raw); +} + +window.ensureDmRoomKey = ensureDmRoomKey; + async function encrypt(plainText, keyBytes) { const iv = window.crypto.getRandomValues(new Uint8Array(16)); const encoder = new TextEncoder(); @@ -671,6 +875,115 @@ var passwordHash = ""; var host = ""; var lang = getLang(); + +var userKeysPublic = null; //2string json +var userKeysPrivate = null; // decrypted private bundle (JSON object) +var userKeysEncrypted = null; // encrypted private bundle (string envelope JSON) + +function publishUserKeysGlobals() { + window.userKeysPublic = userKeysPublic; + window.userKeysPrivate = userKeysPrivate; + window.userKeysEncrypted = userKeysEncrypted; +} + +async function generateUserKeysBundle() { + // X25519 keypair + const privX25519 = window.x25519.utils.randomSecretKey(); + const pubX25519 = window.x25519.getPublicKey(privX25519); + + // ML-KEM-768 keypair + const mlkem = new window.MlKem768(); + const [pubMlKem, privMlKem] = await mlkem.generateKeyPair(); + + const pub = { + v: 1, + alg: "hybrid-x25519-mlkem768", + pubX25519: uint8ToBase64(pubX25519), + pubMlKem: uint8ToBase64(pubMlKem) + }; + const priv = { + v: 1, + alg: "hybrid-x25519-mlkem768", + privX25519: uint8ToBase64(privX25519), + privMlKem: uint8ToBase64(privMlKem) + }; + return { pub, priv }; +} + +async function ensureUserKeys() { + const lsEncKey = `userKeys.enc.v1:${id}`; + const lsPubKey = `userKeys.pub.v1:${id}`; + + try { //1. check server + const serverKeysRaw = await fetchEncrypted("user/key/get"); + if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) { + const serverKeys = JSON.parse(serverKeysRaw); + const serverEncPriv = serverKeys.string1; + const serverPub = serverKeys.string2; + if (serverEncPriv && serverPub) { + userKeysEncrypted = serverEncPriv; + userKeysPublic = JSON.parse(serverPub); + userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password); + localStorage.setItem(lsEncKey, userKeysEncrypted); + localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic)); + publishUserKeysGlobals(); + return "success:keys.server"; + } + } + } catch (e) { + //ignore, fallback to local + } + + //fallback to local + const localEnc = localStorage.getItem(lsEncKey); + const localPub = localStorage.getItem(lsPubKey); + if (localEnc && localPub) { + try { + userKeysEncrypted = localEnc; + userKeysPublic = JSON.parse(localPub); + userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password); + + //push keys to server + try { + await fetchEncrypted( + "user/key/update", + JSON.stringify({ + string1: userKeysEncrypted, + string2: JSON.stringify(userKeysPublic) + }) + ); + } catch (e) { + + } + + publishUserKeysGlobals(); + return "success:keys.local"; + } catch (e) { + + } + } + + //new keys, encrypt private with user's password + const bundle = await generateUserKeysBundle(); + userKeysPublic = bundle.pub; + userKeysPrivate = bundle.priv; + userKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, password); + + localStorage.setItem(lsEncKey, userKeysEncrypted); + localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic)); + + await fetchEncrypted( + "user/key/update", + JSON.stringify({ + string1: userKeysEncrypted, + string2: JSON.stringify(userKeysPublic) + }) + ); + + publishUserKeysGlobals(); + return "success:keys.generated"; +} + async function mainJS() { await initBlahs(); @@ -1123,10 +1436,32 @@ async function switchInvitesTab(tab) { async function acceptInvite(targetId) { //TODO: Implement key generation try { showAction("action.invite.accepting", "invite.action"); + + await ensureUserKeys(); + + const inviterPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${targetId}`); + const inviterPublicKeys = JSON.parse(inviterPublicKeysRaw); + + //generate room key + const roomKey = randomRoomKey256Chars(); + + //encrypt for self + const selfWrapped = await wrapRoomKeyForSelf(roomKey); + + //encrypt for inviter + const hybrid = await deriveHybridKeyToUser(inviterPublicKeys); + const encForInviter = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes); + const setupPayload = JSON.stringify({ + v: 1, + alg: "hybrid-x25519-mlkem768", + ctMlKem: hybrid.ciphertextMlKemBase64, + enc: encForInviter + }); + let payload = JSON.stringify({ string1: targetId, - string2: "", // TODO: Generate symmetric keys - string3: "" // TODO: Encrypt key for targetId + string2: selfWrapped, + string3: setupPayload }); let res = await fetchEncrypted("user/dm/create", payload); clearAction("invite.action"); diff --git a/webroot/index.html b/webroot/index.html index b99a1670..b6eb73ec 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -103,6 +103,12 @@ let res = await Auth(username, password); clearAction("startauth"); if (res.startsWith("success:")) { + try { + await ensureUserKeys(); + } catch (e) { + //if fails continue loading encryption may be broken + console.error(e); + } await refreshDms(); } else { showBlahNotification("error:auth.failed.redirect.to.login"); diff --git a/webroot/main.js b/webroot/main.js index 280befb2..e15f77bd 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -136,6 +136,210 @@ function uint8ToBase64(uint8) { return fixBase64Padding(btoa(String.fromCharCode(...uint8))); } +function concatUint8(a, b) { + const out = new Uint8Array(a.length + b.length); + out.set(a, 0); + out.set(b, a.length); + return out; +} + +async function deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations) { + const enc = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + enc.encode(passwordPlain), + { name: "PBKDF2" }, + false, + ["deriveKey"] + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt, + iterations, + hash: "SHA-256" + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); +} + +async function encryptJsonWithPassword(jsonObj, passwordPlain) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const iterations = 310000; + const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, iterations); + const plaintext = new TextEncoder().encode(JSON.stringify(jsonObj)); + const ciphertextBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext); + const ciphertext = new Uint8Array(ciphertextBuf); + + return JSON.stringify({ + v: 1, + kdf: "PBKDF2-SHA256", + iter: iterations, + alg: "AES-256-GCM", + salt: uint8ToBase64(salt), + iv: uint8ToBase64(iv), + ct: uint8ToBase64(ciphertext) + }); +} + +async function decryptJsonWithPassword(envelopeJson, passwordPlain) { + const env = JSON.parse(envelopeJson); + if (!env || env.v !== 1 || env.kdf !== "PBKDF2-SHA256" || env.alg !== "AES-256-GCM") { + throw new Error("unsupported.key.envelope"); + } + const salt = base64ToUint8(env.salt); + const iv = base64ToUint8(env.iv); + const ct = base64ToUint8(env.ct); + const key = await deriveAesGcmKeyFromPassword(passwordPlain, salt, env.iter); + const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); + return JSON.parse(new TextDecoder().decode(plainBuf)); +} + +async function sha256Bytes(bytes) { + const hashBuffer = await crypto.subtle.digest("SHA-256", bytes); + return new Uint8Array(hashBuffer); +} + +async function hkdfSha256Bytes(ikmBytes, infoString, saltBytes = null, length = 32) { + const info = new TextEncoder().encode(infoString); + const salt = saltBytes ?? new Uint8Array(32); + const keyMaterial = await crypto.subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveBits"]); + const bits = await crypto.subtle.deriveBits( + { name: "HKDF", hash: "SHA-256", salt, info }, + keyMaterial, + length * 8 + ); + return new Uint8Array(bits); +} + +async function importAesGcmKey(keyBytes) { + return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]); +} + +async function encryptAesGcmToBase64(plainText, keyBytes) { + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await importAesGcmKey(keyBytes); + const pt = new TextEncoder().encode(plainText); + const ctBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, pt); + const combined = new Uint8Array(iv.length + ctBuf.byteLength); + combined.set(iv, 0); + combined.set(new Uint8Array(ctBuf), iv.length); + return uint8ToBase64(combined); +} + +async function decryptAesGcmFromBase64(cipherBase64, keyBytes) { + const combined = base64ToUint8(cipherBase64); + const iv = combined.slice(0, 12); + const ct = combined.slice(12); + const key = await importAesGcmKey(keyBytes); + const ptBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); + return new TextDecoder().decode(ptBuf); +} + +function randomRoomKey256Chars() { + const bytes = crypto.getRandomValues(new Uint8Array(192)); + return uint8ToBase64(bytes); +} + +async function deriveDmSelfWrapKeyBytes() { + if (!userKeysPrivate) throw new Error("missing.user.private.keys"); + const privX = base64ToUint8(userKeysPrivate.privX25519); + const privM = base64ToUint8(userKeysPrivate.privMlKem); + return await hkdfSha256Bytes(concatUint8(privX, privM), "larpix:dm:selfwrap:v1"); +} + +async function wrapRoomKeyForSelf(roomKey) { + const keyBytes = await deriveDmSelfWrapKeyBytes(); + const enc = await encryptAesGcmToBase64(roomKey, keyBytes); + return `DMK1:${enc}`; +} + +async function unwrapRoomKeyForSelf(wrapped) { + if (!wrapped || !wrapped.startsWith("DMK1:")) throw new Error("unsupported.dm.key.format"); + const keyBytes = await deriveDmSelfWrapKeyBytes(); + return await decryptAesGcmFromBase64(wrapped.substring("DMK1:".length), keyBytes); +} + +async function deriveHybridKeyToUser(targetPublicKeysObj) { + const pubX25519Target = base64ToUint8(targetPublicKeysObj.pubX25519); + const pubMlKemTarget = base64ToUint8(targetPublicKeysObj.pubMlKem); + + const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519); + const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Target); + + const mlkem = new window.MlKem768(); + const [ciphertextMlKem, secretMlKem] = await mlkem.encap(pubMlKemTarget); + + const combined = concatUint8(secretX25519, secretMlKem); + const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1"); + return { + aesKeyBytes, + ciphertextMlKemBase64: uint8ToBase64(ciphertextMlKem) + }; +} + +async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase64) { + const pubX25519Other = base64ToUint8(otherPublicKeysObj.pubX25519); + const privX25519Mine = base64ToUint8(userKeysPrivate.privX25519); + const secretX25519 = window.x25519.getSharedSecret(privX25519Mine, pubX25519Other); + + const ct = base64ToUint8(ciphertextMlKemBase64); + const privMlKemMine = base64ToUint8(userKeysPrivate.privMlKem); + const mlkem = new window.MlKem768(); + const secretMlKem = await mlkem.decap(ct, privMlKemMine); + + const combined = concatUint8(secretX25519, secretMlKem); + const aesKeyBytes = await hkdfSha256Bytes(combined, "larpix:dm:roomkeywrap:v1"); + return aesKeyBytes; +} + +async function fetchDmKey(dmId) { + return await fetchEncrypted("dm/key/get", dmId); +} + +async function updateDmKey(dmId, keyValue) { + return await fetchEncrypted( + "dm/key/update", + JSON.stringify({ + string1: dmId, + string2: keyValue + }) + ); +} + +async function ensureDmRoomKey(dmId) { + //returns decrypted roomkey, and converts SETUP key + await ensureUserKeys(); + const raw = await fetchDmKey(dmId); + if (!raw) throw new Error("error:dm.key.missing"); + + if (raw.startsWith("SETUP:")) { + const rest = raw.substring("SETUP:".length); + const parts = rest.split(";"); + if (parts.length < 2) throw new Error("error:dm.key.setup.malformed"); + + const otherPublicJson = parts[0]; + const setupPayloadJson = parts.slice(1).join(";"); + const otherPublic = JSON.parse(otherPublicJson); + const setupPayload = JSON.parse(setupPayloadJson); + + const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem); + const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes); + + const selfWrapped = await wrapRoomKeyForSelf(roomKey); + await updateDmKey(dmId, selfWrapped); + return roomKey; + } + + return await unwrapRoomKeyForSelf(raw); +} + +window.ensureDmRoomKey = ensureDmRoomKey; + async function encrypt(plainText, keyBytes) { const iv = window.crypto.getRandomValues(new Uint8Array(16)); const encoder = new TextEncoder(); @@ -671,6 +875,115 @@ var passwordHash = ""; var host = ""; var lang = getLang(); + +var userKeysPublic = null; //2string json +var userKeysPrivate = null; // decrypted private bundle (JSON object) +var userKeysEncrypted = null; // encrypted private bundle (string envelope JSON) + +function publishUserKeysGlobals() { + window.userKeysPublic = userKeysPublic; + window.userKeysPrivate = userKeysPrivate; + window.userKeysEncrypted = userKeysEncrypted; +} + +async function generateUserKeysBundle() { + // X25519 keypair + const privX25519 = window.x25519.utils.randomSecretKey(); + const pubX25519 = window.x25519.getPublicKey(privX25519); + + // ML-KEM-768 keypair + const mlkem = new window.MlKem768(); + const [pubMlKem, privMlKem] = await mlkem.generateKeyPair(); + + const pub = { + v: 1, + alg: "hybrid-x25519-mlkem768", + pubX25519: uint8ToBase64(pubX25519), + pubMlKem: uint8ToBase64(pubMlKem) + }; + const priv = { + v: 1, + alg: "hybrid-x25519-mlkem768", + privX25519: uint8ToBase64(privX25519), + privMlKem: uint8ToBase64(privMlKem) + }; + return { pub, priv }; +} + +async function ensureUserKeys() { + const lsEncKey = `userKeys.enc.v1:${id}`; + const lsPubKey = `userKeys.pub.v1:${id}`; + + try { //1. check server + const serverKeysRaw = await fetchEncrypted("user/key/get"); + if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) { + const serverKeys = JSON.parse(serverKeysRaw); + const serverEncPriv = serverKeys.string1; + const serverPub = serverKeys.string2; + if (serverEncPriv && serverPub) { + userKeysEncrypted = serverEncPriv; + userKeysPublic = JSON.parse(serverPub); + userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password); + localStorage.setItem(lsEncKey, userKeysEncrypted); + localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic)); + publishUserKeysGlobals(); + return "success:keys.server"; + } + } + } catch (e) { + //ignore, fallback to local + } + + //fallback to local + const localEnc = localStorage.getItem(lsEncKey); + const localPub = localStorage.getItem(lsPubKey); + if (localEnc && localPub) { + try { + userKeysEncrypted = localEnc; + userKeysPublic = JSON.parse(localPub); + userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password); + + //push keys to server + try { + await fetchEncrypted( + "user/key/update", + JSON.stringify({ + string1: userKeysEncrypted, + string2: JSON.stringify(userKeysPublic) + }) + ); + } catch (e) { + + } + + publishUserKeysGlobals(); + return "success:keys.local"; + } catch (e) { + + } + } + + //new keys, encrypt private with user's password + const bundle = await generateUserKeysBundle(); + userKeysPublic = bundle.pub; + userKeysPrivate = bundle.priv; + userKeysEncrypted = await encryptJsonWithPassword(userKeysPrivate, password); + + localStorage.setItem(lsEncKey, userKeysEncrypted); + localStorage.setItem(lsPubKey, JSON.stringify(userKeysPublic)); + + await fetchEncrypted( + "user/key/update", + JSON.stringify({ + string1: userKeysEncrypted, + string2: JSON.stringify(userKeysPublic) + }) + ); + + publishUserKeysGlobals(); + return "success:keys.generated"; +} + async function mainJS() { await initBlahs(); @@ -1123,10 +1436,32 @@ async function switchInvitesTab(tab) { async function acceptInvite(targetId) { //TODO: Implement key generation try { showAction("action.invite.accepting", "invite.action"); + + await ensureUserKeys(); + + const inviterPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${targetId}`); + const inviterPublicKeys = JSON.parse(inviterPublicKeysRaw); + + //generate room key + const roomKey = randomRoomKey256Chars(); + + //encrypt for self + const selfWrapped = await wrapRoomKeyForSelf(roomKey); + + //encrypt for inviter + const hybrid = await deriveHybridKeyToUser(inviterPublicKeys); + const encForInviter = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes); + const setupPayload = JSON.stringify({ + v: 1, + alg: "hybrid-x25519-mlkem768", + ctMlKem: hybrid.ciphertextMlKemBase64, + enc: encForInviter + }); + let payload = JSON.stringify({ string1: targetId, - string2: "", // TODO: Generate symmetric keys - string3: "" // TODO: Encrypt key for targetId + string2: selfWrapped, + string3: setupPayload }); let res = await fetchEncrypted("user/dm/create", payload); clearAction("invite.action");