diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index d9e7e382..a41d1b4f 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -125,6 +125,8 @@ "desc.no.dms": "no direct meowchats :c", "action.dm.opening": "opening meowchat...", "dm.open.failed": "failed to open meowchat :c", + "keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on", + "keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c", "dm.messages.fetch.failed": "failed to fetch meows :c", "messages.decrypt.failed": "failed to decrypt meow :c" } diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index b222b3b2..31c45501 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -124,6 +124,8 @@ "desc.no.dms": "No direct messages", "action.dm.opening": "Opening DM...", "dm.open.failed": "Failed to open DM", + "keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.", + "keys.server.decrypt.failed": "Could not decrypt your account keys with this password.", "dm.messages.fetch.failed": "Failed to fetch messages", "messages.decrypt.failed": "Failed to decrypt message" } diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 1020d879..c3fd59d3 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -297,6 +297,11 @@ async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase6 return aesKeyBytes; } +function userPublicKeysFingerprint(pub) { + if (!pub) return ""; + return `${pub.pubX25519 || ""}|${pub.pubMlKem || ""}`; +} + async function fetchDmKey(dmId) { return await fetchEncrypted("dm/key/get", dmId); } @@ -331,6 +336,7 @@ async function ensureDmRoomKey(dmId) { const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes); const selfWrapped = await wrapRoomKeyForSelf(roomKey); + await unwrapRoomKeyForSelf(selfWrapped); await updateDmKey(dmId, selfWrapped); return roomKey; } @@ -918,10 +924,12 @@ async function ensureUserKeys() { const lsEncKey = `userKeys.enc.v1:${id}`; const lsPubKey = `userKeys.pub.v1:${id}`; + let serverKeys = null; + let serverKeysRaw = ""; try { //1. check server - const serverKeysRaw = await fetchEncrypted("user/key/get"); + serverKeysRaw = await fetchEncrypted("user/key/get"); if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) { - const serverKeys = JSON.parse(serverKeysRaw); + serverKeys = JSON.parse(serverKeysRaw); const serverEncPriv = serverKeys.string1; const serverPub = serverKeys.string2; if (serverEncPriv && serverPub) { @@ -935,7 +943,9 @@ async function ensureUserKeys() { } } } catch (e) { - //ignore, fallback to local + if (serverKeys?.string1) { + throw new Error("error:keys.server.decrypt.failed"); + } } //fallback to local @@ -943,30 +953,45 @@ async function ensureUserKeys() { const localPub = localStorage.getItem(lsPubKey); if (localEnc && localPub) { try { + const localPubObj = JSON.parse(localPub); + if (serverKeys?.string2) { + const serverPubObj = JSON.parse(serverKeys.string2); + if (userPublicKeysFingerprint(localPubObj) !== userPublicKeysFingerprint(serverPubObj)) { + throw new Error("error:keys.local.server.mismatch"); + } + } + userKeysEncrypted = localEnc; - userKeysPublic = JSON.parse(localPub); + userKeysPublic = localPubObj; userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password); - //push keys to server - try { - await fetchEncrypted( + //push keys to server only when server has none yet + if (!serverKeys?.string1) { + const updateRes = await fetchEncrypted( "user/key/update", JSON.stringify({ string1: userKeysEncrypted, string2: JSON.stringify(userKeysPublic) }) ); - } catch (e) { - + if (updateRes === "error:keys.public.mismatch") { + throw new Error("error:keys.local.server.mismatch"); + } } publishUserKeysGlobals(); return "success:keys.local"; } catch (e) { - + if (e.message && e.message.startsWith("error:keys.")) { + throw e; + } } } + if (serverKeys?.string1) { + throw new Error("error:keys.server.decrypt.failed"); + } + //new keys, encrypt private with user's password const bundle = await generateUserKeysBundle(); userKeysPublic = bundle.pub; @@ -1777,8 +1802,8 @@ async function openDm(dmId, username, targetId) { currentDmId = dmId; let pfp = await getAvatarUrl(targetId, username); - let iconHtml = ``; - await switchRoomContent(username.split(':')[0], chatScreen, false, iconHtml); + let iconHtml = ``; + await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml); let msgContainer = document.getElementById("chat-messages"); if (msgContainer) { @@ -1796,7 +1821,11 @@ async function openDm(dmId, username, targetId) { } catch (e) { clearAction("dmopen"); console.error(e); - showBlahNotification("error:dm.open.failed"); + if (e.message === "error:keys.local.server.mismatch" || e.message === "error:keys.server.decrypt.failed") { + showBlahNotification(e.message); + } else { + showBlahNotification("error:dm.open.failed"); + } } } diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index 67111acb..2dc7189f 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -136,8 +136,10 @@ var chatScreen = `
-
diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index f6631631..f39af177 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -148,7 +148,7 @@ indicator.active { width: calc(var(--icon-button-height) - (var(--border-width) * 2)); height: calc(var(--icon-button-height) - (var(--border-width) * 2)); pointer-events: none !important; - border-radius: 0.6rem; + border-radius: 0.65rem; } roomcontent { @@ -605,7 +605,7 @@ space{ .room-pfp { width: 2.2rem; height: 2.2rem; - border-radius: 0.6rem; + border-radius: 0.65rem; border: var(--border-width) solid rgba(255, 255, 255, 0.2); object-fit: cover; pointer-events: none; diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index d9e7e382..a41d1b4f 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -125,6 +125,8 @@ "desc.no.dms": "no direct meowchats :c", "action.dm.opening": "opening meowchat...", "dm.open.failed": "failed to open meowchat :c", + "keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on", + "keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c", "dm.messages.fetch.failed": "failed to fetch meows :c", "messages.decrypt.failed": "failed to decrypt meow :c" } diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index b222b3b2..31c45501 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -124,6 +124,8 @@ "desc.no.dms": "No direct messages", "action.dm.opening": "Opening DM...", "dm.open.failed": "Failed to open DM", + "keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.", + "keys.server.decrypt.failed": "Could not decrypt your account keys with this password.", "dm.messages.fetch.failed": "Failed to fetch messages", "messages.decrypt.failed": "Failed to decrypt message" } diff --git a/webroot/main.js b/webroot/main.js index 9bbdd297..c3fd59d3 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -297,6 +297,11 @@ async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase6 return aesKeyBytes; } +function userPublicKeysFingerprint(pub) { + if (!pub) return ""; + return `${pub.pubX25519 || ""}|${pub.pubMlKem || ""}`; +} + async function fetchDmKey(dmId) { return await fetchEncrypted("dm/key/get", dmId); } @@ -331,6 +336,7 @@ async function ensureDmRoomKey(dmId) { const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes); const selfWrapped = await wrapRoomKeyForSelf(roomKey); + await unwrapRoomKeyForSelf(selfWrapped); await updateDmKey(dmId, selfWrapped); return roomKey; } @@ -918,10 +924,12 @@ async function ensureUserKeys() { const lsEncKey = `userKeys.enc.v1:${id}`; const lsPubKey = `userKeys.pub.v1:${id}`; + let serverKeys = null; + let serverKeysRaw = ""; try { //1. check server - const serverKeysRaw = await fetchEncrypted("user/key/get"); + serverKeysRaw = await fetchEncrypted("user/key/get"); if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) { - const serverKeys = JSON.parse(serverKeysRaw); + serverKeys = JSON.parse(serverKeysRaw); const serverEncPriv = serverKeys.string1; const serverPub = serverKeys.string2; if (serverEncPriv && serverPub) { @@ -935,7 +943,9 @@ async function ensureUserKeys() { } } } catch (e) { - //ignore, fallback to local + if (serverKeys?.string1) { + throw new Error("error:keys.server.decrypt.failed"); + } } //fallback to local @@ -943,30 +953,45 @@ async function ensureUserKeys() { const localPub = localStorage.getItem(lsPubKey); if (localEnc && localPub) { try { + const localPubObj = JSON.parse(localPub); + if (serverKeys?.string2) { + const serverPubObj = JSON.parse(serverKeys.string2); + if (userPublicKeysFingerprint(localPubObj) !== userPublicKeysFingerprint(serverPubObj)) { + throw new Error("error:keys.local.server.mismatch"); + } + } + userKeysEncrypted = localEnc; - userKeysPublic = JSON.parse(localPub); + userKeysPublic = localPubObj; userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password); - //push keys to server - try { - await fetchEncrypted( + //push keys to server only when server has none yet + if (!serverKeys?.string1) { + const updateRes = await fetchEncrypted( "user/key/update", JSON.stringify({ string1: userKeysEncrypted, string2: JSON.stringify(userKeysPublic) }) ); - } catch (e) { - + if (updateRes === "error:keys.public.mismatch") { + throw new Error("error:keys.local.server.mismatch"); + } } publishUserKeysGlobals(); return "success:keys.local"; } catch (e) { - + if (e.message && e.message.startsWith("error:keys.")) { + throw e; + } } } + if (serverKeys?.string1) { + throw new Error("error:keys.server.decrypt.failed"); + } + //new keys, encrypt private with user's password const bundle = await generateUserKeysBundle(); userKeysPublic = bundle.pub; @@ -1796,7 +1821,11 @@ async function openDm(dmId, username, targetId) { } catch (e) { clearAction("dmopen"); console.error(e); - showBlahNotification("error:dm.open.failed"); + if (e.message === "error:keys.local.server.mismatch" || e.message === "error:keys.server.decrypt.failed") { + showBlahNotification(e.message); + } else { + showBlahNotification("error:dm.open.failed"); + } } }