Wire Format
Cryptid uses a double-encrypted envelope design that provides strong privacy guarantees. This document specifies the wire format — what servers see, what clients see, and how messages are protected.
Design Goals
Section titled “Design Goals”- Server ignorance: Servers learn nothing about message content, type, or participants
- End-to-end encryption: Only recipients can decrypt message content
- Metadata privacy: Delivery addresses are the only visible routing information
Server View: CryptidEnvelope
Section titled “Server View: CryptidEnvelope”When a message is sent, servers only ever see the outer envelope:
struct CryptidEnvelope { // Delivery address for routing recipient_address: DeliveryAddress,
// Ciphertext encrypted to recipient's public key (not MLS) encrypted_blob: Vec<u8>,}What Servers Learn
Section titled “What Servers Learn”recipient_address: Where to deliver the messagetimestamp: When the server received the message (for ordering)- Nothing else: No message type, no sender, no group, no content
Server Limitations
Section titled “Server Limitations”Servers cannot:
- Know what type of message it is (CryptidMessage, SystemOperation, ContactRequest)
- Know who sent the message
- Know which group it belongs to
- Read or modify the message content
- Correlate messages to specific users or groups
- Track communication patterns beyond delivery addresses
Client View: InnerEnvelope
Section titled “Client View: InnerEnvelope”When the recipient device receives the message, it:
- Decrypts the outer layer using its device private key
- Extracts the InnerEnvelope
- Uses the group_id to route to the correct MLS group
- Decrypts the MLS ciphertext to get the actual message
struct InnerEnvelope { // Sender's device (from MLS authenticated data) sender_device_id: DeviceId,
// Group ID (None for ContactRequest) group_id: Option<GroupId>,
// MLS-encrypted message content mls_ciphertext: Vec<u8>,}Handling group_id = None:
When group_id is None, this indicates a ContactRequest (first contact). The client:
- Creates a new DirectMessageGroup with deterministic ID (Blake3 of sorted user IDs)
- Stores the sender’s UserIdentity in the contact store
- Adds the sender’s devices to the new group
- Establishes the MLS group state via MLSWelcome
Message Type Classification
Section titled “Message Type Classification”Inside the MLS ciphertext (inside InnerEnvelope), messages are distinguished by the MessageType enum:
enum MessageType { // Application-level messages (user content) CryptidMessage,
// Protocol/MLS operations (group state changes) SystemOperation,
// Contact establishment (new contacts/DM groups) ContactRequest,}Security Properties
Section titled “Security Properties”Against Servers
Section titled “Against Servers”- Servers cannot read message content
- Servers cannot determine message type
- Server cannot identify sender
- Server cannot determine group membership
Against Network Observers
Section titled “Against Network Observers”- Ephemeral delivery addresses prevent long-term tracking
- Message type and content hidden
- Fixed-size envelopes possible
Against Compromised Recipients
Section titled “Against Compromised Recipients”- MLS provides forward secrecy and post compromise security
- Individual devices can be removed
Encryption Layers
Section titled “Encryption Layers”Layer 1: Identity Encryption (Outer)
Section titled “Layer 1: Identity Encryption (Outer)”- Algorithm: X25519 (ECDH with recipient’s public key)
- Key: Recipient’s long-term device identity key
- Purpose: Secure envelope delivery to correct device
- Who sees: Only recipient device
Layer 2: MLS Encryption (Inner)
Section titled “Layer 2: MLS Encryption (Inner)”- Algorithm: MLS cipher suite (ChaCha20-Poly1305)
- Key: MLS group key derived from group state
- Purpose: Group confidentiality and authentication
- Who sees: All group members
Delivery Address Structure
Section titled “Delivery Address Structure”struct DeliveryAddress { // Address identifier (e.g., "a1b2c3d4e5...") prefix: String,
// Server domain (e.g., "chat.example.com") server: String,
// Timestamp of when the address was generated created_at: u64,
// Can be deactivated without deletion active: bool,}
impl DeliveryAddress { fn full_address(&self) -> String { format!("{}@{}", self.prefix, self.server) }}Not Server Responsibilities
Section titled “Not Server Responsibilities”The server is intentionally limited:
- Does NOT store group state
- Does NOT track message history
- Does NOT know group membership
- Does NOT verify message authenticity
- Does NOT enforce permissions
- Does NOT validate message content
The server is a dumb pipe: receive blob, deliver blob, forget.
Future Considerations
Section titled “Future Considerations”The wire format may evolve to add:
- Padding: Fixed-size envelopes to prevent traffic analysis
- Padding schemes: Constant-time padding algorithms
- Additional metadata: For advanced routing scenarios
Any changes will maintain the core property: servers learn nothing about message content.