// ## Device to Device Protocol // // ### General Information // // **Encryption cipher:** XSalsa20-Poly1305, unless otherwise specified. // // All strings are UTF-8 encoded. syntax = "proto3"; package d2d; option java_package = "ch.threema.protobuf.d2d"; import "common.proto"; import "md-d2d-sync.proto"; // D2D protocol versions. // // Note 1: The most significant byte is the major version and the least // significant byte is the minor version. // // Note 2: Once the D2D protocol is more stable, an unknown major version can be // interpreted as incompatible. For now, we only have 0.X versions that define // in which way they break compatibility. enum ProtocolVersion { // The version is unspecified. UNSPECIFIED = 0; // V0.1 // // Devices using this version use the opportunistic (but problematic) group // sync mechanism via pure CSP reflection. D2D group sync reflection was // totally underspecified but is partially supported on the receiving side. V0_1 = 0x0001; // V0.2 // // Builds on V0.1 with backwards compatibility to V0.1. Devices using this // version use the explicit group sync mechanism via D2D sync reflection. // // Upon reception, V0.2 devices detecting a reflected message will switch over // the version, and: // // - for V0.1 in combination with a CSP group message, fall back to the // backwards compatibility mode and update the group according to the // message. // - for V0.2+ in combination with a CSP group message, ignore it. V0_2 = 0x0002; // V0.3 // // Builds on V0.2 but removes the backwards compatibility to V0.1. // // Upon reception, V0.2 devices detecting a reflected message will switch over // the version, and: // // - for V0.1 in combination with a CSP group message, spit out a warning that // the group is going to desync. // - for V0.2+ in combination with a CSP group message, ignore it. V0_3 = 0x0003; } // Data shared across all devices and transmitted during the handshake. message SharedDeviceData { // Random amount of padding, ignored by the receiver bytes padding = 1; // Current lowest protocol version that must be supported by all devices uint32 version = 2; } // Metadata about a device, determined by the device itself. message DeviceInfo { // Random amount of padding, ignored by the receiver bytes padding = 1; // Platform enum Platform { // Unknown platform UNSPECIFIED = 0; // Android ANDROID = 1; // Apple iOS IOS = 2; // Desktop application DESKTOP = 3; // Web application WEB = 4; } Platform platform = 2; // Platform details (smartphone model / browser), e.g. "Firefox 91.0.2" or // "iPhone 11 Pro" string platform_details = 3; // App version, e.g. "4.52" (Android) or "4.6.12b2653" (iOS) string app_version = 4; // User defined device label (e.g. "PC at Work"), may be empty if not set. // Recommended to not not exceed 64 grapheme clusters. string label = 5; } // A transaction scope. Used in the d2m transaction messages. message TransactionScope { enum Scope { USER_PROFILE_SYNC = 0; CONTACT_SYNC = 1; GROUP_SYNC = 2; DISTRIBUTION_LIST_SYNC = 3; SETTINGS_SYNC = 4; MDM_PARAMETER_SYNC = 5; NEW_DEVICE_SYNC = 6; } Scope scope = 1; } // Root message message Envelope { // Random amount of padding, ignored by the receiver bytes padding = 1; // Sender device id fixed64 device_id = 13; // D2D (`ProtocolVersion`) the device used when it sent this message. // // If `0`, assume V0.1 (`0x0001`). uint32 protocol_version = 3; // The enveloped reflected message oneof content { OutgoingMessage outgoing_message = 2; OutgoingMessageUpdate outgoing_message_update = 10; IncomingMessage incoming_message = 4; IncomingMessageUpdate incoming_message_update = 11; UserProfileSync user_profile_sync = 5; ContactSync contact_sync = 6; GroupSync group_sync = 7; DistributionListSync distribution_list_sync = 8; SettingsSync settings_sync = 9; MdmParameterSync mdm_parameter_sync = 12; }; } // Unique conversation identifier. message ConversationId { // A contact's Threema ID, distribution list ID or group identity to identify // the conversation. oneof id { string contact = 1; fixed64 distribution_list = 2; common.GroupIdentity group = 3; } } // An outgoing message, reflected to other devices. // // When sending this message: // // 1. [...] // 2. Set `nonces` to the nonces of the associated CSP // `e2e.message-with-metadata` (or `e2e.legacy-message`) messages that // contained the `body` in encrypted form.¹ // // When receiving this message: // // 1. Add all `nonces` to the CSP nonce storage (discarding any nonces that // already exist in the nonce storage). // 2. If a message with the same `message_id` exists within the associated // `conversation`, discard the message and abort these steps. // 3. [...] // // ¹: For contacts and distribution lists, there will be exactly one nonce. For // groups, there will be as many nonces as there are group members minus one. message OutgoingMessage { // Conversation ID of the enclosed message. // // Note: If the conversation is of type group, group and group creator id of // the enclosed CSP E2E message must match the values of the supplied group // identity. Otherwise, the message must be considered invalid. ConversationId conversation = 1; // Unique ID of the enclosed message fixed64 message_id = 2; // Optional thread message ID (the message ID of the last incoming message in // the current conversation) optional fixed64 thread_message_id = 6; // Unix-ish timestamp in milliseconds for when the enclosed message has been // created // // Note: Take this value from the // `csp.payload.legacy-message`/`csp.payload.message-with-metadata-box` that // enclosed the message. uint64 created_at = 3; // Enclosed message's type common.CspE2eMessageType type = 4; // The message's body, i.e. the unpadded `csp.e2e.container.padded-data` bytes body = 5; // Nonces the message was encrypted with towards each receiver (the shared // secret derived from the long-term keys). // // Optional for now, always required in a future version. repeated bytes nonces = 7; } // Update one or more existing outgoing messages. message OutgoingMessageUpdate { // Mark the referred message as sent (acknowledged by the chat server). // // Note 1: The timestamp of the `reflect-ack`/`reflected` message determines // the timestamp for when the referred message has been sent. // // Note 2: This indicates that the referred message has been successfully // stored in the message queue of the server. It does NOT indicate that the // referred message has been delivered to the intended receiver. message Sent {} message Update { // Conversation ID of the referred message. ConversationId conversation = 1; // Unique ID of the referred message fixed64 message_id = 2; // Update type oneof update { // Mark the referred message as sent Sent sent = 3; } } // Updates repeated Update updates = 1; } // An incoming message, reflected to other devices. // // // When sending this message: // // 1. [...] // 2. Set `nonce` to the nonce of `e2e.message-with-metadata` (or // `e2e.legacy-message`) that contained the `body` in encrypted form. // // When receiving this message: // // 1. Add `nonce` to the CSP nonce storage (discard a nonces that already exist // in the nonce storage). // 2. If a message with the same `message_id` exists within the associated // `conversation`, discard the message and abort these steps. // 3. [...] message IncomingMessage { reserved 4; // Skipped by accident, available to use! // Sender's Threema ID string sender_identity = 1; // Unique ID of the enclosed message fixed64 message_id = 2; // Unix-ish timestamp in milliseconds for when the enclosed message has been // created. // // Note: Take this value from the // `csp.payload.legacy-message`/`csp.payload.message-with-metadata-box` that // enclosed the message. uint64 created_at = 3; // Enclosed message's type. common.CspE2eMessageType type = 5; // The message's body, i.e. the unpadded `csp.e2e.container.padded-data` bytes body = 6; // Nonce the message was encrypted with by the sender (the shared secret // derived from the long-term keys). // // Optional for now, always required in a future version. bytes nonce = 7; } // Update one or more existing incoming messages. message IncomingMessageUpdate { // Mark the referred message as read. // // Note: This may only be used when _read receipts_ have been turned off, i.e. // as a replacement for reflecting `delivery-receipt` type _read_ (`0x02`). message Read { // Unix-ish timestamp in milliseconds for when the referred message has been // read. uint64 at = 1; } message Update { // Conversation ID of the referred message. ConversationId conversation = 1; // Unique ID of the referred message fixed64 message_id = 2; // Update type oneof update { // Mark the referred message as read Read read = 3; } } // Updates repeated Update updates = 1; } // User profile synchronisation message. message UserProfileSync { // Update the user's profile message Update { sync.UserProfile user_profile = 1; } // Synchronisation type oneof action { Update update = 1; } } // Contact synchronisation message. message ContactSync { // Create a Threema contact. message Create { sync.Contact contact = 1; } // Update a Threema contact. message Update { sync.Contact contact = 1; } // Synchronisation type oneof action { // Create a Threema contact Create create = 1; // Update a Threema contact Update update = 2; } } // Group synchronisation message. message GroupSync { // Create a group. message Create { sync.Group group = 1; } // Update a group. // // When receiving this variant: // // 1. Let `current` be a snapshot of the current group state. // 2. Persist the update to the group. // 3. Let `member-changes` be an empty list. // 4. For each `identity`, `state-change` pair of `member_state_changes`: // 1. If `state-change` is `ADDED` and `identity` does not exist in // `current.members`, add the tuple `identity`, `state-change` to // `member-changes`. // 2. If `state-change` is `KICKED` or `LEFT` and `identity` does exist in // `current.members`, add the tuple `identity`, `state-change` to // `member-changes`. // 5. Group `member-changes` by their associated `state-change` and add // appropriate status messages to the associated conversation. message Update { sync.Group group = 1; enum MemberStateChange { // The member has been added ADDED = 0; // The member has been kicked from the group. KICKED = 1; // The member left the group. LEFT = 2; } // A map of the member identity string to the member state change. map member_state_changes = 2; } // Delete a group. message Delete { // Unique group identity common.GroupIdentity group_identity = 1; } // Synchronisation type oneof action { // Create a group Create create = 1; // Update a group Update update = 2; // Delete a group Delete delete = 3; } } // Distribution list synchronisation message. message DistributionListSync { // Create a distribution list. message Create { sync.DistributionList distribution_list = 1; } // Update a distribution list. message Update { sync.DistributionList distribution_list = 1; } // Delete a distribution list. message Delete { // Unique ID of the distribution list fixed64 distribution_list_id = 1; } // Synchronisation type oneof action { // Create a distribution list Create create = 1; // Update a distribution list Update update = 2; // Delete a distribution list Delete delete = 3; } } // Settings synchronisation message. message SettingsSync { // Update settings. message Update { sync.Settings settings = 1; } // Synchronisation type oneof action { Update update = 1; } } // MDM parameter synchronisation message. message MdmParameterSync { // Update MDM parameters. // // When receiving this variant, run the _MDM Merge And Apply Steps_. message Update { sync.MdmParameters parameters = 1; } // Synchronisation type oneof action { Update update = 1; } }