diff --git a/packages/shared/src/chat/convKeys.ts b/packages/shared/src/chat/convKeys.ts index 29e3901..9ff1354 100644 --- a/packages/shared/src/chat/convKeys.ts +++ b/packages/shared/src/chat/convKeys.ts @@ -125,12 +125,25 @@ export async function getOrCreateConvKey( if (cached) return cached; const bundle = await fetchKeyBundle(client, conversationId, own.userId, version); if (bundle) { - const key = await unwrapConvKey( - bundle.encryptedKey, bundle.nonce, bundle.sender.senderPublicKey, own.privateKey, - ); - const handle = { conversationId, keyVersion: version, key }; - cache.set(cacheKey(conversationId, version), handle); - return handle; + try { + const key = await unwrapConvKey( + bundle.encryptedKey, bundle.nonce, bundle.sender.senderPublicKey, own.privateKey, + ); + const handle = { conversationId, keyVersion: version, key }; + cache.set(cacheKey(conversationId, version), handle); + return handle; + } catch (err) { + // A bundle exists for us but our current private key cannot unwrap it. + // The most common cause is `reset_user_key`: a fresh user-key pair was + // generated locally while the on-server bundle is still wrapped against + // the previous public key. Treat this the same as "no bundle for me" — + // mint a fresh conv-key at version+1 wrapped to our CURRENT key. Old + // messages stay unreadable for us; new ones flow. + console.warn( + '[conv-key] unwrap own bundle failed at v' + version + ' — auto-rotating', + err, + ); + } } const { count, error: cntErr } = await rawFrom(client, 'conversation_keys') .select('recipient_user_id', { count: 'exact', head: true }) @@ -138,12 +151,13 @@ export async function getOrCreateConvKey( .eq('key_version', version); if (cntErr) throw cntErr; if ((count ?? 0) > 0) { - // Rows exist for this version, but none for me. Either I lost the device-key - // that originally received my bundle, or my own bundle was wiped by the - // 0.18.0 reset_user_key bug. Either way, the only way out is to mint a fresh - // conv-key at version+1 and wrap it for everyone we can. Old messages stay - // unreadable for me; new ones flow. - console.info('[conv-key] no bundle for me at v' + version + ' — auto-rotating'); + // Rows exist for this version, but none usable for me. Either I lost the + // device-key that originally received my bundle, my own bundle was wiped + // by the 0.18.0 reset_user_key bug, or my key was reset and the existing + // bundle is unwrappable (handled in the try/catch above). The only way + // out is to mint a fresh conv-key at version+1 and wrap it for everyone + // we can. Old messages stay unreadable for me; new ones flow. + console.info('[conv-key] no usable bundle for me at v' + version + ' — auto-rotating'); return rotateConvKey(client, conversationId, own); } return bootstrapConvKey(client, conversationId, own, version); @@ -248,9 +262,24 @@ export async function tryGetConvKey( if (cached) return cached; const bundle = await fetchKeyBundle(client, conversationId, ownUserId, keyVersion); if (!bundle) return null; - const key = await unwrapConvKey( - bundle.encryptedKey, bundle.nonce, bundle.sender.senderPublicKey, ownPrivateKey, - ); + let key: Uint8Array; + try { + key = await unwrapConvKey( + bundle.encryptedKey, bundle.nonce, bundle.sender.senderPublicKey, ownPrivateKey, + ); + } catch (err) { + // Bundle exists but the current private key doesn't unwrap it (typically + // after `reset_user_key`). Return null so the caller treats the message + // as un-decryptable instead of throwing and killing the whole batch. + // The conversation will be auto-rotated to a fresh key on the next send + // or chat open via `getOrCreateConvKey`'s own recovery path. + console.warn( + '[conv-key] tryGetConvKey unwrap failed at v' + keyVersion + + ' (conv=' + conversationId.slice(0, 8) + ') — marking as un-decryptable', + err, + ); + return null; + } const handle = { conversationId, keyVersion, key }; cache.set(cacheKey(conversationId, keyVersion), handle); return handle;