fix: stop reset_user_key from wiping conv-key bundles + auto-rotate stuck convs

Root cause of "alle Nachrichten verschlüsselt + kann nicht schreiben":
uploadUserKeyBlob (called by setupNewUserIdentity, changePin and
regenerateRecoveryCode) routed through reset_user_key, which DELETES
every conversation_keys row addressed to the user or one of their
devices. So setting a PIN destroyed every legacy bundle BEFORE the
migration could re-wrap them. The user ended up with user_keys set,
zero un-migrated bundles, no decryption, no send.

Fixes shipped:

  * supabase/migrations/20260516000001_user_key_rpcs_v2.sql
    - upsert_user_key: same UPSERT, NO delete. Used everywhere except
      "Identität zurücksetzen" (which keeps reset_user_key on purpose).
    - rotate_conv_key: bumps active_key_version atomically and inserts
      a fresh batch of bundles (per-user + per-device fallback).
  * shared/auth/userKey.ts: uploadUserKeyBlob now calls upsert_user_key.
  * shared/chat/convKeys.ts: new rotateConvKey() that wraps the fresh
    conv-key for every member's user_keys (preferred) and falls back to
    each member's per-device public_key for peers still on 0.17.x.
  * shared/chat/convKeys.ts: getOrCreateConvKey auto-triggers rotate
    when the user has no recipient_user_id row at the active version
    but rows exist (the deadlock case). Existing outbox retries drain
    on their own once the rotate completes — no manual button.
  * desktop/MessageBubble.tsx: "...cannot decrypt" is now a softer,
    German "Nachricht nicht lesbar" so users don't think the app
    crashed when historical messages can't be unwrapped.
This commit is contained in:
byGalax
2026-05-16 00:48:28 +02:00
parent 9207f473cd
commit d39a0fb6dc
5 changed files with 285 additions and 6 deletions
@@ -416,7 +416,11 @@ export function MessageBubble({
</button>
)}
{message.plaintext === null ? (
<span className="italic opacity-70">cannot decrypt</span>
<span className="italic text-fg-muted opacity-60">
{t('app:chats.unreadable', {
defaultValue: 'Nachricht nicht lesbar',
})}
</span>
) : parsed.kind === 'poll' ? (
<PollCard
question={parsed.question}