Skip to content

feat(chat): open the invite key bundle for invite-joined members - #1144

Merged
bmc08gt merged 5 commits into
mainfrom
feat/group-invites-key-bundle
Oct 29, 2026
Merged

bmc08gt merged 5 commits into
mainfrom
feat/group-invites-key-bundle

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

First of three PRs for v1 invite links to private groups. #1149 (host surfaces) stacks on this one, and C (feat/group-invites-joiner, joiner surfaces) stacks on #1149.

The bundle is opened with SharedInviteCipher from code-payments/code-android-app#1866, so this branch pins flipcash-shared-core to 0.17.0.

Key bundle

A member who joined by invite and hasn't stored their own envelope gets invite_key_bundle from GetKeyEnvelope. Before this PR, FlipClient.getKeyEnvelope and ChatNotificationClient both turned that into noEnvelope, so the group's messages never opened.

  • GroupKeyEnvelopes takes a GroupKeyMaterial (own envelope or invite copy) and opens the bundle through a new GroupKeySealing.open, which SharedCoreGroupKeySealer implements with SharedInviteCipher.openKeyBundle.
  • The creator check follows InviteKeyEnvelope in chat/v1 model.proto:
    • A bundle whose wrapped_by isn't Metadata.creator is rejected before any key is looked up.
    • The creator's public key comes from the resolver when it can be resolved. If that fails, it falls back to wrapped_by_public_key from the same response, which the contract accepts.
    • If the creator isn't known yet, this throws a retriable error and does not mark the chat wrong-key.
  • Once opened, the key is re-wrapped for the viewer with SetKeyEnvelope, best effort. If that write fails, the next launch opens the bundle again.
  • If the bundle fails a check (wrapped_by, signature, secret open, key open), it throws GroupKeyUnusable(failsAt:). ChatKeyring reports that as the new ChatOpener.unusableKey instead of "awaiting key". Messages stay covered and sending throws. C reads this state to draw frame 4d.
  • A bundle in a scheme this build doesn't know throws UnsupportedKeyEnvelopeScheme, as an own envelope in an unknown scheme already does. The chat waits for a key; it isn't marked wrong-key.

Notification extension

Its GroupKeyEnvelopes passes opensInviteBundles: false, so it never opens a bundle or writes an envelope. A push for an invite-joined member stays undecrypted until the app has stored the member's own envelope. Spec Decision 2 says the extension opens the bundle to decrypt only. To follow that instead, flip that one argument.

Schema

systemViaInvite is dropped from schema 60 (Decision 3). It is unreleased: flipcash-2026.10.4 ships 54. invited_by is required on the wire, so a set systemInvitedBy is enough to mark an invite join. userJoinedViaInvite.invitedBy is no longer optional. canInvite was already persisted by #1141, and a round-trip test now covers it.

Tests

GroupKeyEnvelopesTests adds 10 cases with a fake sealer:

  • open and self-wrap
  • reject on a failed check
  • reject when the wrapper isn't the creator, and when wrapped_by is missing
  • the resolved creator key preferred over the one sent with the bundle
  • the fallback to wrapped_by_public_key
  • self-wrap failure, then reopen
  • fetch failure, then retry
  • the extension leaves a bundle unopened
  • an unknown creator

ChatKeyringTests adds the wrong-key opener, and SharedCoreGroupKeySealerTests covers the unknown-scheme mapping.

@bmc08gt bmc08gt self-assigned this Oct 28, 2026
@bmc08gt
bmc08gt force-pushed the feat/group-invites-key-bundle branch from 82b6293 to 7844d2c Compare October 28, 2026 17:44
@bmc08gt
bmc08gt added this pull request to stack #1154 October 28, 2026 18:18
@bmc08gt
bmc08gt marked this pull request as ready for review October 28, 2026 18:47
`invited_by` is required on `UserJoined.Invited`, so a set `systemInvitedBy`
marks an invite join on its own and the extra column carried nothing. Schema
60 hasn't shipped (flipcash-2026.10.4 is on 54), so the column comes out of 60
rather than a new 61. An `invited_by` that doesn't parse now reads as a plain
join, and `userJoinedViaInvite` takes a non-optional inviter.

Adds a round-trip test for `canInvite`, which the scaffold already persists.
GetKeyEnvelope returns `invite_key_bundle` for a member who joined by invite
and hasn't stored an envelope of their own. Both callers mapped that to
`noEnvelope`, so the group's messages never opened.

`GroupKeyEnvelopes` now gets a `GroupKeyMaterial` and opens the bundle through
`SharedInviteCipher.openKeyBundle`, behind `GroupKeySealing` so tests can fake
it. The creator's key is the resolved one, falling back to
`wrapped_by_public_key`. An opened key is re-wrapped for the viewer with
SetKeyEnvelope, best effort: if that write fails, the bundle opens again next
launch.

A bundle that fails any check throws `GroupKeyUnusable`, which the keyring
surfaces as `ChatOpener.unusableKey` rather than folding it into "awaiting
key". Messages stay covered and sending fails. This is the wrong-key state
frame 4d draws.

The notification extension's instance passes `opensInviteBundles: false`, so
it never opens a bundle or writes an envelope. An invite-joined member's push
stays undecrypted until the app has stored their own envelope.
…chemes as unsupported

Follows chat/v1 model.proto on InviteKeyEnvelope: reject a bundle whose
wrapped_by isn't Metadata.creator before resolving any key, prefer the
creator key resolved outside GetKeyEnvelope, and fall back to
wrapped_by_public_key when that fails. A bundle in a scheme this build
doesn't know now throws UnsupportedKeyEnvelopeScheme, as an own envelope
does, so the chat waits for a key instead of showing the wrong-key state.
@bmc08gt
bmc08gt force-pushed the feat/group-invites-key-bundle branch from fafa9bc to 6cd8545 Compare October 29, 2026 01:16
@bmc08gt
bmc08gt merged commit b41020e into main Oct 29, 2026
7 of 8 checks passed
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.

1 participant