Add key mismatch error handling
This commit is contained in:
parent
bba80a19df
commit
aa0351fb2d
8 changed files with 96 additions and 28 deletions
|
|
@ -125,6 +125,8 @@
|
||||||
"desc.no.dms": "no direct meowchats :c",
|
"desc.no.dms": "no direct meowchats :c",
|
||||||
"action.dm.opening": "opening meowchat...",
|
"action.dm.opening": "opening meowchat...",
|
||||||
"dm.open.failed": "failed to open meowchat :c",
|
"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",
|
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
||||||
"messages.decrypt.failed": "failed to decrypt meow :c"
|
"messages.decrypt.failed": "failed to decrypt meow :c"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,8 @@
|
||||||
"desc.no.dms": "No direct messages",
|
"desc.no.dms": "No direct messages",
|
||||||
"action.dm.opening": "Opening DM...",
|
"action.dm.opening": "Opening DM...",
|
||||||
"dm.open.failed": "Failed to open 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",
|
"dm.messages.fetch.failed": "Failed to fetch messages",
|
||||||
"messages.decrypt.failed": "Failed to decrypt message"
|
"messages.decrypt.failed": "Failed to decrypt message"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,11 @@ async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase6
|
||||||
return aesKeyBytes;
|
return aesKeyBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userPublicKeysFingerprint(pub) {
|
||||||
|
if (!pub) return "";
|
||||||
|
return `${pub.pubX25519 || ""}|${pub.pubMlKem || ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchDmKey(dmId) {
|
async function fetchDmKey(dmId) {
|
||||||
return await fetchEncrypted("dm/key/get", dmId);
|
return await fetchEncrypted("dm/key/get", dmId);
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +336,7 @@ async function ensureDmRoomKey(dmId) {
|
||||||
const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
|
const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
|
||||||
|
|
||||||
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
|
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
|
||||||
|
await unwrapRoomKeyForSelf(selfWrapped);
|
||||||
await updateDmKey(dmId, selfWrapped);
|
await updateDmKey(dmId, selfWrapped);
|
||||||
return roomKey;
|
return roomKey;
|
||||||
}
|
}
|
||||||
|
|
@ -918,10 +924,12 @@ async function ensureUserKeys() {
|
||||||
const lsEncKey = `userKeys.enc.v1:${id}`;
|
const lsEncKey = `userKeys.enc.v1:${id}`;
|
||||||
const lsPubKey = `userKeys.pub.v1:${id}`;
|
const lsPubKey = `userKeys.pub.v1:${id}`;
|
||||||
|
|
||||||
|
let serverKeys = null;
|
||||||
|
let serverKeysRaw = "";
|
||||||
try { //1. check server
|
try { //1. check server
|
||||||
const serverKeysRaw = await fetchEncrypted("user/key/get");
|
serverKeysRaw = await fetchEncrypted("user/key/get");
|
||||||
if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) {
|
if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) {
|
||||||
const serverKeys = JSON.parse(serverKeysRaw);
|
serverKeys = JSON.parse(serverKeysRaw);
|
||||||
const serverEncPriv = serverKeys.string1;
|
const serverEncPriv = serverKeys.string1;
|
||||||
const serverPub = serverKeys.string2;
|
const serverPub = serverKeys.string2;
|
||||||
if (serverEncPriv && serverPub) {
|
if (serverEncPriv && serverPub) {
|
||||||
|
|
@ -935,7 +943,9 @@ async function ensureUserKeys() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//ignore, fallback to local
|
if (serverKeys?.string1) {
|
||||||
|
throw new Error("error:keys.server.decrypt.failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//fallback to local
|
//fallback to local
|
||||||
|
|
@ -943,30 +953,45 @@ async function ensureUserKeys() {
|
||||||
const localPub = localStorage.getItem(lsPubKey);
|
const localPub = localStorage.getItem(lsPubKey);
|
||||||
if (localEnc && localPub) {
|
if (localEnc && localPub) {
|
||||||
try {
|
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;
|
userKeysEncrypted = localEnc;
|
||||||
userKeysPublic = JSON.parse(localPub);
|
userKeysPublic = localPubObj;
|
||||||
userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
|
userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
|
||||||
|
|
||||||
//push keys to server
|
//push keys to server only when server has none yet
|
||||||
try {
|
if (!serverKeys?.string1) {
|
||||||
await fetchEncrypted(
|
const updateRes = await fetchEncrypted(
|
||||||
"user/key/update",
|
"user/key/update",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
string1: userKeysEncrypted,
|
string1: userKeysEncrypted,
|
||||||
string2: JSON.stringify(userKeysPublic)
|
string2: JSON.stringify(userKeysPublic)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
if (updateRes === "error:keys.public.mismatch") {
|
||||||
|
throw new Error("error:keys.local.server.mismatch");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishUserKeysGlobals();
|
publishUserKeysGlobals();
|
||||||
return "success:keys.local";
|
return "success:keys.local";
|
||||||
} catch (e) {
|
} 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
|
//new keys, encrypt private with user's password
|
||||||
const bundle = await generateUserKeysBundle();
|
const bundle = await generateUserKeysBundle();
|
||||||
userKeysPublic = bundle.pub;
|
userKeysPublic = bundle.pub;
|
||||||
|
|
@ -1777,8 +1802,8 @@ async function openDm(dmId, username, targetId) {
|
||||||
currentDmId = dmId;
|
currentDmId = dmId;
|
||||||
|
|
||||||
let pfp = await getAvatarUrl(targetId, username);
|
let pfp = await getAvatarUrl(targetId, username);
|
||||||
let iconHtml = `<img src="${pfp}" style="width: 1.8rem; height: 1.8rem; border-radius: 0.4rem; margin-right: 0.5rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
let iconHtml = `<img src="${pfp}" style="width: 2.3rem; border-radius: 0.65rem; margin-right: 0.38rem; object-fit: cover; border: var(--border-width) solid rgba(255, 255, 255, 0.2);">`;
|
||||||
await switchRoomContent(username.split(':')[0], chatScreen, false, iconHtml);
|
await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml);
|
||||||
|
|
||||||
let msgContainer = document.getElementById("chat-messages");
|
let msgContainer = document.getElementById("chat-messages");
|
||||||
if (msgContainer) {
|
if (msgContainer) {
|
||||||
|
|
@ -1796,7 +1821,11 @@ async function openDm(dmId, username, targetId) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearAction("dmopen");
|
clearAction("dmopen");
|
||||||
console.error(e);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,10 @@ var chatScreen = `
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
<div style="padding: 1rem; border-top: var(--border-width) solid var(--light-border-color); display: flex; gap: 0.5rem;">
|
||||||
<input type="text" id="chat-input" class="forminput" style="flex-grow: 1; margin: 0;" placeholder="{blah(placeholder.message.input)}">
|
<input type="text" id="chat-input" class="forminput" style="flex-grow: 1; margin: 0;" placeholder="{blah(placeholder.message.input)}">
|
||||||
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0 1.5rem;">
|
<button class="submit-button" onclick="sendMessage()" style="margin: 0; padding: 0; aspect-ratio: 1;">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="var(--main-bg-color)" width="1.5rem"><path d="M120-160v-240l320-80-320-80v-240l760 320-760 320Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.9 -0.5 15.6 16" fill="none" width="1.5rem">
|
||||||
|
<path stroke="var(--main-bg-color)" stroke-linecap="round" stroke-linejoin="round" d="m3.75 7.5 -1.875 5.625 11.25 -5.625L1.875 1.875l1.875 5.625zm0 0h3.75" stroke-width="1.1"></path>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ indicator.active {
|
||||||
width: calc(var(--icon-button-height) - (var(--border-width) * 2));
|
width: calc(var(--icon-button-height) - (var(--border-width) * 2));
|
||||||
height: calc(var(--icon-button-height) - (var(--border-width) * 2));
|
height: calc(var(--icon-button-height) - (var(--border-width) * 2));
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
roomcontent {
|
roomcontent {
|
||||||
|
|
@ -605,7 +605,7 @@ space{
|
||||||
.room-pfp {
|
.room-pfp {
|
||||||
width: 2.2rem;
|
width: 2.2rem;
|
||||||
height: 2.2rem;
|
height: 2.2rem;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.65rem;
|
||||||
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
|
border: var(--border-width) solid rgba(255, 255, 255, 0.2);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,8 @@
|
||||||
"desc.no.dms": "no direct meowchats :c",
|
"desc.no.dms": "no direct meowchats :c",
|
||||||
"action.dm.opening": "opening meowchat...",
|
"action.dm.opening": "opening meowchat...",
|
||||||
"dm.open.failed": "failed to open meowchat :c",
|
"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",
|
"dm.messages.fetch.failed": "failed to fetch meows :c",
|
||||||
"messages.decrypt.failed": "failed to decrypt meow :c"
|
"messages.decrypt.failed": "failed to decrypt meow :c"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,8 @@
|
||||||
"desc.no.dms": "No direct messages",
|
"desc.no.dms": "No direct messages",
|
||||||
"action.dm.opening": "Opening DM...",
|
"action.dm.opening": "Opening DM...",
|
||||||
"dm.open.failed": "Failed to open 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",
|
"dm.messages.fetch.failed": "Failed to fetch messages",
|
||||||
"messages.decrypt.failed": "Failed to decrypt message"
|
"messages.decrypt.failed": "Failed to decrypt message"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,11 @@ async function deriveHybridKeyFromSetup(otherPublicKeysObj, ciphertextMlKemBase6
|
||||||
return aesKeyBytes;
|
return aesKeyBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userPublicKeysFingerprint(pub) {
|
||||||
|
if (!pub) return "";
|
||||||
|
return `${pub.pubX25519 || ""}|${pub.pubMlKem || ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchDmKey(dmId) {
|
async function fetchDmKey(dmId) {
|
||||||
return await fetchEncrypted("dm/key/get", dmId);
|
return await fetchEncrypted("dm/key/get", dmId);
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +336,7 @@ async function ensureDmRoomKey(dmId) {
|
||||||
const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
|
const roomKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes);
|
||||||
|
|
||||||
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
|
const selfWrapped = await wrapRoomKeyForSelf(roomKey);
|
||||||
|
await unwrapRoomKeyForSelf(selfWrapped);
|
||||||
await updateDmKey(dmId, selfWrapped);
|
await updateDmKey(dmId, selfWrapped);
|
||||||
return roomKey;
|
return roomKey;
|
||||||
}
|
}
|
||||||
|
|
@ -918,10 +924,12 @@ async function ensureUserKeys() {
|
||||||
const lsEncKey = `userKeys.enc.v1:${id}`;
|
const lsEncKey = `userKeys.enc.v1:${id}`;
|
||||||
const lsPubKey = `userKeys.pub.v1:${id}`;
|
const lsPubKey = `userKeys.pub.v1:${id}`;
|
||||||
|
|
||||||
|
let serverKeys = null;
|
||||||
|
let serverKeysRaw = "";
|
||||||
try { //1. check server
|
try { //1. check server
|
||||||
const serverKeysRaw = await fetchEncrypted("user/key/get");
|
serverKeysRaw = await fetchEncrypted("user/key/get");
|
||||||
if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) {
|
if (serverKeysRaw && serverKeysRaw.trim() !== "" && serverKeysRaw.trim().startsWith("{")) {
|
||||||
const serverKeys = JSON.parse(serverKeysRaw);
|
serverKeys = JSON.parse(serverKeysRaw);
|
||||||
const serverEncPriv = serverKeys.string1;
|
const serverEncPriv = serverKeys.string1;
|
||||||
const serverPub = serverKeys.string2;
|
const serverPub = serverKeys.string2;
|
||||||
if (serverEncPriv && serverPub) {
|
if (serverEncPriv && serverPub) {
|
||||||
|
|
@ -935,7 +943,9 @@ async function ensureUserKeys() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//ignore, fallback to local
|
if (serverKeys?.string1) {
|
||||||
|
throw new Error("error:keys.server.decrypt.failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//fallback to local
|
//fallback to local
|
||||||
|
|
@ -943,30 +953,45 @@ async function ensureUserKeys() {
|
||||||
const localPub = localStorage.getItem(lsPubKey);
|
const localPub = localStorage.getItem(lsPubKey);
|
||||||
if (localEnc && localPub) {
|
if (localEnc && localPub) {
|
||||||
try {
|
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;
|
userKeysEncrypted = localEnc;
|
||||||
userKeysPublic = JSON.parse(localPub);
|
userKeysPublic = localPubObj;
|
||||||
userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
|
userKeysPrivate = await decryptJsonWithPassword(userKeysEncrypted, password);
|
||||||
|
|
||||||
//push keys to server
|
//push keys to server only when server has none yet
|
||||||
try {
|
if (!serverKeys?.string1) {
|
||||||
await fetchEncrypted(
|
const updateRes = await fetchEncrypted(
|
||||||
"user/key/update",
|
"user/key/update",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
string1: userKeysEncrypted,
|
string1: userKeysEncrypted,
|
||||||
string2: JSON.stringify(userKeysPublic)
|
string2: JSON.stringify(userKeysPublic)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) {
|
if (updateRes === "error:keys.public.mismatch") {
|
||||||
|
throw new Error("error:keys.local.server.mismatch");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
publishUserKeysGlobals();
|
publishUserKeysGlobals();
|
||||||
return "success:keys.local";
|
return "success:keys.local";
|
||||||
} catch (e) {
|
} 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
|
//new keys, encrypt private with user's password
|
||||||
const bundle = await generateUserKeysBundle();
|
const bundle = await generateUserKeysBundle();
|
||||||
userKeysPublic = bundle.pub;
|
userKeysPublic = bundle.pub;
|
||||||
|
|
@ -1796,7 +1821,11 @@ async function openDm(dmId, username, targetId) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearAction("dmopen");
|
clearAction("dmopen");
|
||||||
console.error(e);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue