fix(messages): address S2 review — partner key refresh + reactive me
- On a decrypt failure, refresh the partner's cached public key (and drop the stale derived conversation key) once per pass and retry. A passphrase reset rotates a keypair, so a partner who cached the old public key would otherwise encrypt undecryptable messages until a reload; the first failed decrypt now self-heals reads AND future sends. (pubCache was assumed set-once.) - KeyGate reads has_password via a reactive useQuery(['me'], enabled:false) passive observer instead of a one-shot getQueryData, so the reset password field stays correct even if me resolves after mount. - Document the remaining multi-device staleness (a reset on another device leaves this one 'ready' with the dead key) as a known limitation in useKeyState.
This commit is contained in:
@@ -8,7 +8,7 @@ import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { LoadingState, StateMessage } from "./QueryState";
|
||||
import { relativeTime } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { partnerPub, POLL_MS, useKeyState } from "../lib/messaging";
|
||||
import { partnerPub, POLL_MS, refreshPartnerPub, useKeyState } from "../lib/messaging";
|
||||
import * as e2ee from "../lib/e2ee";
|
||||
import KeyGate from "./KeyGate";
|
||||
|
||||
@@ -49,14 +49,28 @@ export default function ChatThread({
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const out: Record<number, string> = {};
|
||||
let pub = await partnerPub(partnerId);
|
||||
let refreshed = false;
|
||||
for (const m of q.data?.items ?? []) {
|
||||
if (m.ciphertext && m.iv) {
|
||||
try {
|
||||
const pub = await partnerPub(partnerId);
|
||||
out[m.id] = await e2ee.decryptFrom(partnerId, pub, m.ciphertext, m.iv);
|
||||
} catch {
|
||||
out[m.id] = "⚠ " + t("messages.cantDecrypt");
|
||||
if (!m.ciphertext || !m.iv) continue;
|
||||
try {
|
||||
out[m.id] = await e2ee.decryptFrom(partnerId, pub, m.ciphertext, m.iv);
|
||||
} catch {
|
||||
// A decrypt failure can mean the partner rotated their keypair (a passphrase reset), so
|
||||
// this device's cached public key is stale. Refresh it ONCE per pass and retry — that
|
||||
// also re-derives future sends under the current key. A genuinely bad message still ends
|
||||
// up "can't decrypt".
|
||||
if (!refreshed) {
|
||||
refreshed = true;
|
||||
try {
|
||||
pub = await refreshPartnerPub(partnerId);
|
||||
out[m.id] = await e2ee.decryptFrom(partnerId, pub, m.ciphertext, m.iv);
|
||||
continue;
|
||||
} catch {
|
||||
/* fall through to the error label below */
|
||||
}
|
||||
}
|
||||
out[m.id] = "⚠ " + t("messages.cantDecrypt");
|
||||
}
|
||||
}
|
||||
if (!cancelled) setPlain(out);
|
||||
|
||||
Reference in New Issue
Block a user