Found + VERIFIED in the impl-pass security review over merged 3b (#160). CRITICAL.
Bug
crates/mae/src/collab_bridge.rs::build_kb_node_update_request chooses seal-vs-plaintext purely on
content_key.is_some() (the match (content_key, signing_identity)), with no knowledge that the KB is E2e:
_ => arm (no key): ships the plaintext update as the wire payload.
Err(_) => arm (seal failed): also ships plaintext — comment "fail safe: keep the plaintext payload rather
than dropping the edit" — that is the bug for an E2e KB.
content_keys (network task, init empty at ~:2849) is repopulated only in-session: enable (:3674), rotation
(:3476), share/join responses (:4477/:4622). There is no startup reload into the seal path (the
content_key_store::load at :3642 is only inside the enable handler). So after a restart/reconnect, editing an
already-E2e node before a share/join response repopulates the map → content_key=None → plaintext written to
the key-blind daemon, which stores + relays it. Silent confidentiality breach — exactly what ADR-037 prevents.
Fix (fail CLOSED)
Gate the seal decision on the KB's authoritative (signed) encryption mode, not map presence. When
derive_encryption(...)==E2e (or a known per-KB e2e flag) and the key is missing OR seal_op errors: do not
send — requeue + surface an error/notification. Thread an e2e: bool into build_kb_node_update_request so the
no-key and Err arms return no payload when e2e. Also repopulate content_keys at task startup from
content_key_store for owned/joined E2e KBs.
Test gap
collab_bridge_tests.rs:2768,2788 pass content_key:None and assert the plaintext path — framed as legacy/
unencrypted. No test asserts "E2e KB + missing key ⇒ MUST NOT emit plaintext."
Found + VERIFIED in the impl-pass security review over merged 3b (#160). CRITICAL.
Bug
crates/mae/src/collab_bridge.rs::build_kb_node_update_requestchooses seal-vs-plaintext purely oncontent_key.is_some()(thematch (content_key, signing_identity)), with no knowledge that the KB is E2e:_ =>arm (no key): ships the plaintext update as the wire payload.Err(_) =>arm (seal failed): also ships plaintext — comment "fail safe: keep the plaintext payload ratherthan dropping the edit" — that is the bug for an E2e KB.
content_keys(network task, init empty at ~:2849) is repopulated only in-session: enable (:3674), rotation(:3476), share/join responses (:4477/:4622). There is no startup reload into the seal path (the
content_key_store::loadat :3642 is only inside the enable handler). So after a restart/reconnect, editing analready-E2e node before a share/join response repopulates the map →
content_key=None→ plaintext written tothe key-blind daemon, which stores + relays it. Silent confidentiality breach — exactly what ADR-037 prevents.
Fix (fail CLOSED)
Gate the seal decision on the KB's authoritative (signed) encryption mode, not map presence. When
derive_encryption(...)==E2e(or a known per-KB e2e flag) and the key is missing ORseal_operrors: do notsend — requeue + surface an error/notification. Thread an
e2e: boolintobuild_kb_node_update_requestso theno-key and
Errarms return no payload when e2e. Also repopulatecontent_keysat task startup fromcontent_key_storefor owned/joined E2e KBs.Test gap
collab_bridge_tests.rs:2768,2788passcontent_key:Noneand assert the plaintext path — framed as legacy/unencrypted. No test asserts "E2e KB + missing key ⇒ MUST NOT emit plaintext."