From f1cba99b9e5471eaa2a69a96979d8bad955a5213 Mon Sep 17 00:00:00 2001 From: byGalax Date: Thu, 21 May 2026 22:51:17 +0200 Subject: [PATCH] fix(conv-key): bootstrap re-fetches canonical key after share to handle concurrent race --- packages/shared/src/chat/convKeys.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/chat/convKeys.ts b/packages/shared/src/chat/convKeys.ts index 9ff1354..8bb7fc4 100644 --- a/packages/shared/src/chat/convKeys.ts +++ b/packages/shared/src/chat/convKeys.ts @@ -110,7 +110,26 @@ export async function bootstrapConvKey( p_bundles: bundles, }); if (error) throw error; - const handle = { conversationId, keyVersion, key: convKey }; + + // `share_conv_keys` uses `ON CONFLICT (conv, recipient_user_id, key_version) + // DO NOTHING`. If a concurrent peer bootstrapped first at the same version, + // OUR INSERTs were silently skipped server-side and the row on the server + // holds THEIR conv-key, not ours. Trusting the locally-generated key here + // would leave both clients with mutually un-decryptable bundles (each + // encrypting/decrypting with its own key — exactly the bug that broke + // conv aae12d84). Re-fetch our own bundle and unwrap to get the CANONICAL + // server key. Whoever wrote first wins; the loser converges. + const ownBundle = await fetchKeyBundle(client, conversationId, own.userId, keyVersion); + if (!ownBundle) { + throw new Error('bootstrapConvKey: own bundle missing after share_conv_keys'); + } + const canonicalKey = await unwrapConvKey( + ownBundle.encryptedKey, + ownBundle.nonce, + ownBundle.sender.senderPublicKey, + own.privateKey, + ); + const handle = { conversationId, keyVersion, key: canonicalKey }; cache.set(cacheKey(conversationId, keyVersion), handle); return handle; }