Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,12 @@ Don't add speculative configurability or one-off helpers.

## 4. Security invariants — never weaken to make something pass

- **Verification is not optional.** `decryptEvent` rejects unverified events by
default; a below-floor key version must never verify; an invalid signature must
never yield plaintext. Verify against caller-supplied signing keys — **never** a
key carried inside the event. Verification covers every signed event type, not
- **Verification is not optional.** `decryptEvent` rejects unverified encrypted
messages and key changes by default, and any event whose signature is present
but invalid; other events without a checkable signature are returned with
`verified: false`, never silently dropped. A below-floor key version must never
verify; an invalid signature must never yield plaintext. Verify against
caller-supplied signing keys — **never** a key carried inside the event. Verification covers every signed event type, not
just messages. Signature mismatches are bugs to investigate, not checks to silence.
- **Key downgrade protection is automatic.** Conversation-key versions move
forward only (monotonic high-water mark held for the `Chat` lifetime); the
Expand Down
23 changes: 23 additions & 0 deletions crates/core/examples/gen_sdk_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const EVENT_MESSAGE_TEXT: &str = "fixture event message";
const EVENT_REPLY_TEXT: &str = "fixture reply message";
const EVENT_REPLY_FORGED_PREVIEW_TEXT: &str = "forged preview text";
const EVENT_GARBAGE: &str = "!!!not-an-event!!!";
const EVENT_UNENCRYPTED_MESSAGE_TEXT: &str = "fixture unencrypted message";
const EVENT_READ_SEEN_UNTIL_ID: &str = "42";

// Raw Thrift enum values embedded in the failure vector; suites assert the
// decoded names ("RateLimitUpsell" / "Premium", camelCased in JS).
Expand Down Expand Up @@ -174,6 +176,23 @@ fn main() {
)
.expect("frame failure event");

// Unencrypted message and unsigned read receipt: both decode under the
// default reject-unverified policy with `verified: false`.
let event_unencrypted_message_b64 = internals::frame_unencrypted_message(
"plain-msg-1",
EVENT_SENDER_ID,
EVENT_CONVERSATION_ID,
EVENT_UNENCRYPTED_MESSAGE_TEXT,
)
.expect("frame unencrypted message event");
let event_unsigned_read_receipt_b64 = internals::frame_unsigned_read_receipt(
"read-msg-1",
EVENT_SENDER_ID,
EVENT_CONVERSATION_ID,
EVENT_READ_SEEN_UNTIL_ID,
)
.expect("frame unsigned read receipt event");

let obj = json!({
"identity_private_b64": B64.encode(identity_private),
"signing_private_b64": B64.encode(signing_private),
Expand All @@ -199,6 +218,10 @@ fn main() {
"event_signing_key_version": EVENT_SIGNING_KEY_VERSION,
"event_recipient_key_version": EVENT_RECIPIENT_KEY_VERSION,
"event_message_text": EVENT_MESSAGE_TEXT,
"event_unencrypted_message_b64": event_unencrypted_message_b64,
"event_unencrypted_message_text": EVENT_UNENCRYPTED_MESSAGE_TEXT,
"event_unsigned_read_receipt_b64": event_unsigned_read_receipt_b64,
"event_read_seen_until_id": EVENT_READ_SEEN_UNTIL_ID,
});

println!("{}", serde_json::to_string_pretty(&obj).unwrap());
Expand Down
10 changes: 6 additions & 4 deletions crates/core/src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,12 @@ impl Chat {
}
}

/// When enabled — the default — `decrypt_event` returns an error for any
/// signed event whose signature cannot be verified (invalid, missing, or
/// no matching signing key) instead of returning it with
/// `verified: false`.
/// When enabled — the default — `decrypt_event` returns an error for an
/// encrypted message or key change whose signature cannot be verified
/// (invalid, missing, or no matching signing key), and for any other
/// signed event whose signature is present but invalid. Unencrypted
/// messages and other events without a checkable signature are returned
/// with `verified: false`. See `ChatCore::set_reject_unverified`.
pub fn set_reject_unverified(&mut self, reject: bool) {
self.inner.set_reject_unverified(reject);
}
Expand Down
Loading
Loading