Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,10 @@ Rather than distributing the sender key to all group devices on every message, t
On the **first** group send (or any send where the cached map is empty), the filter still runs unconditionally — every resolved participant device is treated as `has_key=false` and receives the SKDM. This matches WhatsApp Web, which iterates an empty `senderKey` Map as `false` per participant. There is no early-exit for an empty cache; otherwise the very first message after a fresh start would skip distribution entirely.
</Note>

<Note>
**Own devices are never marked `has_key=true` ([#999](https://github.com/oxidezap/whatsapp-rust/pull/999)).** The post-ACK warm mark excludes the account's own companion devices, matching WhatsApp Web's `!isMeDevice` guard on `markHasSenderKey`. They therefore never leave the "not yet tracked" bucket above and are re-included as SKDM targets on every send — see the follow-up note under ["Parallelized group encrypt fan-out"](#parallelized-group-encrypt-fan-out) for why. External group members are unaffected: a successful distribution still marks them warm.
</Note>

Location: `src/send.rs`, `src/client/sender_keys.rs`, `wacore/src/send.rs`

### Parallelized group encrypt fan-out
Expand Down Expand Up @@ -687,6 +691,10 @@ Prior to [#807](https://github.com/oxidezap/whatsapp-rust/pull/807), a single ch
**Session-setup failures are isolated per device (v0.6).** `ensure_sessions_for_devices` used to abort with `Err` the moment `process_prekey_bundle` failed for *any* one target device. Since `prepare_group_stanza` gates the entire SKDM fan-out on `session_plan.is_some()`, one device's X3DH failure nulled the plan and **every** device in the cohort — not just the failing one — got no SKDM, even though the `skmsg` still shipped and the phash covered the full set. An external member recovers via a retry receipt, but an own companion's retry hits `mark_forget_sender_key` with `exclude_own_devices=true`, which filters own-user JIDs and returns early — so that companion stayed `has_key=true` forever and couldn't decrypt the group from that device until an unrelated full rotation (participant removal or PN↔LID migration).

As of [#996](https://github.com/oxidezap/whatsapp-rust/pull/996), a device whose session setup fails is logged and skipped rather than aborting the plan — matching WhatsApp Web's `GroupKeyDistributionMsg`, which wraps each device's `ensureE2ESessions` in its own try/catch and drops only the failing one. The sessionless device is then naturally excluded by the encrypt fan-out (which already skips devices without a session), so every other device still receives its pairwise SKDM.

[#996](https://github.com/oxidezap/whatsapp-rust/pull/996) closed the primary harm — an *unrelated* device's setup failure no longer suppresses the whole cohort's SKDM. A narrower window remained: the **warm mark** (`update_sender_key_devices`, called after the server ACK) recorded the *full* distribution target as `has_key=true`, including our own companion devices, regardless of whether each one's pairwise SKDM encryption actually succeeded. Since the forget path (`mark_forget_sender_key`) excludes own devices for the reason above, an own companion whose one SKDM encryption failed — or that was warm-marked without ever receiving a node — was marked warm and could **never** be un-marked: a permanent orphan until an unrelated full rotation. External devices didn't have this problem; they recover through the retry-receipt forget path.

[#999](https://github.com/oxidezap/whatsapp-rust/pull/999) closes this residual by excluding own devices from the warm mark too (`exclude_own_devices=true`), mirroring WhatsApp Web's `ParticipantStore` helper, which guards *both* `markHasSenderKey` and `markForgetSenderKey` with the same `!isMeDevice` check. Own companions are therefore never memoized as `has_key=true` — `filter_skdm_targets` (["Incremental targeting"](#per-device-sender-key-tracking) above) always re-includes them, so they get a fresh SKDM on every group send. This is a deliberate trade-off (a few extra pairwise SKDM nodes per send when the account has companions) in exchange for making the orphan impossible. External devices are unaffected: a successful distribution still marks them warm, and the retry-receipt path still repairs any that go stale.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
</Note>

### In-memory sender key device cache
Expand Down