DM keys!!!
All checks were successful
Android Build / publish (push) Successful in 43s
Linux Build / publish (push) Successful in 51s

This commit is contained in:
olcxja 2026-05-27 23:45:38 +02:00
commit 8f28879f18
4 changed files with 686 additions and 4 deletions

View file

@ -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");

View file

@ -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");

View file

@ -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");

View file

@ -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");