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 = `