Skip to content

fix(#243,#247,#248): close the GDPR group-messaging bug cluster#278

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/gdpr-group-messaging-trio
Jul 13, 2026
Merged

fix(#243,#247,#248): close the GDPR group-messaging bug cluster#278
TortoiseWolfe merged 1 commit into
mainfrom
fix/gdpr-group-messaging-trio

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Summary

Closes the GDPR / group-messaging subset of the Gap-Audit cluster: #243, #247, #248 — the trio about group conversations. Each was verified against the real code + live DB before fixing, and a 3-agent adversarial design review caught five defects in the initial designs that are all fixed here.

Follow-up to #276 (the security-critical batch). Two cluster bugs remain after this — messaging reliability #244/#245/#246 — designed and deferred.

The three fixes

#247 — Account deletion blocked for group creators (right-to-erasure)

conversations.created_by + group_keys.created_by were ON DELETE NO ACTION, so a user who created a group or distributed group keys couldn't be erased.

  • Both FKs → ON DELETE SET NULL (deleting the creator preserves the group + every other member's keys; CASCADE would destroy the group for everyone). created_by made nullable, CHK023 relaxed.
  • Adversarial-review catch: SET NULL then erasing a sole owner orphaned the group with no owner — every isOwner-gated op (rename/remove/delete/transfer) would fail forever. Added a BEFORE DELETE trigger on conversation_members that auto-transfers ownership to the earliest-joined survivor.

#248 — Data export omits group conversations (data-portability)

exportUserData enumerated by participant_1/2 columns — NULL for groups → groups silently absent.

  • Enumerate groups via conversation_members, decrypt each message through the group-key path at its own key_version (new order-agnostic group-message-decrypt helper), include group_name + member roster, count in stats.
  • Review catches: call restoreKeysFromCache() before the group loop (the export page has no EncryptionKeyGate → cold keys → placeholders otherwise); surface system-message plaintext markers instead of decrypting them to a placeholder.

#243 — Rotating a group creator's personal keys bricked group history

The reader unwrapped with the creator's current public key; after rotation, ECDH mismatched → history bricked for every member.

  • Capture the creator's wrap-time public JWK on each group_keys row (creator_public_key) and unwrap against it; legacy rows fall back to a time-window resolver over the retained key history.
  • Backfill (run via Management API — the immutable-keys UPDATE policy blocks client writes) repaired all 331 existing rows, un-bricking past history.
  • Review catch: the fallback is null-safe on created_by (which Account deletion fails for users who created group chats (GDPR erasure blocked) #247 can SET NULL).

Verification

Schema applied live via the Management API (idempotent, in the monolithic migration) — no separate merge-time DB step needed; the backfill already ran. No edge-function changes.

🤖 Generated with Claude Code

Three confirmed Gap-Audit bugs about group conversations, each verified against
the real code + live DB before fixing and re-verified after. A 3-agent
adversarial design-review caught five defects in the initial designs — all
addressed here.

#247 Account deletion blocked for group creators (right-to-erasure): both
conversations.created_by and group_keys.created_by were ON DELETE NO ACTION, so
a user who created a group or distributed group keys could not be deleted. Change
both FKs to ON DELETE SET NULL (deleting the creator preserves the group and
every OTHER member's key row — CASCADE would destroy the group for everyone),
make group_keys.created_by nullable, and relax CHK023 to allow a NULL creator.
Adversarial-review catch: SET NULL then let a sole owner be erased, orphaning the
group with no owner — every isOwner-gated op (rename/remove/delete/transfer)
would fail forever. Added a BEFORE DELETE trigger on conversation_members that
auto-transfers ownership to the earliest-joined survivor, so the "a group with
members always has an owner" invariant holds through erasure.

#248 Data export omits group conversations (data-portability): exportUserData
enumerated conversations by participant_1/2 columns, which are NULL for groups,
so groups never appeared and their messages were silently absent. Enumerate
groups via conversation_members (active memberships), decrypt each message
through the group-key path at its own key_version (new shared
group-message-decrypt helper, order-agnostic — no .reverse() coupling), include
group_name + member roster, and count them in the statistics. Review catches:
call restoreKeysFromCache() before the group loop (the export page has no
EncryptionKeyGate, so keys are cold → placeholders otherwise); surface
system-message plaintext markers instead of running them through decryption.

#243 Rotating a group creator's personal keys bricked group history: the reader
unwrapped the group key with the creator's CURRENT public key (getUserPublicKey
filters revoked=false); after the creator rotated, ECDH mismatched and history
bricked with "Encrypted with previous keys" for EVERY member. Capture the
creator's wrap-time public JWK on each group_keys row (creator_public_key) and
unwrap against THAT; legacy rows fall back to getUserPublicKeyAt, a time-window
resolver over the retained key history. A one-shot backfill (run via the
Management API, since the immutable-keys UPDATE policy blocks client writes)
repaired all 331 existing rows — un-bricking past history. Review catch: the
fallback is null-safe on created_by (which #247 can SET NULL).

Verification: #243 proven with REAL crypto (group-key-roundtrip.node.test — wrap,
rotate the creator, prove unwrap with the current key FAILS and with the stored
key SUCCEEDS + a pre-rotation message still decrypts). #247 proven live (RLS
DB-contract tests: erasure succeeds, group survives with created_by nulled,
ownership auto-transfers to the survivor). #248 unit-tested (group enumeration,
roster, decrypt threading, cold-key restore, stats). Full 68-test RLS suite and
179 messaging unit tests green; type-check + lint clean. Schema applied live via
the Management API (idempotent, monolithic migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants