CR-7 violation on prod: plaintext sent into an encrypted DM — absent m.room.encryption fact read as known-unencrypted #66

Closed
opened 2026-08-07 16:53:07 +02:00 by thecrealm · 1 comment
Owner

User-reported on prod via the demo: messages to a normally-encrypted DM went out as plain m.room.message. The demo wires VodozemacCryptoDriver and uses session.send, so the engine itself decided the room was unencrypted.

Suspect seam: SendQueue.send/sendAttachment/transmit guard CR-7 with currentState(principal, roomId, 'm.room.encryption', '') and only throw EncryptionStateUnknown when membership is ALSO null. Membership present + encryption fact absent is treated as known-unencrypted and sends plaintext — but §10.4 requires refusing when state is 'not yet synced, or state unavailable'. Any path that lands membership without the room's encryption state silently downgrades: sliding-sync window/subscription edges, gappy or limited v2 syncs, invite→join state deltas, or a store written before the state projection covered the room.

To confirm: inspect the affected demo store for the room's m.room.encryption fact; identify which sync strategy ran against the prod server. Fix direction needs a decision (D-entry): distinguish 'state known complete for this room' from 'fact absent' — e.g. a per-room state-synced marker the reducer sets when a full state delta commits, with absent ⇒ refuse (fail closed, CR-7). Regression e2e: joined room whose encryption state is withheld from sync must refuse with the typed error, never emit plaintext (CR-7 test tag).

User-reported on prod via the demo: messages to a normally-encrypted DM went out as plain m.room.message. The demo wires VodozemacCryptoDriver and uses session.send, so the engine itself decided the room was unencrypted. Suspect seam: SendQueue.send/sendAttachment/transmit guard CR-7 with currentState(principal, roomId, 'm.room.encryption', '') and only throw EncryptionStateUnknown when membership is ALSO null. Membership present + encryption fact absent is treated as known-unencrypted and sends plaintext — but §10.4 requires refusing when state is 'not yet synced, or state unavailable'. Any path that lands membership without the room's encryption state silently downgrades: sliding-sync window/subscription edges, gappy or limited v2 syncs, invite→join state deltas, or a store written before the state projection covered the room. To confirm: inspect the affected demo store for the room's m.room.encryption fact; identify which sync strategy ran against the prod server. Fix direction needs a decision (D-entry): distinguish 'state known complete for this room' from 'fact absent' — e.g. a per-room state-synced marker the reducer sets when a full state delta commits, with absent ⇒ refuse (fail closed, CR-7). Regression e2e: joined room whose encryption state is withheld from sync must refuse with the typed error, never emit plaintext (CR-7 test tag).
thecrealm 2026-08-07 18:19:36 +02:00
  • closed this issue
  • removed the
    now
    label
Author
Owner

Fixed in 1911dcc (D34). Code-level confirmation stood in for the prod-store inspection: the guard's 'membership present + encryption fact absent ⇒ known-unencrypted' read was reachable from every sync strategy — sliding sync commits membership on every room payload while required_state only arrives on initial payloads (the parser ignored MSC4186 initial), so the demo's likely path was a window/subscription edge. The affected demo store heals on its next connect: the sliding pos resets, initial payloads re-deliver full state, and the room's marker commits — until then sends into unmarked rooms refuse with EncryptionStateUnknown (fail closed, CR-7). On-demand probe healing is #68. Regression coverage: CryptoGuardConformanceTest (membership-without-marker refusal, sliding window-edge e2e shape, marker-heals path), source parser semantics, reducer stickiness, store conformance + migration v8.

Fixed in 1911dcc (D34). Code-level confirmation stood in for the prod-store inspection: the guard's 'membership present + encryption fact absent ⇒ known-unencrypted' read was reachable from every sync strategy — sliding sync commits membership on every room payload while required_state only arrives on initial payloads (the parser ignored MSC4186 `initial`), so the demo's likely path was a window/subscription edge. The affected demo store heals on its next connect: the sliding pos resets, initial payloads re-deliver full state, and the room's marker commits — until then sends into unmarked rooms refuse with EncryptionStateUnknown (fail closed, CR-7). On-demand probe healing is #68. Regression coverage: CryptoGuardConformanceTest (membership-without-marker refusal, sliding window-edge e2e shape, marker-heals path), source parser semantics, reducer stickiness, store conformance + migration v8.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
thecrealm/katrix#66
No description provided.