# Meta information meta: # Document name and ID id: csp name: Chat Server Protocol # References used by the structs references: # A public or secret key key: &key b32 # A random cookie cookie: &cookie b16 # A random nonce nonce: &nonce b24 # A Threema ID identity: &identity b8 # Multiple Threema IDs identities: &identities b8[] # A message ID message-id: &message-id u64-le # Multiple message IDs message-ids: &message-ids u64-le[] # A blob ID blob-id: &blob-id b16 # A poll ID poll-id: &poll-id u64-le # A group ID group-id: &group-id u64-le # Virtual namespace, just containing the below docstring index: &index _doc: |- # Chat Server Protocol The Chat Server Protocol is a custom transport encrypted frame-based protocol, originally designed to operate on top of TCP. it uses the NaCl cryptography library to provide authentication, integrity and encryption. The login [**handshake**](ref:handshake) takes two round trips and establishes ephemeral encryption keys along the way. Authentication is solely based on the secret key associated to a Threema ID. After the handshake process, [**payloads**](ref:payload) can be exchanged bidirectionally although some payload structs may only be used in one direction. A client may now send and receive end-to-end encrypted [**messages**](ref:e2e) (wrapped in [message payload structs](ref:payload.container)). ## Terminology - `CK`: Client Key (permanent secret key associated to the Threema ID) - `SK`: Permanent Server Key - `TCK`: Temporary Client Key - `TSK`: Temporary Server Key - `CCK`: Client Connection Cookie - `SCK`: Server Connection Cookie - `CSN`: Client Sequence Number - `SSN`: Server Sequence Number - `ID`: The client's Threema ID ## General Information **Endianness:** All integers use little-endian encoding. **Encryption cipher:** XSalsa20-Poly1305, unless otherwise specified. **Nonce format:** - a 16 byte cookie (CCK/SCK), followed by - a monotonically increasing sequence number (CSN/SSN, u64-le). **Sequence number:** The sequence number starts with `1` and is counted separately for each direction (i.e. there is one sequence number counter for the client and one for the server). We will use `CSN+` and `SSN+` in this document to denote that the counter should be increased **after** the value has been inserted (i.e. semantically equivalent to `x++` in many languages). ## Size Limitations The chat server protocol currently allows for up to 8192 bytes within a single frame. Because we make heavy use of Protobuf messages, the overhead cannot be calculated reliably ahead of time. Therefore, the total amount of user-defined bytes should be constrained to ~7000 bytes. To achieve this, the maximum recommended size of each property will be defined for each message, so that it's total size roughly matches that constraint. # Handshake structs handshake: &handshake _doc: |- ## Handshake To perform authentication handshake, the following handshake structs have to be exchanged in this order: C -- client-hello -> S C <- server-hello -- S C ---- login ---- -> S C <-- login-ack ---- S Note that handshake structs have no wrapping frame container struct. client-hello: _doc: |- Initial message from the client, containing a server authentication challenge in order to establish transport layer encryption. Direction: Client --> Server fields: - _doc: |- 32 byte temporary public key (`TCK.public`). name: tck type: *key - _doc: |- 16 byte random cookie used for nonces (also acting as server authentication challenge). name: cck type: *cookie server-hello: _doc: |- Initial message from the server, containing the server's authentication challenge response. This concludes establishing transport layer encryption based on `TCK` and `TSK`. Direction: Client <-- Server When creating this message: 1. Ensure that CCK and SCK are not equal. When receiving this message: 1. If CCK and SCK are equal, abort the connection and these steps. 2. If the repeated random cookie of the client does not equal CCK, abort the connection and these steps. fields: - _doc: |- 16 byte random cookie used for nonces (also acting as client authentication challenge) name: sck type: *cookie - _doc: |- The server's challenge response (`server-challenge-response`), encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(SK.secret, TCK.public), nonce=SCK || u64-le(SSN+), ) name: server-challenge-response-box type: b64 server-challenge-response: _doc: |- Authentication challenge response from the server. fields: - _doc: |- 32 byte temporary public key (`TSK.public`) name: tsk type: *key - _doc: |- 16 byte repeated random cookie of the client (acting as the server's challenge response) name: cck type: *cookie login: _doc: |- Login request from the client. IMPORTANT: `CSN` is used and increased for `box` and then for `extension-box`. It must follow this exact order. Direction: Client --> Server fields: - _doc: |- The [`login-data`](ref:handshake.login-data), encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(TCK.secret, TSK.public), nonce=CCK || u64-le(CSN+), ) name: box type: b144 - _doc: |- An optional arbitrary amount of [`extension`](ref:handshake.extension)s, encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(TCK.secret, TSK.public), nonce=CCK || u64-le(CSN+), ) These fields are only present if the [`extension-indicator`](ref:handshake.extension-indicator) of the [`login-data`](ref:handshake.login-data) field is present. If so, extensions should be consumed until the extension indicator `length` field is zero. name: extensions-box type: b* login-data: _doc: |- Login data of the client. fields: - _doc: |- Threema ID of the client. name: identity type: *identity - _doc: |- This is either the old client info field or an extension indicator. If the first 30 bytes of the field start with the string `threema-clever-extension-field`, then parse this field as an [`extension-indicator`](ref:handshake.extension-indicator) and parse `extensions-box` appropriately. Otherwise, this represents an old client info and the content is identical to the content of [`client-info`](ref:handshake.client-info). Since the field has a fixed size, the string is zero-padded. name: client-info-or-extension-indicator type: b32 - _doc: |- 16 byte repeated random cookie of the server (acting as the client's challenge response) name: sck type: *cookie - _doc: |- 24 zero bytes (previously used as vouch nonce, now set to zero indicating that the new vouch format is being used) name: reserved1 type: b24 - _doc: |- The vouch value, calculated as follows: SS1 = X25519HSalsa20(CK.secret, SK.public) SS2 = X25519HSalsa20(CK.secret, TSK.public) VouchKey = BLAKE2b(key=SS1 || SS2, salt='v2', personal='3ma-csp') vouch = BLAKE2b( out-length=32, key=VouchKey, input=SCK || TCK.public, ) name: vouch type: b32 - _doc: |- 16 zero bytes (previously part of the vouch box, now set to zero for compatibility) name: reserved2 type: b16 extension-indicator: _doc: |- Indicates that extensions are present fields: - _doc: |- Magic string: `threema-clever-extension-field` name: magic type: b30 - _doc: |- Amount of encrypted bytes present for extensions. Extension fields need to be consumed until `length` is zero. name: length type: u16-le extension: _doc: |- An extension field. fields: - _doc: |- Type of the extension. Must correspond to the encoded extension struct of the `payload` field: - `0x00`: `client-info` - `0x01`: `csp-device-id` - `0x02`: `message-payload-version` - `0x03`: `device-cookie` name: type type: u8 - _doc: |- Length of the extension's `payload` field. name: length type: u16-le - _doc: |- Extension payload. Needs to be parsed according to the `type` field. name: payload type: b{length} client-info: _doc: |- Client info extension payload. fields: - _doc: |- Client info string in the following format (without line breaks): ; ; /; The `` looks like this for mobile clients (A/I/W): ; The `` looks like this for web/desktop clients (Q): ; ; ; The `` looks like this for Bots (B): ; The fields may contain the following values: - `app-version`: Arbitrary version string, depending on the platform - `platform`: * `A`: Android * `I`: iOS * `Q`: Desktop/Web * `W`: Windows Phone * `B`: Bot - `lang`: ISO 639-1:2002-ish language code - `country-code`: ISO 3166-1-ish country code - `device-model`: Arbitrary smartphone model - `os-version`: Arbitrary OS version string - `renderer`: Renderer name for Desktop/Web (e.g. `Firefox` or `Electron`) - `renderer-version`: Renderer major version (e.g. `107`) - `os-name`: Name of the operating system (e.g. `Linux` or `Windows`) - `os-architecture`: Architecture of the operating system (e.g. `x64`) name: client-info type: b* csp-device-id: _doc: |- CSP device ID extension payload. fields: - _doc: |- CSP device ID, randomly generated **once** when the device got the Mediator device ID. name: csp-device-id type: u64-le message-payload-version: _doc: |- Message payload struct version to be used. In case this extension is not present, the server must assume that version `0x00` has been selected. In case the server receives an unknown or unsupported protocol version, it shall complete the handshake and then immediately send a `close-error` payload. fields: - _doc: |- Indicates the payload struct version the client will send and expects to receive when exchanging message payload structs with the server: - `0x00`: `legacy-message` - `0x01`: `message-with-metadata-box` name: version type: u8 device-cookie: _doc: |- A 16 byte random value chosen by the client and stored in a secure, device-specific location (not included in any backups etc., not viewable/exportable). Its purpose is to allow detection when a different (rogue) device has connected to the chat server, e.g. because an attacker has obtained the secret key of a user. The server will store the device cookie of the last connection, and if a different cookie is sent by the client, it will set a flag on the identity and send a [`device-cookie-change-indication`](ref:payload.device-cookie-change-indication) payload to the client every time it connects. The client should then show a warning in form of a notification or a dialog to the user. Note that the normal protocol flow should continue regardless of whether the user has acknowledged the warning or not. If this extension is not sent by the client, then the server's behavior depends on whether it has already stored a device cookie for this identity or not. If not, then nothing will happen. If yes, then it will act as if the client had sent an all-zero device cookie. fields: - _doc: |- Device cookie, randomly generated **once** per device. name: device-cookie type: b16 login-ack: _doc: |- Login acknowledgement from the server. Direction: Client <-- Server fields: - _doc: |- Reserved (16 zero bytes), encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(TSK.secret, TCK.public), nonce=SCK || u64-le(SSN+), ) name: reserved-box type: b32 # Payload structs payload: &payload _doc: |- ## Payload After the handshake process, payloads may be sent and received without any strictly defined order. Note that payload structs are mandatory to encrypt and frame. To achieve this, first wrap the payload struct in a [`container`](ref:payload.container) struct, encrypt it and wrap the encrypted bytes in a [`frame`](ref:payload.frame) struct. frame: _group: Header _doc: |- Contains an encrypted [payload](ref:payload#payload) wrapped in a [container](ref:payload.container). Direction: Client <-> Server fields: - _doc: |- Length of the `box` field. name: length type: u16-le - _doc: |- The encrypted [payload](ref:payload#payload). For messages from the server to the client, encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(TSK.secret, TCK.public), nonce=SCK || u64-le(SSN+), ) For messages from the client to the server, encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(TSK.secret, TCK.public), nonce=CCK || u64-le(CSN+), ) name: box type: b{length} container: _group: Header _doc: |- Contains an inner [payload](ref:payload#payload) struct. Direction: Client <-> Server fields: - _doc: |- Type of the payload. Must correspond to the encoded payload struct of the `data` field: - `0x00`: [`echo-request`](ref:payload.echo-request) - `0x80`: [`echo-response`](ref:payload.echo-response) - `0x01`: outgoing [`legacy-message`](ref:payload.legacy-message) or [`message-with-metadata-box`](ref:payload.message-with-metadata-box) - `0x81`: outgoing [`message-ack`](ref:payload.message-ack) - `0x02`: incoming [`legacy-message`](ref:payload.legacy-message) or [`message-with-metadata-box`](ref:payload.message-with-metadata-box) - `0x82`: incoming [`message-ack`](ref:payload.message-ack) - `0x03`: [`unblock-incoming-messages`](ref:payload.unblock-incoming-messages) - `0x20`: [`set-push-notification-token`](ref:payload.set-push-notification-token) - `0x21`: (obsolete, formerly used by iOS to set a push filter) - `0x22`: (obsolete, formerly used by iOS to set a push sound for contacts) - `0x23`: (obsolete, formerly used by iOS to set a push sound for groups) - `0x24`: high-priority token for notifications that require immediate delivery (e.g. for calls) using the same struct as [`set-push-notification-token`](ref:payload.set-push-notification-token) - `0x25`: [`delete-push-notification-token`](ref:payload.delete-push-notification-token) - `0x30`: [`set-connection-idle-timeout`](ref:payload.set-connection-idle-timeout) - `0x31`: (obsolete, formerly used to ensure that a push message is sent for all messages, regardless of the flag) - `0xd0`: [`queue-send-complete`](ref:payload.queue-send-complete) - `0xd1`: (obsolete, formerly used for a function similar to the device cookie) - `0xd2`: [`device-cookie-change-indication`](ref:payload.device-cookie-change-indication) - `0xd3`: [`clear-device-cookie-change-indication`](ref:payload.clear-device-cookie-change-indication) - `0xe0`: [`close-error`](ref:payload.close-error) - `0xe1`: [`alert`](ref:payload.alert) name: type type: u8 - _doc: |- Reserved, currently all zeroes. name: reserved type: b3 - _doc: |- Inner payload. Needs to be parsed according to the `type` field. name: data type: b* echo-request: _group: Payloads _doc: |- An echo request to be answered by a corresponding echo response. Can be used for connection keep-alive or RTT estimation. Direction: Client <-> Server [//]: # "TODO(SE-128)" fields: - _doc: |- Data to be echoed back in the echo response. name: data type: b* echo-response: _group: Payloads _doc: |- An echo response corresponding to an echo request. Direction: Client <-> Server [//]: # "TODO(SE-128)" fields: - _doc: |- Data echoed back from the echo request. name: data type: b* legacy-message: _group: Payloads _doc: |- An end-to-end encrypted Threema message. Direction: Client <-> Server Note: This payload is deprecated and may be phased out eventually. It will only be used in case the [`message-payload-version`](ref:handshake.message-payload-version) was not present during login or was explicitly set to the version `0x00`. Conversion to [`message-with-metadata-box`](ref:payload.message-with-metadata-box): - Copy `legacy-message.sender-nickname` to `message-with-metadata-box.legacy-sender-nickname` - Copy all other fields of `legacy-message` to their respective counterparts in `message-with-metadata-box` - Set `message-with-metadata-box.metadata-length` to `0` - Omit `message-with-metadata-box.metadata-container` (i.e. set it to contain 0 bytes) - Copy `legacy-message.message-nonce` to `message-with-metadata-box.message-and-metadata-nonce`. When sending or receiving this payload, convert it to a `message-with-metadata-box` and handle it as defined by that struct. [//]: # "TODO(SE-128)" fields: - &message-sender-identity _doc: |- The sender's Threema ID. name: sender-identity type: *identity - &message-receiver-identity _doc: |- The receiver's Threema ID. name: receiver-identity type: *identity - &message-message-id _doc: |- Unique message ID for each sender/receiver pair. Used for duplicate detection and for quotes. Messages sent in a group must have the same message ID for each group member. name: message-id type: *message-id - &message-created-at _doc: |- Unix timestamp in seconds for when the message has been created. Messages sent in a group must have the same timestamp for each group member. However, the server overrides this timestamp with the current time if - the declared timestamp is in the future, or - the _short-lived server queuing_ flag was set (`0x20`). Note: The original timestamp is still available in an attached `csp-e2e.MessageMetadata`. name: created-at type: u32-le - &message-flags _doc: |- Flags: - `0x01`: Send push notification. The server will send a push message to the receiver of the message. Only use this for messages that require a notification. For example, do not set this for delivery receipts. - `0x02`: No server queuing. Use this for messages that can be discarded by the chat server in case the receiver is not connected to the chat server, e.g. the _typing_ indicator. - `0x04`: No server acknowledgement. Use this for messages where reliable delivery and acknowledgement is not essential, e.g. the _typing_ indicator. Will not be acknowledged by the chat server when sending. No acknowledgement should be sent by the receiver to the chat server. - `0x10`: Reserved (formerly _group message marker_). - `0x20`: Short-lived server queuing. Messages with this flag will only be queued for 60 seconds. - `0x80`: No automatic delivery receipts. A receiver of a message with this flag must not send automatic delivery receipt of type _received_ (`0x01`) or _read_ (`0x02`). This is not used by the apps but can be used by Threema Gateway IDs which do not necessarily want a delivery receipt for a message. name: flags type: u8 - &message-reserved _doc: |- Reserved, must be set to zero. name: reserved type: u8 - _doc: |- Reserved for header compatibility with metadata message. Must be set to zero by legacy clients. name: reserved-metadata-length type: b2 - _doc: |- The sender's public nickname, padded with zeroes if needed. name: sender-nickname type: b32 - _doc: |- Nonce used for the message box. name: message-nonce type: *nonce - &message-message-box _doc: |- The message, end-to-end encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(.secret, .public), nonce=, ) name: message-box type: b* message-with-metadata-box: _group: Payloads _doc: |- An end-to-end encrypted Threema message with additional end-to-end encrypted metadata. Direction: Client <-> Server Note: This payload will only be used in case the [`message-payload-version`](ref:handshake.message-payload-version) was set to version `0x01`. Conversion to [`legacy-message`](ref:payload.legacy-message): - Discard `message-with-metadata-box.metadata-length` and `message-with-metadata-box.metadata-container` - Copy `message-with-metadata-box.legacy-sender-nickname` to `legacy-message.sender-nickname` - Copy `message-with-metadata-box.message-and-metadata-nonce` to `legacy-message.message-nonce`. - Copy all other fields of `message-with-metadata-box` to their respective counterparts in `legacy-message` Creating this payload is only allowed as part of the _Bundled Messages Send Steps_. When receiving this payload: 1. (MD) If the device is currently not declared _leader_, exceptionally abort these steps and the connection. 2. If the nonce of `message-and-metadata-nonce` has been used before, log a warning, _Acknowledge_ and discard the message and abort these steps. 3. If `receiver-identity` does not equal the user's Threema ID, log a warning, _Acknowledge_ and discard the message and abort these steps. 4. Run the _Valid Contacts Lookup Steps_ for `sender-identity` and let `contact-or-init` be the result. 5. If `contact-or-init` indicates that the _contact is the user_ or that the _contact is invalid_, log a warning, _Acknowledge_ and discard the message and abort these steps. 6. If `metadata-length` is greater zero, decrypt the `metadata-container` and let `outer-metadata` be the result. If this fails, log a warning, _Acknowledge_ and discard the message and abort these steps. 7. Decrypt the `message-box`, decode it to a [`container`](ref:payload.container) struct and let `outer` be the result. If this fails, log a warning, _Acknowledge_ and discard the message and abort these steps. 8. If `outer.type` is `0xff`, log a warning, _Acknowledge_ and discard the message and abort these steps. (Legacy logic, may be removed in the future.) 9. If `outer.type` is unknown, log a notice, _Acknowledge_ and discard the message and abort these steps. 10. Decode `outer.padded-data` into the message type associated to `outer.type` and let `outer-message` be the result. If this fails, log a warning, _Acknowledge_ and discard the message and abort these steps. 11. If `outer.type` is not `0xa0`, let `inner-metadata` be `outer-metadata`, let `inner-type` be `outer.type` and let `inner-message` be `outer-message`. 12. If `outer.type` is `0xa0`: 1. Run the receive steps associated to `csp-e2e-fs.Envelope` with the decoded `outer-message` and let `inner-metadata`, `inner-type`, `inner-message` and `fs-commit-fn` be the result. If this fails, exceptionally abort these steps and the connection. If the message has been discarded, _Acknowledge_ and abort these steps. 2. If `inner-metadata` is not defined, set `inner-metadata` to `outer-metadata`. 13. If `message-id` does not equal `inner-metadata.message_id`, log a warning, _Acknowledge_ and discard the message and abort these steps. 14. If `message-id` refers to a message that has been received previously from `sender-identity` (including group messages), log a warning, _Acknowledge_ and discard the message and abort these steps. 15. If `inner-type` is not defined (i.e. handling an FS control message), log a notice, _Acknowledge_ and discard the message and abort these steps. 16. If `inner-type` is unknown, log a notice, _Acknowledge_ and discard the message and abort these steps. 17. If `inner-type` is `0xa0` (i.e. FS encapsulation within FS encapsulation), log a warning, _Acknowledge_ and discard the message and abort these steps. 18. If `inner-type` has dedicated blocking exemption steps, run these with `sender-identity` and `inner-message`. If the result indicates that the message should be discarded, _Acknowledge_ the message and abort these steps. 19. If `inner-type` does not have dedicated blocking exemption steps and is not exempted from blocking, run the _Identity Blocked Steps_ for `sender-identity`. If the result indicates that `sender-identity` is blocked, _Acknowledge_ and discard the message and abort these steps. 20. If `sender-identity` equals `*3MAPUSH`: 1. If `inner-type` is not `0xfe`, log a warning, _Acknowledge_ and discard the message and abort these steps. 2. Run the receive steps associated to `inner-type` with `inner-message`. If this fails, exceptionally abort these steps and the connection. If the message has been discarded, _Acknowledge_ the message and abort these steps. 21. If `sender-identity` is not a _Special Contact_: 1. If `inner-metadata.nickname` is defined, let `nickname` be the value of `inner-metadata.nickname`.¹ 2. If `inner-metadata` is not defined and _User Profile Distribution_ was expected for `inner-type`, let `nickname` be the result of decoding the plaintext `legacy-sender-nickname`.¹ 3. If `nickname` is present, trim any excess whitespaces from the beginning and the end of `nickname`. 4. If `contact-or-init` does not contain an existing contact: 1. If `inner-type` does not require to create an implicit _direct_ contact, log a notice, _Acknowledge_ and discard the message and abort these steps. 2. (MD) Run the following sub-steps (labelled _add-contact_): 1. Begin a transaction with scope `CONTACT_SYNC` and the following precondition: 1. If the contact for `sender-identity` exists, abort the _add-contact_ sub-steps. 2. Reflect a `ContactSync.Create` with `contact` set from `contact-or-init` and the following additional properties: - `created_at` set to now, - `nickname` set to `nickname`, - `acquaintance_level` set to `DIRECT`, - all policies and categories set to their defaults. 3. Commit the transaction and await acknowledgement. 3. If the contact for `sender-identity` does not exist, persist a new contact from `contact-or-init` and `nickname`. 4. TODO(SE-510): Schedule fetching gateway-defined profile picture here, if contact was added and if necessary. 5. Lookup the contact associated to `sender-identity` and let `contact` be the result (at this point, `contact` must exist). 6. (MD) If the contact's nickname is different to `nickname`: 1. Begin a transaction with scope `CONTACT_SYNC` and the following precondition: 1. If the contact no longer exists, log an error and exceptionally abort these steps and the connection. 2. Reflect a `ContactSync.Update` with `contact` including the new `nickname`. 3. Commit the transaction and await acknowledgement. 7. Update the contact's nickname with `nickname`. Remove the contact's nickname if `nickname` is empty. 8. Run the receive steps associated to `inner-type` with `inner-message`. If this fails, exceptionally abort these steps and the connection. If the message has been discarded, _Acknowledge_ the message and abort these steps. 22. (MD) If the properties associated to `inner-type` require reflecting incoming messages, reflect a `d2d.IncomingMessage` from `outer-type` and `outer-message` and the associated conversation to other devices and wait for reflection acknowledgement.² If this fails, exceptionally abort these steps and the connection.³ 23. If the properties associated to `inner-type` require sending automatic delivery receipts and `flags` does not contain the _no automatic delivery receipts_ (`0x80`) flag, schedule a persistent task to run the _Bundled Messages Send Steps_ with the following properties: - `id` being a random message ID, - `created-at` set to the current timestamp, - `receivers` set to `contact`, - to construct a [`delivery-receipt`](ref:e2e.delivery-receipt) message with status _received_ (`0x01`) and the respective `message-id`. 24. _Acknowledge_ the message. ¹: Note that the `nickname` of `MessageMetadata` may be undefined (leading to no changes) or defined but explicitly empty (leading to the nickname of the contact being removed) which is an important semantic difference. Unlike the legacy nickname field which always contains a value and therefore cannot represent this semantic difference without having to check whether _User Profile Distribution_ was required for the type. ²: We reflect the **outer** message container depending on the unwrapped **inner** message type, so the forward security properties are untouched and all other devices need to go through the same process. ³: Reflection needs to happen after the message has been processed and all side effects have been applied. Otherwise, if the receive process is interrupted and another device takes over, it would discard the message as a duplicate. The following steps are defined as _Acknowledge_ steps for an incoming message: 1. If the steps for this message have already been invoked once, abort these steps. 2. If `flags` does not contain the _no server acknowledgement_ (`0x04`) flag, send a [`message-ack`](ref:payload.message-ack) payload to the chat server with the respective `message-id`. 3. If the properties associated to `inner-type` require protection against replay, mark the nonce of `message-and-metadata-nonce` as used. 4. If `fs-commit-fn` is defined, run it. [//]: # "TODO(SE-128)" fields: - *message-sender-identity - *message-receiver-identity - *message-message-id - *message-created-at - *message-flags - *message-reserved - _doc: |- Length of the metadata box. In case it is zero, no metadata is present (for compatibility with clients using [`legacy-message`](ref:payload.legacy-message)). Note: For outgoing messages, a metadata box should always be present. name: metadata-length type: u16-le - _doc: |- Backwards compatibility field for the sender's public nickname. Padded with zeroes if needed. When sending a message towards a Threema Gateway ID (starts with a `*`), add the same nickname as included in the encrypted metadata box. Otherwise, set it to all zeroes. Note: The backwards compatibility for Threema Gateway IDs will be removed eventually! name: legacy-sender-nickname type: b32 - _doc: |- Metadata associated to the message. Must be ignored in case `metadata-length` is zero. Message Metadata Key (`MMK`) derivation: S = X25519HSalsa20(.secret, .public) MMK = BLAKE2b(key=S, salt='mm', personal='3ma-csp') The encoded `csp-e2e.MessageMetadata` is then encrypted in the following way: XSalsa20-Poly1305( key=MMK, nonce=, ) name: metadata-container type: b{metadata-length} - _doc: |- Nonce used for the message and the metadata box. name: message-and-metadata-nonce type: *nonce - *message-message-box message-ack: _group: Payloads _doc: |- Acknowledges that a message has been received. Direction: Client <-> Server [//]: # "TODO(SE-128)" fields: - _doc: |- Identity of the sender for an incoming (`0x82`) message / of the receiver for an outgoing (`0x81`) message. name: identity type: *identity - _doc: |- Refers to the `message-id` of the acknowledged message. name: message-id type: *message-id unblock-incoming-messages: _group: Payloads _doc: |- Unblock incoming messages from the server. Sent by a multi-device capable client once it is nominated to receive incoming messages. Direction: Client --> Server [//]: # "TODO(SE-128)" set-push-notification-token: _group: Payloads _doc: |- Sets the push notification token to be used when sending a push message. Direction: Client --> Server fields: - _doc: |- Type of the push token: - `0x00`: No push - `0x01`: APNs Production - `0x02`: APNs Development - `0x05`: APNs Production with `mutable-content` key - `0x06`: APNs Development with `mutable-content` key - `0x11`: FCM with empty payload - `0x13`: HMS with empty payload name: type type: u8 - _doc: |- Push token, maximum 255 bytes. name: token type: b* delete-push-notification-token: _group: Payloads _doc: |- Deletes push tokens for a Threema ID. Can be used when self-removing or removing another device from a device group. Direction: Client --> Server When receiving this payload: 1. If `csp-device-ids` is empty, delete all tokens for all devices except the device sending the payload and abort these steps. 2. Delete all tokens for the devices specified in `csp-device-ids`. fields: - _doc: |- Delete tokens belonging to a [`csp-device-id`](ref:handshake.csp-device-id) in the same device group. name: csp-device-ids type: u64-le[] set-connection-idle-timeout: _group: Payloads _doc: |- Request a different idle timeout than the default one of 5 minutes. The new setting is valid for the connection only. The client must ensure that it sends echo requests or other traffic frequently to keep the connection alive. Direction: Client --> Server [//]: # "TODO(SE-128)" fields: - _doc: |- Idle timeout in seconds. Minium 30s, maximum 600s. name: timeout type: u16-le queue-send-complete: _group: Payloads _doc: |- Indicates that the incoming message queue on the server has been fully transmitted to the client. A client should not disconnect prior to having received this payload. Direction: Client <-- Server [//]: # "TODO(SE-128)" device-cookie-change-indication: _group: Payloads _doc: |- Indicates to the client that a device cookie mismatch has been detected since the last time that the device cookie change indication has been cleared (using the [`clear-device-cookie-change-indication`](ref:clear-device-cookie-change-indication) payload). The client should display a warning in form of a notification and/or dialog to the user, informing them that a new and potentially unauthorized device has accessed the account. When the user confirms, the client should send a [`clear-device-cookie-change-indication`](ref:clear-device-cookie-change-indication) payload to clear the indication. Direction: Client <-- Server clear-device-cookie-change-indication: _group: Payloads _doc: |- Causes the server to clear the flag that triggers sending the [`device-cookie-change-indication`](ref:device-cookie-change-indication) on each connection. The flag will be set again by the server if another device cookie mismatch is detected. Direction: Client --> Server close-error: _group: Payloads _doc: |- Indicates that the connection has experienced an unrecoverable error and must be closed. Direction: Client <-- Server [//]: # "TODO(SE-128)" fields: - _doc: |- Indicates whether the client is allowed to reconnect automatically after the connection has been severed. This allows the server to prevent infinite loops in case of a recurring error. Set to `0` in case the client may not reconnect automatically or any other value otherwise. name: can-reconnect type: u8 - _doc: |- Error message (UTF-8 encoded) name: message type: b* alert: _group: Payloads _doc: |- Generic alert that should be displayed in the client's user interface. Direction: Client <-- Server [//]: # "TODO(SE-128)" fields: - _doc: |- Alert message (UTF-8 encoded) name: message type: b* # End-to-end encrypted structs e2e: &e2e _doc: |- ## End-to-End Encrypted Messages An end-to-end encrypted message can be sent or received once the handshake was successful. Every end-to-end encrypted message is wrapped inside of a [`container`](ref:payload.container) struct that is then encrypted and wrapped by a payload [`legacy-message`](ref:payload.legacy-message) or [`message-with-metadata-box`](ref:payload.message-with-metadata-box) struct. ### Predefined Contacts A pedefined contacts can be added to the contact list and is automatically initialised with its identity, nickname, hard-coded public key and the verification level _fully verified_. Once a predefined contact is in the contact list, it is treated like any other normal contact (with editable properties like first and last name, etc.). A predefined contact may be marked _special_ meaning it follows special logic. These are also known as _Special Contact_s. Even though special contacts should not normally appear in the contact list, there's nothing stopping a user from adding a special contact to its contact list. While they are treated like normal contacts in the contact list, depending on the special handling logic, it may not be possible to send or receive normal messages from them. The following list contains all predefined contacts: - `*3MAPUSH`: - Nickname: Threema Push - Public Key: - Production: fd711e1a0db0e2f03fcaab6c43da2575b9513664a62a12bd0728d87f7125cc24 - Sandbox: fd711e1a0db0e2f03fcaab6c43da2575b9513664a62a12bd0728d87f7125cc24 - Special: Yes - `*3MATOKN`: - Nickname: Threema Token - Public Key: - Production: 04884d12d668f855d00d71fb1d9d413c95f271312f7e077846af671875c4101b - Special: No - `*3MAWORK`: - Nickname: Threema Work Channel - Public Key: - Production: 9aa0a72a8fb6f0cc53727fea6096f1b7b0ebefcc2650ad39a1e54837bba0bc4b - Sandbox: 9aa0a72a8fb6f0cc53727fea6096f1b7b0ebefcc2650ad39a1e54837bba0bc4b - Special: No - `*BETAFBK`: - Nickname: Threema Beta Feedback - Public Key: - Production: 5684d6dcd32a16488df8371095fc9a1fc25baeb6b97366d99fdf2aba00e2bc5c - Special: No - `*MY3DATA`: - Nickname: My Threema Data - Public Key: - Production: 3b01854f24736e2d0d2dc387eaf2c0273c5049052147132369bf3960d0a0bf02 - Sandbox: 83adfee6558b68ae3cd6bbe2a33f4e4409d5624a7cea23a18975aea6272a0070 - Special: No - `*SUPPORT`: - Nickname: Threema Support - Public Key: - Production: 0f944d18324b2132c61d8e40afce60a0ebd701bb11e89be94972d4229e94722a - Sandbox: 0f944d18324b2132c61d8e40afce60a0ebd701bb11e89be94972d4229e94722a - Special: No - `*THREEMA`: - Nickname: Threema Channel - Public Key: - Production: 3a38650c681435bd1fb8498e213a2919b09388f5803aa44640e0f706326a865c - Sandbox: 3a38650c681435bd1fb8498e213a2919b09388f5803aa44640e0f706326a865c - Special: No Note: OnPrem provisions predefined contacts in the associated OPPF file. ### Mitigating Replay To prevent replay attacks, a client must permanently store used nonces for incoming and outgoing end-to-end encrypted messages. Messages reusing previously used nonces must not be processed and discarded. One nonce store for all end-to-end encrypted messages across different contacts is sufficient. Note that it is still possible for the chat server to replay old messages to a device whose database has been erased (e.g. when restoring a backup). However, this is not applicable to forward security encrypted messages. ### Message ID Each message has an associated message ID. It is crucial to understand that this is not a unique identifier across multiple conversations. Unique identification of a message is determined by: - 1:1 Chats: The message ID in combination with the contact's Threema ID. - Group Chats: The message ID in combination with the group creator's Threema ID and the group ID. - Distribution Lists: The message ID with an artificial distribution list ID. When a message is being quoted, it may only be looked up within the associated conversation. ### Flags For each message, we will define _mandatory_ and _optional_ flags referring to the `flags` field of the payload [`legacy-message`](ref:payload.legacy-message) or [`message-with-metadata-box`](ref:payload.message-with-metadata-box) struct. A flag must be considered _mandatory_ unless it has been explicitly marked _optional_. ### Delivery Receipts There are two types of delivery receipts (sent using the [`delivery-receipt`](ref:e2e.delivery-receipt) message): - Automatic: "received" and "read" - Manual: "acknowledged" and "declined" For each message, we will define whether automatic delivery receipts should be sent and whether it is eligible for sending manual delivery receipts (e.g. acknowledge/decline). However, two general exceptions apply: 1. Automatic delivery receipts are not sent to group members (i.e. when any message struct is wrapped in a `group` message struct). 2. Messages whose flags include `0x80` must not trigger any automatic delivery receipts. ### Blocking The sender Threema ID may be blocked explicitly (i.e. blocking a specific Threema ID) or implicitly (blocking all unknown Threema IDs). This does not require special handling on the server but instead is done entirely by the clients. Note that the protocol does not distinguish between implicitly and explicitly blocked Threema IDs. An implicitly blocked Threema ID (i.e. blocking unknown contacts) must be treated the same as an explicitly blocked Threema ID (i.e. blocking specific contacts). The UI must prevent users from composing or submitting messages towards a blocked contact. In practise, this is only relevant for explicitly blocked contacts. The following steps are defined as the _Identity Blocked Steps_: 1. Let `identity` be the Threema ID to be checked. 2. If `identity` is a _Special Contact_, return that it is not blocked. 3. If `identity` is explicitly blocked, return that it is blocked. 4. If the settings indicate that unknown contacts should not be blocked, return that it is not blocked. 5. If `identity` is a _Predefined Contact_, return that it is not blocked. 6. Let `contact` be the associated contact to `identity`. 7. If `contact` is not defined, return that it is blocked. 8. If `contact` has acquaintance level _direct_, return that it is not blocked. 9. If `contact` is part of a group that is not marked as _left_, return that it is not blocked. 10. Return that it is blocked. ### Contact Flows The following steps are defined as the _Valid Contacts Lookup Steps_: 1. Let `identities` be the identities to look up. 2. Let `contact-or-inits` be an empty map of Threema IDs to a contact, properties to create a contact from, or _contact is the user_ or _contact is invalid_ marker. 3. Let `unknown-identities` be an empty list. 4. For each `identity` of `identities`: 1. If `identity` equals the user's Threema ID, add the information that the _contact is the user_ to the `contact-or-inits` map and abort these sub-steps. 2. If `identity` is a _Special Contact_, add that special contact to the `contact-or-inits` map and abort these sub-steps. 3. Lookup the contact associated to `identity` and let `contact` be the result. 4. If `contact` is defined, add `contact` to the `contact-or-inits` map and abort these sub-steps. 5. Lookup the properties to create a contact from associated to `identity` from the _contact lookup cache_ and let `init` be the result. 6. If `init` is defined, add `init` to the `contact-or-inits` map and abort these sub-steps. 7. Add `identity` to `unknown-identities`. 5. Let `directory-response` be the response of asynchronously looking up `unknown-identities` on the Directory Server. 6. If Work flavour, let `work-directory-response` be the response of asynchronously looking up `unknown-identities` on the Work Contacts API endpoint. 7. Await `directory-response` and `work-directory-response`. 8. Process the result of `directory-response`: 1. If the server could not be reached, exceptionally abort these steps. 2. If the status code is `429`, exceptionally abort these steps and add a minimum delay of 10s before retrying a connection. 3. If the status code is not `200`, exceptionally abort these steps. 4. For each contact entry of the result: 1. Remove the contact from `unknown-identities`. If it was not present in `unknown-identities`, log a warning and abort these sub-steps. 2. If the contact is marked as _invalid_ (never existed or has been revoked), add the information that the _contact is invalid_ to the `contact-or-inits` map and abort these sub-steps. 3. If the contact is a _Predefined Contact_: 1. If the contact's public key does not equal the _Predefined Contact_s public key, log a warning and exceptionally abort these steps. 2. Update the contact information with the following information: - set `verification_level` to `FULLY_VERIFIED`, - set `nickname` to the nickname of the _Predefined Contact_, - if _Predefined Contact_ defines a first name, set it accordingly, - if _Predefined Contact_ defines a last name, set it accordingly, 3. Add the resulting contact information to the `contact-or-inits` map from which a new contact can be created. 5. For each `identity` of `unknown-identities`: 1. Add the information that the _contact is invalid_ to the `contact-or-inits` map for `identity`. 6. Clear `unknown-identities`. 9. If `work-directory-response` is defined, process its result: 1. If the server could not be reached, exceptionally abort these steps. 2. If the status code is `401`, exceptionally abort these steps, notify the user that the Work credentials are invalid and request new ones. The connection should not be retried until new Work credentials have been entered and checked for validity. 3. If the status code is `429`, exceptionally abort these steps and add a minimum delay of 10s before retrying a connection. 4. If the status code is not `200`, exceptionally abort these steps. 5. For each `work-contact` of the result: 1. If an entry for the `work-contact`'s identity does not exist in `contact-or-inits`, log a warning and abort these sub-steps. 2. If `work-contact`'s public key does not equal `contact-or-init`'s public key, log a warning and exceptionally abort these steps. 3. Update the contact entry for `work-contact`'s identity in `contact-or-inits` with the following information: - if `verification_level` is not defined or `UNVERIFIED`, set it to `SERVER_VERIFIED`, - set `work_verification_level` to `WORK_SUBSCRIPTION_VERIFIED`, - if `work-contact.first-name` is defined, set the first name accordingly, - if `work-contact.last-name` is defined, set the last name accordingly, 10. TODO(SE-173): Run the contact import flow for `contact-or-inits` and update the `verification_level` for all whose associated phone number / email could be matched. Import `first_name` and `last_name` (if not already defined) and set `sync_state` to `IMPORTED`. Clarify precedence regarding Work API. 11. For each `init` of `contact-or-inits`: 1. If `init` does not contain properties to create a contact from (i.e. it is a contact or any of the special markers), abort these sub-steps. 2. If `init.sync_state` is not defined, set it to `INITIAL`. 3. If `init.verification_level` is not defined, set it to `UNVERIFIED`. 4. If `init.work_verification_level` is not defined, set it to `NONE`. 12. Update the _contact lookup cache_ with the contents of `contact-or-inits`. Each newly added or updated entry has an expiration time of 10m after which the entry is to be removed from the cache. 13. Return `contact-or-inits`. ### Groups Groups are handled in a decentralised manner. Messages are sent to each group member individually. On a technical level, a group is identified by **both** the Threema ID of the creator and the random group ID the creator chose. A group **must never** be identified by the group ID alone. Group messages are special containers wrapped around normal messages (it is actually just a common header): - [`group-member-container`](ref:e2e.group-member-container): For group message communication between members, including the creator. - [`group-creator-container`](ref:e2e.group-creator-container): For special messages that may only be sent from the creator to normal group members and vice versa. Group messages have special types in order to separate them from other messages. These types also define which container must be used. The group members are determined by the [`group-setup`](ref:e2e.group-setup) message and continuously updated by any following [`group-leave`](ref:e2e.group-leave) messages. Any following [`group-setup`](ref:e2e.group-setup) overrides the previous member state. ### Implicit Contact Creation When the user is added to a group, every unknown member of the group must be added to the contact list with acquaintance level _group_. Messages from a contact with any acquaintance level will not be implicitly blocked by a _block unknown_ setting. The contact remains at the acquaintance level _group_ until a 1:1 conversation with that contact is being started by either side in which case the acquaintance level should be changed to _direct_. A contact with acquaintance level _group_ will remain indefinitely even if the contact is being removed from all groups of the user or if all remaining common groups are marked as _left_. In that case, the contact is implicitly marked as _deleted_ so that it is covered by _block unknown_. ### Notes Group A group is identified as a _notes_ group if all of the following criteria are met: 1. The user is the creator of the group. 2. The group currently has no members (besides the creator). 3. The group is not marked as _left_. Messages in a _notes_ group are synchronised across devices but are not sent to the chat server (since there are no other members). Therefore, it is ideal for "notes to self", hence the name. A group seamlessly transforms into a _notes_ group and out of it given the above criteria. Right now this can happen in three scenarios: - A _notes_ group is created explicitly (i.e. a group with only the user is being created). - The user is the creator of a group and one or more members are being added in which case the _notes_ group transforms into a regular group. - The user is the creator of a group whose members have just been removed (but the group has not been disbanded) in which case the group transforms into a _notes_ group. The UI should signal the _notes_ status of a group to the user. ### Group Flows The following steps are defined as the _Active Group Update Steps_: 1. If the user is not the creator of the group or the group is marked as _left_, log an error and abort these steps. 2. Let `message-ids` be a list of four pre-generated message IDs. 3. Let `changes` be the set of expected changes to the group which may contain the following properties: - `profile-picture` is defined if the group's profile picture is expected to be changed and contains either the new profile picture or a _remove_ mark to remove it - `profile-picture.blob` may contain the associated blob information data in case of a changed profile picture. - `add-members` is a set of new members to be added to the group - `remove-members` is a set of existing members to be removed from the group 4. Let `group` be a snapshot of the current group state. 5. Remove all members from `changes.add-members` that are not in `group.members`. 6. Remove all members from `changes.remove-members` that are in `group.members`. 7. Let `messages` be an empty list. 8. If `changes.remove-members` is not empty, add a message entry to `messages` to remove members to be removed with the following properties: - `id` set to the first message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `changes.remove-members`, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) with an empty members set. 9. If `group.members` is not empty: 1. Add a message entry to `messages` to update the group for the members with the following properties: - `id` set to the first message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `group.members`, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) from `group.members`. 2. Add a message entry to `messages` to announce the group's name to the members with the following properties: - `id` set to the second message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `group.members`, - to construct a [`group-name`](ref:e2e.group-name) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) from `group.name`.¹ 3. If `group.profile-picture` is defined: 1. Let `profile-picture-blob` be `changes.profile-picture.blob`. 2. If `group.profile-picture` does not equal `changes.profile-picture`, upload `group.profile-picture` to the blob server with the _persist_ flag and set `profile-picture-blob` to the result. 4. Add a message entry to `messages` to announce the group's profile picture to the members with the following properties: - `id` set to the third message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `group.members`, - to construct a [`set-profile-picture`](ref:e2e.set-profile-picture) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) from `profile-picture-blob` if `profile-picture-blob` is defined or [`delete-profile-picture`](ref:e2e.delete-profile-picture) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) otherwise.¹ 5. Let `chosen-call` be the result of the most recent invocation of the _Group Call Refresh Steps_ for the group. 6. If `chosen-call` is defined, add a message entry to `messages` to announce the ongoing group call to newly added members with the following properties: - `id` set to the fourth message ID of `message-ids`, - `created-at` set to the `started_at` value associated to `chosen-call`, - `receivers` set to `changes.add-members`, - to construct a repeat of `csp-e2e.GroupCallStart` (wrapped by [`group-member-container`](ref:e2e.group-member-container)) that is associated to `chosen-call`. 10. Run the _Bundled Messages Send Steps_ with `messages`. ¹: This results in the group name and the group profile picture being distributed to all members regardless of whether it was changed or not. This is deemed acceptable for the sake of implementation simplicity and reusability. The following steps are defined as the _Active Group State Resync Steps_: 1. If the user is not the creator of the group or the group is marked as _left_, log an error and abort these steps. 2. Let `message-ids` be a list of four pre-generated message IDs. 3. Let `target-members` be a set of members to receive the resync. 4. Remove all members from `target-members` that are not in `group.members`. 5. If `target-members` is empty, abort these steps. 6. Let `messages` be an empty list. 7. Add a message entry to `messages` to announce the group composition with the following properties: - `id` set to the first message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `target-members`, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) from `group.members`. 8. Add a message entry to `messages` to announce the group's name with the following properties: - `id` set to the second message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `target-members`, - to construct a [`group-name`](ref:e2e.group-name) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) from `group.name`. 9. If `group.profile-picture` is defined, upload `group.profile-picture` to the blob server with the _persist_ flag and let `profile-picture-blob` be the result. 10. Add a message entry to `messages` to announce the group's profile picture with the following properties: - `id` set to the third message ID of `message-ids`, - `created-at` set to the current timestamp, - `receivers` set to `target-members`, - to construct a [`set-profile-picture`](ref:e2e.set-profile-picture) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) from `profile-picture-blob` if `profile-picture-blob` is defined or [`delete-profile-picture`](ref:e2e.delete-profile-picture) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) otherwise. 11. Let `chosen-call` be the result of the most recent invocation of the _Group Call Refresh Steps_ for the group. 12. If `chosen-call` is defined, add a message entry to `messages` to announce the ongoing group call with the following properties: - `id` set to the fourth message ID of `message-ids`, - `created-at` set to the `started_at` value associated to `chosen-call`, - `receivers` set to `target-members`, - to construct a repeat of `csp-e2e.GroupCallStart` (wrapped by [`group-member-container`](ref:e2e.group-member-container)) that is associated to `chosen-call`. 13. Run the _Bundled Messages Send Steps_ with `messages`. 14. For each member of `target-members`, mark the group as _recently resynced_ for 1h. #### Create/Clone Group The following steps must be invoked when the user wants to create or clone a group: 1. Let `init` contain the following properties to create a group: - `name` of the new group or an empty string - `profile-picture` of the new group or undefined - `members` is a set of initial members to be added to the group¹ 2. Let `parameters` be the MDM parameters. If `parameters.th_disable_create_group` is `true`, abort these steps. 3. Let `group-id` be a random group ID. 4. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If a group with `group-id` and the user as creator exists, log an error and abort these steps. 2. If `init.members` contains a member that is not an existing contact, log an error and abort these steps. 5. If `init.profile-picture` is defined, upload `init.profile-picture` to the blob server with the _persist_ flag and set `init.profile-picture.blob` to the result. 6. (MD) Reflect a `GroupSync.Create` with `group` set to contain: - `group_identity` being `group-id` and the user as the creator, - `created_at` set to now, - `name` set to `init.name` - `user_state` set to `MEMBER`, - `profile_picture` set from `init.profile-picture.blob`, - `member_identities` set from `init.members`, - all policies and categories set to their defaults. 7. (MD) Commit the transaction and await acknowledgement. 8. Persist the newly created group from `init` and `group-id` to storage. 9. If `init.members` is empty, abort these steps. 10. Let `message-ids` be a list of four random message IDs. 11. Schedule a persistent task to run the following steps: 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_ or the group has no members, log a warning that a group sync race occurred and abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If any of the following cases is true, log a warning that a group sync race occurred: - `init.name` is defined and does not equal `group.name`, - `init.profile-picture` does not equal `group.profile-picture`, - `init.members` does not equal `group.members`. 4. Run the _Active Group Update Steps_ with `message-ids` and the following expected set of `changes`: - `profile-picture` set to `init.profile-picture`, - `add-members` set to `init.members`, - `remove-members` set to an empty list. 5. (MD) Commit the transaction and await acknowledgement. ¹: Note that all contacts must be added before they can be added as initial members of the group. #### Update Group The following steps must be invoked when the user is the creator of a group and intends to apply a change to the group's name, profile picture or add/remove members to/from the group: 1. If the user is not the creator of the group or the group is marked as _left_, log an error and abort these steps. 2. Let `changes` be the set of changes to the group which may contain the following properties: - `name` is defined if the group's name is to be changed and contains the new name or an empty string - `profile-picture` is defined if the group's profile picture is to be changed and contains either the new profile picture or a _remove_ mark to remove it - `add-members` is a set of new members to be added to the group¹ - `remove-members` is a set of existing members to be removed from the group¹ 3. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If `changes.add-members` or `changes.remove-members` contains a member that is not an existing contact, log an error and abort these steps. 2. If the group does not exist or the group is marked as _left_, log a warning and abort these steps. 4. Let `updated-members` be a copy of the current member set of the group. Add all `changes.add-members` to this set that are to be added to the group. Remove all `changes.remove-members` from this set that are to be removed from the group. 5. If `changes.profile-picture` is defined and contains a profile picture, upload `changes.profile-picture` to the blob server with the _persist_ flag and let `changes.profile-picture.blob` be the result. 6. (MD) Reflect a `GroupSync.Update` with `member_state_changes` constructed from `changes.add-members` and `changes.remove-members` and `group` set to contain: - `name` set to `changes.name`, - `profile_picture` set according to `changes.profile-picture` (and `changes.profile-picture.blob`), - `member_identities` set from `updated-members`. 7. (MD) Commit the transaction and await acknowledgement. 8. If the user is currently participating in a group call of this group, remove all `change.remove-members` participants from the group call (handle them as if they left the call). 9. Persist the `updated-members` and other `changes` to the group. 10. If `changes.add-members` or `changes.remove-members` is not empty, run the _Rejected Messages Refresh Steps_ for the group. 11. Let `message-ids` be a list of four random message IDs. 12. Schedule a persistent task to run the following steps: 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning that a group sync race occurred and abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If any of the following cases is true, log a warning that a group sync race occurred: - `changes.name` is defined and does not equal `group.name`, - `changes.profile-picture` contains a profile picture and does not equal `group.profile-picture`, - `changes.profile-picture` contains the _remove_ mark and `group.profile-picture` is defined, - `updated-members` does not equal `group.members`. 4. Run the _Active Group Update Steps_ with `message-ids` and `changes`. 5. (MD) Commit the transaction and await acknowledgement. ¹: Note that all contacts must be added before they can be added as members to the group. The same applies to members that are being removed, obviously. #### Disband/Remove Group The following steps must be invoked when the user is the creator of a group and intends to _disband_ or _disband and remove_ the group: 1. Let `intent` be the user's intent which can be either to _disband_ or to _disband and remove_ the group. 2. If the user is not the creator of the group or the group is marked as _left_, log an error and abort these steps. 3. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning and abort these steps. 4. (MD) If `intent` is to _disband_, reflect a `GroupSync.Update` with `group` set to contain `user_state` set to `LEFT`. 5. (MD) If `intent` is to _disband and remove_, reflect a `GroupSync.Delete` for this group. 6. (MD) Commit the transaction and await acknowledgement. 7. If the user is participating in a group call of this group, trigger leaving the call. 8. If the `intent` is to _disband_: 1. Mark the group as _left_. 2. Persist the previous member setup so that the group can be cloned. 3. Run the _Rejected Messages Refresh Steps_ for the group. 9. Let `group` be a snapshot of the current group state. 10. If the `intent` is to _disband and remove_, remove the group and all associated messages from storage. 11. Let `message-id` be a random message ID. 12. Schedule a persistent task to run the following steps: 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group exists and is not marked as _left_, log an error that a major group state inconsistency has been detected¹ and abort these steps. 2. Run the _Bundled Messages Send Steps_ with the following properties: - `id` set to `message-id`, - `created-at` set to the current timestamp, - `receivers` set to `group.members`, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) with an empty members set. 3. (MD) Commit the transaction and await acknowledgement. ¹: Disbanding a group as the creator makes the group strictly non-reusable. #### Leave/Remove Group The following steps must be invoked when the user is not the creator of a group and intends to _leave_ or _leave and remove_ the group: 1. Let `intent` be the user's intent which can be either to _leave_ or to _leave and remove_ the group. 2. If the user is the creator of the group or the group is marked as _left_, log an error and abort these steps. 3. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning and abort these steps. 4. (MD) If `intent` is to _leave_, reflect a `GroupSync.Update` with `group` set to contain `user_state` set to `LEFT`. 5. (MD) If `intent` is to _leave and remove_, reflect a `GroupSync.Delete` for this group. 6. (MD) Commit the transaction and await acknowledgement. 7. If the user is participating in a group call of this group, trigger leaving the call. 8. If the `intent` is to _leave_: 1. Mark the group as _left_. 2. Persist the previous member setup so that the group can be cloned. 3. Run the _Rejected Messages Refresh Steps_ for the group. 9. Let `group` be a snapshot of the current group state. 10. If the `intent` is to _leave and remove_, remove the group and all associated messages from storage. 11. Let `message-id` be a random message ID. 12. Schedule a persistent task to run the following steps: 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group exists and is not marked as _left_, log a warning that a group sync race occurred and abort these steps. 2. Run the _Bundled Messages Send Steps_ with the following properties: - `id` set to `message-id`, - `created-at` set to the current timestamp, - `receivers` set to `group.members`, - to construct a [`group-leave`](ref:e2e.group-leave) (wrapped by [`group-member-container`](ref:e2e.group-member-container)) 3. (MD) Commit the transaction and await acknowledgement. #### Remove Group The following steps must be invoked when the user intends to remove a group that is marked as _left_. 1. If the group is not marked as _left_, log an error and abort these steps. 2. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is not marked as _left_, log a warning and abort these steps. 3. (MD) Reflect a `GroupSync.Delete` for this group. 4. (MD) Commit the transaction and await acknowledgement. 5. Remove the group and all associated messages from storage. #### Group Resync The following steps must be invoked when the user is the creator of a group and intends to resync the group manually: 1. If the user is not the creator of the group or the group is marked as _left_, log an error and abort these steps. 2. Let `message-ids` be a list of four random message IDs. 3. Schedule a volatile task to run the following steps: 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning and abort these steps. 2. Run the _Active Group State Resync Steps_ with `message-ids` and `target-members` being all current group members.¹ 3. (MD) Commit the transaction and await acknowledgement. ¹: This mechanic intentionally bypasses the 1h _recently resynced_ mark due to an explicit manual request by the user to resync the group. #### Update Conversation The following steps must be invoked when the user intends to change any other synchronised property of the group: 1. Let `change` be one of the following changes to the group as defined by `sync.Group`: - `notification_trigger_policy_override` - `notification_sound_policy_override` - `conversation_category` - `conversation_visibility` 2. Persist the `change` to the group. 3. (MD) Schedule a persistent task to run the following steps: 1. Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist, log a warning and abort these steps. 2. Reflect a `GroupSync.Update` with `group` set to contain the `change`. 3. Commit the transaction and await acknowledgement. 4. Persist the `change` to the group (again). ### Device Flows #### Deactivate Multi-Device Flow The following steps must be invoked when the user wants to deactivate multi-device and continue using the current device. 1. Run the _Drop Devices Steps_ with the intent to _deactivate_ multi-device and keep the user informed regarding the process status and any encountered issues. #### Drop Own Device Flow The following steps must be invoked when the user wants to stop using the current device. 1. If the device does not have multi-device enabled, log an error and abort these steps. 2. Begin a transaction (scope: `DROP_DEVICE`, precondition: none). 3. Send a `DropDevice` with this device's Device ID. 4. Await the corresponding `DropDeviceAck` or the connection closing with close code `4113`. 5. TODO(SE-494): Enter _read-only_ mode persistently. #### Drop Other Devices Flow The following steps must be invoked when the user wants to drop one or more other devices from the device group. 1. Let `device-ids-to-drop` be a set of Device IDs that should be dropped from the device group. 2. If this device's Device ID is contained in `device-ids-to-drop`, log an error and abort these steps. 3. Run the _Drop Devices Steps_ with the intent to _drop specific_ `device-ids-to-drop` and keep the user informed regarding the process status and any encountered issues. #### Drop Devices Steps The following steps are defined as the _Drop Devices Steps_: 1. If the device does not have multi-device enabled¹, run the _Application Setup Steps_ step 2.2. through 2.6. and abort these steps. TODO(SE-199): This shall be removed once multi-device supports FS. 2. Let `intent` be the intent which can be either to _deactivate_ multi-device or _drop specific_ devices, letting `device-ids-to-drop` be that set of specific Device IDs. 3. If `device-ids-to-drop` is defined: 1. If `device-ids-to-drop` is empty, log an error and abort these steps. 2. If `device-ids-to-drop` contains this device's Device ID, log an error and abort these steps. 4. Begin a transaction (scope: `DROP_DEVICE`, precondition: none). 5. Send a `GetDevicesInfo` message. 6. Await the `DevicesInfo` message and let `other-device-ids` be a set of the contained Device IDs excluding this device's Device ID. 7. If `device-ids-to-drop` is defined, remove each Device ID from `device-ids-to-drop` that is not present in `other-device-ids`. 8. If `device-ids-to-drop` is not defined, define it to be a copy of `other-device-ids`. 9. Send a `DropDevice` message for each Device ID of `device-ids-to-drop`. 10. Await all corresponding `DropDeviceAck`s of each Device ID in `device-ids-to-drop`. 11. If `device-ids-to-drop` contains the same Device IDs as `other-device-ids` (i.e. all other devices but this device have been dropped): 1. Send a `DropDevice` with this device's Device ID. 2. Await the corresponding `DropDeviceAck` or the connection closing with close code `4113`. 3. Disable multi-device and purge any existing device group data. 4. Run the _Application Setup Steps_ step 2.2. through 2.6. TODO(SE-199): This shall be removed once multi-device supports FS. ¹: This can happen if the steps are run within a persistent task that is aborted before reactivating FS successfully. TODO(SE-199): This shall be removed once multi-device supports FS. ### Sending The following steps are defined as the _Bundled Messages Send Steps_ and must always be invoked as part of a task in order to send a message: 1. Let `messages` be a list of messages with each having the following properties: - `id` being the associated message ID¹, - `created-at` timestamp, - `receivers` being the set of receivers for the message², - the associated conversation, - all necessary information to construct a _canonical_ message from it, - all necessary information to construct a _specific_ message from it, given the specific receiver. Note: If only one set of informations to construct a message is provided, this is considered both the _canonical_ and the _specific_ message construction variant. 2. For each `message` of `messages`: 1. For each `receiver` of `message.receivers`: 1. If `receiver` is the user, log a warning, remove `receiver` from `receivers` and abort these sub-steps. 2. If `receiver` is marked as _invalid_, remove `receiver` from `receivers and abort these sub-steps. 3. If the properties associated to `message` to be constructed for `receiver` given its feature mask indicates that the message is not exempted from blocking, run the _Identity Blocked Steps_ for `receiver`'s identity. If the result indicates that `receiver` is blocked, remove `receiver` from `receivers` and abort these sub-steps. 4. Construct the _specific_ `message` for `receiver` and attach the constructed message to `receiver`. 5. Run the _Profile Picture Distribution Steps_ with the constructed _specific_ message for `receiver`'s type and `receiver` and extend `messages` with the result. 3. For each `message` of `messages` attach a random nonce for `message` to each receiver of `message.receivers`. 4. (MD) Let `pending-reflect-acks` be an empty list. 5. (MD) For each `message` of `messages`: 1. If the properties associated to the _canonical_ `message` do not require reflecting outgoing messages, abort these sub-steps. 2. Construct a `d2d.OutgoingMessage` from the _canonical_ `message` for the associated conversation and reflect it. 3. Add the pending acknowledgement to `pending-reflect-acks`. 6. (MD) Await all `pending-reflect-acks`. 7. Let `pending-csp-acks` and `fs-commit-fns` be empty lists. 8. For each `message` of `messages`: 1. For each `receiver` of `message.receivers`: 1. If the constructed _specific_ message for `receiver` is of type `0xa0`, let `outer-messages` be a list including only the constructed message for `receiver` and `fs-commit-fns` be an empty list. 2. If the constructed _specific_ message for `receiver` is not of type `0xa0`, run the _FS Encapsulation Steps_ with the constructed _specific_ message for `receiver` and let `outer-messages` and `fs-commit-fns` be the result.³ 3. For each `outer-message` of `outer-messages`: 1. Create a `payload.message-with-metadata-box` for `outer-message` with `receiver` and let `payload` be the result. 2. If `payload.flags` does not contain the _no server acknowledgement_ (`0x04`) flag, add `payload.message-id` to `pending-csp-acks`. 3. If the properties associated to `outer-message` requires protection against replay, mark the nonce of `outer-message` as used. 4. Send `payload`. 9. Await all `pending-csp-acks`. 10. Run each function of `fs-commit-fns`. 11. (MD) Let `pending-reflect-acks` be an empty list. 12. (MD) For each `message` of `messages`: 1. If the properties associated to the _canonical_ `message` is eligible for reflecting `OutgoingMessageUpdate.Sent`: 1. Create an `OutgoingMessageUpdate.Sent` for `message.id` and the associated conversation and reflect it. 2. Add the pending acknowledgement to `pending-reflect-acks`. 13. (MD) Await all `pending-reflect-acks`. 14. For each `message` of `messages`: 1. (MD) If the properties associated to the _canonical_ `message` was eligible for reflecting `OutgoingMessageUpdate.Sent`, mark it as _sent_ with the timestamp from the corresponding `reflect-ack` message. 2. (non-MD) Mark `message` as _sent_ with the current timestamp. ¹: Note that, in groups, this implicitly assigns the same message ID towards each group member which in fact is a requirement of the protocol. ²: Reflecting with `receivers` empty is a legitimate case that occurs when sending a message in a notes group. ³: Always invoking the _FS Encapsulation Steps_ ensures that an FS session is being initiated as soon as possible, so that messages can be protected by FS. Moreover, it ensures that the announced FS session version is up to date (a newer version potentially increasing security or making more messages eligible for FS protection). The following steps are defined as the _Messages Submit Steps_ which must be invoked to submit a user-created message in a conversation: 1. Let `messages` be a list of messages of the user to be added to the conversation with each having the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `blobs` be an optional set of blobs that must be uploaded prior to being able to construct the message, - all necessary information to construct a _canonical_ message from it, - all necessary information to construct a _specific_ message from it, given the specific receiver. Note: If only one set of informations to construct a message is provided, this is considered both the _canonical_ and the _specific_ message construction variant. 2. If the associated conversation is a 1:1 conversation, run the _1:1 Messages Submit Steps_ with `messages`. 3. If the associated conversation is a group conversation, run the _Group Messages Submit Steps_ with `messages`. 4. If the associated conversation is a distribution list conversation, run the _Distribution List Messages Submit Steps_ with `messages`. 5. (Unreachable) The following steps are defined as the _1:1 Messages Submit Steps_ which must be invoked to submit a user-created message in a 1:1 conversation: 1. Let `messages` be a list of messages of the user to be added to the conversation with each having the following properties: - `id` defaulting to a random message ID, - `created-at` defaulting to the current timestamp, - `blobs` be an optional set of blobs that must be uploaded prior to being able to construct the message, - all necessary information to construct a _canonical_ message from it, - all necessary information to construct a _specific_ message from it, given the specific receiver. Note: If only one set of informations to construct a message is provided, this is considered both the _canonical_ and the _specific_ message construction variant. 2. Let `receiver` be the conversation's associated contact. 3. If `receiver` has acquaintance level _deleted_, discard `messages`, log a warning and abort these steps.¹ 4. Run the _Identity Blocked Steps_ for `receiver`'s identity'. If the result indicates that `receiver` is blocked, discard `messages`, log a warning and abort these steps.¹ 5. For each `message` of `messages`, assign a random message ID to `message.id`. 6. Schedule a persistent task to run the following steps:² 1. If the contact for `receiver` no longer exists, log an error, discard `messages` and abort these steps. 2. For each `message` of `messages`: 1. If `message.blob` is not defined, abort these sub-steps. 2. Upload all `message.blobs` to the blob server and update `message` with the result (so that the message can be constructed). 3. (MD) If `receiver`'s acquaintance level is not _direct_ or if at least one of the `messages` associated properties requires to unarchive the conversation and the associated conversation visibility is set to _archived_: 1. Begin a transaction with scope `CONTACT_SYNC` and the following precondition: 1. If the contact for `receiver` no longer exists, log an error, discard `messages` and abort these steps. 2. Let `change` be the following changes as defined by `sync.Contact`: - `acquaintance_level` set to `DIRECT`, - `conversation_visibility` set to `NORMAL` if the associated conversation visibility is currently _archived_, 3. Reflect a `ContactSync.Update` with `contact` set from `change`. 4. Commit the transaction and await acknowledgement. 5. Persist the `change` to the `receiver`.³ 4. Run the _Bundled Messages Send Steps_ for `messages` with the following additional properties added to each message: - `receivers` set to `receiver`. 7. If the the `receiver`'s acquaintance level is not _direct_, update it to _direct_. 8. If at least one of the `messages` associated properties requires to unarchive the conversation and the associated conversation visibility is set to _archived_, set it to _normal_. 9. For each `message` of `messages`, add `message` to the associated conversation or update a stateful message referred to by `message` respectively. 10. If at least one of the `messages` associated properties requires to bump the conversation's _last update_ timestamp, update it to the current timestamp. ¹: While the UI should not allow to submit a message in these states, another device may alter the state just prior to submission, allowing to hit these steps in rare circumstances. ²: Rationale to not check for acquaintance level _deleted_ or whether the receiver is blocked again is the legitimate use case of the user sending a final message prior to blocking or removing a contact. ³: This is intentionally done only for MD since the user may e.g. immediately archive a conversation after submitting a message. This results in both the message and the contact sync being queued as tasks whereas in the non-MD case only the message task would be queued. In the MD case, the conversation briefly flicks back to _unarchived_ once the message has been sent but it immediately flicks back to _archived_ once the contact sync task has been executed. But because no contact sync task is created in the non-MD case, the message task would leave the conversation _unarchived_ which is not intended by the user. The following steps are defined as the _Group Messages Submit Steps_ which must be invoked to submit a user-created message in a group conversation: 1. Let `messages` be a list of messages of the user to be added to the conversation with each having the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `blobs` be an optional set of blobs that must be uploaded prior to being able to construct the message, - all necessary information to construct a _canonical_ message from it, - all necessary information to construct a _specific_ message from it, given the specific receiver. Note: If only one set of informations to construct a message is provided, this is considered both the _canonical_ and the _specific_ message construction variant. 2. If the group is marked as _left_, discard `messages`, log a warning and abort these steps.¹ 3. For each `message` of `messages`, assign a random message ID to `message.id`. 4. Schedule a persistent task to run the following steps:² 1. If the group does not exist or is marked as _left_, log a warning, discard `messages` and abort these steps. 2. For each `message` of `messages`: 1. If `message.blob` is not defined, abort these sub-steps. 2. Upload all `message.blobs` to the blob server with the _persist_ flag and update `message` with the result (so that the message can be constructed). 3. (MD) If at least one of the `messages` associated properties requires to unarchive the conversation and the associated conversation visibility is set to _archived_: 1. Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or is marked as left, log a warning that a group sync race occurred, discard `messages` and abort these steps. 2. Let `change` be the following changes as defined by `sync.Group`: - `conversation_visibility` set to `NORMAL` if the associated conversation visibility is currently _archived_, 3. Reflect a `GroupSync.Update` with `group` set from `change`. 4. Commit the transaction and await acknowledgement. 5. Persist the `change` to the group.³ 4. Run the _Bundled Messages Send Steps_ for `messages` with the following additional properties added to each message: - `receivers` set to the group's members. 5. If at least one of the `messages` associated properties requires to unarchive the conversation and the associated conversation visibility is set to _archived_, set it to _normal_. 6. For each `message` of `messages`, add `message` to the associated conversation or update a stateful message referred to by `message` respectively. 7. If at least one of the `messages` associated properties requires to bump the conversation's _last update_ timestamp, update it to the current timestamp. ¹: While the UI should not allow to submit a message in these states, another device may alter the state just prior to submission, allowing to hit these steps in rare circumstances. ²: Rationale to not check for acquaintance level _deleted_ or whether the receiver is blocked again is the legitimate use case of the user sending a final message prior to removing a group. ³: This is intentionally done only for MD since the user may e.g. immediately archive a conversation after submitting a message. This results in both the message and the group sync being queued as tasks whereas in the non-MD case only the message task would be queued. In the MD case, the conversation briefly flicks back to _unarchived_ once the message has been sent but it immediately flicks back to _archived_ once the group sync task has been executed. But because no group sync task is created in the non-MD case, the message task would leave the conversation _unarchived_ which is not intended by the user. The following steps are defined as the _Distribution List Messages Submit Steps_ which must be invoked to submit a user-composed message in a distribution list conversation: 1. Let `messages` be a list of messages of the user to be added to the conversation with each having the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `blobs` be an optional set of blobs that must be uploaded prior to being able to construct the message, - all necessary information to construct a _canonical_ message from it, - all necessary information to construct a _specific_ message from it, given the specific receiver. Note: If only one set of informations to construct a message is provided, this is considered both the _canonical_ and the _specific_ message construction variant. 2. For each `message` of `messages`, assign a random message ID to `message.id`. 3. Schedule a persistent task to run the following steps: 1. If the distribution list does not exist, log a warning, discard `messages` and abort these steps. 2. For each `message` of `messages`: 1. If `message.blob` is not defined, abort these sub-steps. 2. Upload all `message.blobs` to the blob server with the _persist_ flag and update `message` with the result (so that the message can be constructed). 3. (non-MD) Let `members` be all current members of the distribution list. Remove any members with acquaintance level _deleted_ from `members`.¹ 4. (MD) If at least one of the `messages` associated properties requires to unarchive the conversation and the associated conversation visibility is set to _archived_: 1. Begin a transaction with scope `DISTRIBUTION_LIST_SYNC` and the following precondition: 1. If the distribution list does not exist, log a warning that a distribution list sync race occurred, discard `messages` and abort these steps. 2. Let `members` be all current members of the distribution list. Remove any members with acquaintance level _deleted_ from `members`.¹ 3. Let `change` be the following changes as defined by `sync.DistributionList`: - `member_identities` from `members`, - `conversation_visibility` set to `NORMAL` if the associated conversation visibility is currently _archived_, 4. Reflect a `DistributionList.Update` with `distribution_list` set from `change`. 5. Commit the transaction and await acknowledgement. 6. Persist the `change` to the distribution list.² 5. Run the _Bundled Messages Send Steps_ for `messages` with the following additional properties added to each message: - `receivers` from `members`. 4. If at least one of the `messages` associated properties requires to unarchive the conversation and the associated conversation visibility is set to _archived_, set it to _normal_. 5. For each `message` of `messages`, add `message` to the associated conversation or update a stateful message referred to by `message` respectively. 6. If at least one of the `messages` associated properties requires to bump the conversation's _last update_ timestamp, let `last-update-at` be the current timestamp. 7. If `last-update-at` is defined, update the associated _last update_ timestamp of the conversation to `last-update-at`. 8. For each `member` of the distribution list: 1. Add each message of `messages` to the 1:1 conversation associated to `member`. 2. If `last-update-at` is defined, update the associated _last update_ timestamp of the 1:1 conversation of `member` to `last-update-at`. ¹: There should be no receivers in a distribution list that have acquaintance level _deleted_, so the filtering is only done for safety. ²: This is intentionally done only for MD since the user may e.g. immediately archive a conversation after submitting a message. This results in both the message and the distribution list sync being queued as tasks whereas in the non-MD case only the message task would be queued. In the MD case, the conversation briefly flicks back to _unarchived_ once the message has been sent but it immediately flicks back to _archived_ once the distribution list sync task has been executed. But because no distribution list sync task is created in the non-MD case, the message task would leave the conversation _unarchived_ which is not intended by the user. ### Receiving The following steps are defined as _Common Group Receive Steps_ and will be applied in most cases for group messages: 1. Look up the group. 2. If the group could not be found: 1. If the user is the creator of the group, discard the message and abort these steps. 2. Run the _Identity Blocked Steps_ for the creator of the group. If the result indicates that the creator is blocked, discard the message and abort these steps. 3. Run the _Group Sync Request Steps_ for the group, discard the message and abort these steps. 3. If the group is marked as _left_: 1. If the user is the creator of the group, run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the sender, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) with an empty members set 2. If the user is not the creator of the group, run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the sender, - to construct a [`group-leave`](ref:e2e.group-leave) (wrapped by [`group-member-container`](ref:e2e.group-member-container)) 3. Discard the message and abort these steps. 4. If the sender is not a member of the group: 1. If the user is the creator of the group, run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the sender, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) with an empty members set 2. If the user is not the creator of the group, run the _Group Sync Request Steps_, then discard the message and abort these steps. 3. Discard the message and abort these steps. This rule and any exceptions will be referenced/defined explicitly for each message. Note that steps are not allowed to discard messages from blocked contacts prior to running these steps if the message alters group state (group control messages), or is stateful (i.e. introduces a poll, poll vote, or a group call). ### Periodic Group Sync When the creator of a group... - is about to send a group conversation message, or - did just receive a group conversation message, it must trigger a _group sync_ for this group if the last time the _group sync_ was triggered is more than seven days ago. When a _group sync_ is triggered, the creator assumes it has received a [`group-sync-request`](ref:e2e.group-sync-request) from every group member and must now respond accordingly to each member of the group. A newly created group counts as an initial _group sync_ trigger. In other words, the first group sync of a newly created group triggers seven days in the future when one of the above described conditions is met. This provides a form of self-healing in case a device lost its group state (e.g. due to a backup restore) and was unable to correct this mischief. [//]: # "TODO(SE-40): Group states" ### Blobs Since messages have a strict maximum size limitation, large binary blobs are uploaded to the blob server. Blobs currently have a maximum size of 100 MiB. When Multi-Device is activated, all Blobs must be downloaded via the respective Blob Mirror unless explicitly stated otherwise. The following steps are defined as the _Blob Credentials Refresh Steps_: 1. Let `credentials` be the most recently used cached blob credentials. 2. If `credentials` is defined and is not yet expired, return `credentials`. 3. Request Blob Server Credentials via the Directory Server API and set `credentials` to the result. If no credentials could be obtained within 10s or the request was unsuccessful, exceptionally abort these steps. 4. Cache `credentials` with the provided expiration date. 5. Return `credentials`. #### Upload The following steps are defined as the _Blob Upload Steps_: 1. Let `blob` be the following properties: - `data` being the encrypted binary data to be uploaded, - `scope` being either _public_ (for public facing blobs) or _local_ (for device group facing blobs). - `persist` being a mark (primarily for usage within groups). 2. Run the _Blob Credentials Refresh Steps_ and let `credentials` be the result. 3. (non-MD) Upload to the blob server from `blob` and `credentials`. 4. (MD) Upload to the blob mirror server from `blob`, `credentials` and the device group information. 5. If the upload stream stalls for more than 10s, exceptionally abort these steps. 6. Return the resulting blob ID. #### Download The following steps are defined as the _Blob Download Steps_: 1. Let `blob` be the following properties: - `id` being the blob ID, - `scope` being either _public_ (for public facing blobs) or _local_ (for device group facing blobs). 2. Run the _Blob Credentials Refresh Steps_ and let `credentials` be the result. 3. (non-MD) Download the blob data from the blob server using `blob` and `credentials`. 4. (MD) Download the blob data from the blob mirror server using `blob`, `credentials` and the device group information. 5. If the download stream stalls for more than 10s, exceptionally abort these steps. 6. Schedule a volatile background task to run the following steps: 1. Run the _Blob Credentials Refresh Steps_ and let `credentials` be the result. 2. (non-MD) Request to mark the blob download from the blob server of `blob.id` as _done_ with `credentials`. 3. (MD) Request to mark the blob download from the blob mirror server of `blob.id` as _done_ with `credentials` and the device group information. 4. If no response could be obtained within 10s or the request was unsuccessful, log a warning. 7. Return the resulting blob data. ### Image, Audio, Video vs. File Images, as well as audio and video sources can be either send as special media messages or as files. When sending as a file, i.e. a [`file`](ref:e2e.file) message struct with rendering type `0x00` (file), no transcoding is necessary and no media type restrictions apply. Clients should intelligently choose between a media message and a file message but always leave the final choice to the user. The following sections describe what restrictions apply and modifications need to be made in case the source is sent as a media message, i.e. one of the specialised (deprecated) media structs or a [`file`](ref:e2e.file) message struct with rendering type `0x01` (media) or `0x02` (sticker). ### Images Images must be in JPEG format for the legacy [`deprecated-image`](ref:e2e.deprecated-image) message and for profile pictures. When using the [`file`](ref:e2e.file) message struct, the following media types are explicitly supported: - image/gif - image/jpeg - image/png - image/webp The following media types are explicitly not supported: - image/svg+xml Other media types _may_ be supported. Keep the format when resizing images or creating thumbnails, if possible (e.g. if the source is a JPEG, make the thumbnail a JPEG). When the format cannot be kept, use PNG for source images with transparency or lossless encoding (e.g. screenshots) and JPEG for images without transparency or lossy encoding (e.g. photos). Recommended maximum dimensions: - Small: 640x640 - Medium: 1024x1024 - Large: 1600x1600 - Extra Large: 2592x2592 - Original: As is ### Thumbnails Apply the logic described for images to all thumbnails with recommended maximum dimensions of 512x512. ### User Profile Distribution The shareable part of the user profile consists of the user's public nickname and the profile picture: - The nickname is sent along with outgoing messages as `sender-nickname` inside the [`legacy-message`](ref:payload.legacy-message) or as part of the metadata in [`message-with-metadata-box`](ref:payload.message-with-metadata-box). - The profile picture is distributed as described in [Profile Picture Distribution](ref:e2e#profile-picture-distribution). Whether user profile distribution should be triggered by an outgoing message is specified in the description of every message type below. ### Profile Pictures Apply the logic described for images to all profile pictures with recommended maximum dimensions of 512x512 with a square aspect ratio. #### Profile Picture Distribution Every time a message is being sent to a specific contact or a group of contacts, the sender needs to evaluate whether the profile picture needs to be sent. If the receiver of the message is a group, the evaluation needs to be done for each contact of that group. The following steps are defined as the _Profile Picture Distribution Steps_: 1. Let `type` be a message type associated to a message that is about to be sent. 2. Let `receiver` be the receiver of the message. 3. If the properties associated to `type` do not require _User Profile Distribution_, abort these steps without a message. 4. If `receiver`'s Threema ID is `ECHOECHO` or a Threema Gateway ID (starts with a `*`), abort these steps without a message. 5. Let `cache` be the most recently distributed profile picture message variant towards `receiver`, being either - a blob ID if the user's profile picture was distributed via a [`set-profile-picture`](ref:e2e.set-profile-picture) message, or - a _remove_ mark and a timestamp if the user's profile picture was removed via a [`delete-profile-picture`](ref:e2e.delete-profile-picture) message. 6. If the user has no profile picture or the settings indicate that the user's profile picture should be shared with nobody or the settings associated to `receiver` indicate that the profile picture should not be distributed to it: 1. If `cache` is a _remove_ mark and indicates that the most recent [`delete-profile-picture`](ref:e2e.delete-profile-picture) message towards `receiver` was sent less than seven days ago, abort these steps without a message. 2. Update the `cache` for `receiver` to a _remove_ mark with the current timestamp. 3. Return the following properties: - `id` being a random message ID, - `created-at` set to the current timestamp, - `receivers` set to `receiver`, - to construct a [`delete-profile-picture`](ref:e2e.delete-profile-picture). 7. If there is a cached profile picture of the user and the associated blob was uploaded more than seven days ago, remove the cached profile picture. 8. If `cache` contains a blob ID of the most recent [`set-profile-picture`](ref:e2e.set-profile-picture) message sent towards `receiver` that equals the blob ID of the cached profile picture, abort these steps without a message. 9. If there is no cached profile picture, encrypt the user's profile picture with a random symmetric key and upload it to the blob server. Store the key and the resulting blob ID as the cached profile picture of the user. 10. Update the `cache` for `receiver` to the blob ID of the cached profile picture. 11. Return the following properties: - `id` being a random message ID, - `created-at` set to the current timestamp, - `receivers` set to `receiver`, - to construct a [`set-profile-picture`](ref:e2e.set-profile-picture) message using the cached profile picture's blob ID and key. When the user changes the profile picture, run the steps associated to update the user's profile with the new profile picture or the newly removed profile picture. #### Profile Picture Sharing Settings In the client settings, there are three profile picture sharing options that the user can choose from: - Share with nobody - Share with everybody you write to - Share with selected contacts only The default is to share the profile picture with everyone. #### Contact Profile Picture Precedence There are three different sources of profile pictures, ordered by precedence: 1. _contact-defined_: Set by the contact, distributed through a [`set-profile-picture`](ref:e2e.set-profile-picture) message. 2. _gateway-defined_: Set by the creator in the Threema Gateway (or Threema Broadcast) control panel and distributed through `avatar.threema.ch`. Only applicable to Threema Gateway IDs (starting with a `*`). 3. _user-defined_: Set by the app user for this contact or imported from the address book. Applicable to all Threema IDs which are not Threema Gateway IDs. The following steps are defined as _Contact Profile Picture Selection Steps_ and will be applied to determine the contact's profile picture that should be displayed: 1. Let `id` be the Threema ID of the contact. 2. If the _contact-defined_ picture is set for the contact, apply it and abort these steps. 3. If `id` starts with a `*` (is a Threema Gateway ID) and the _gateway-defined_ picture is set for the contact, apply it and abort these steps. 4. If `id` does not start with `*` and the _user-defined_ picture is set for the contact, apply it and abort these steps. 5. Apply a fallback picture. #### Recurring Gateway Contact Profile Picture Refresh For contacts with a Threema Gateway ID (starting with a `*`), the profile picture needs to be fetched recurringly: 1. Fetch the profile picture for the ID from `avatar.threema.ch`. 2. If no profile picture could be found, schedule the next refresh in 24h and abort these steps. 3. Store the profile picture as the _gateway-defined_ picture. 4. Schedule the next refresh according to the `expires` header of the HTTP response. 5. Run the _Contact Profile Picture Selection Steps_ for this contact. ### Audio Audio must be in AAC format. If the source is already in AAC, no transcoding is necessary. Otherwise, the recommended transcoding settings are: Bitrate 128 kbit/s, 2 channels. When recording audio (i.e. a voice message), the recommended recording settings are: Sample rate 44.1 kHz, bitrate 32 kbit/s, 1 channel. ### Video Videos must be encoded in H.264 and the MP4 container format. Recommended encoding settings for all videos: - Low: 480x480, scale by maintaining aspect ratio to nearest multiple of 16px. Video bitrate 384 kbit/s, audio bitrate 32 kbit/s (2 channels). Baseline Profile, Level 3.1. - High: 848x848, scale by maintaining aspect ratio to nearest multiple of 16px. Video bitrate 1500 kbit/s, audio bitrate 64 kbit/s (2 channels). Baseline Profile, Level 3.1. - Original: As is. Still needs transcoding in case a different codec has been used. When recording a video, the following recording settings are recommended to avoid post-reencoding: 1280x720 / 720x1280. Video bitrate 2000 kbit/s at 30 fps, audio bitrate 128 kbit/s (2 channels). ### Call Features Call features are transmitted within either a [`call-offer`](ref:e2e.call-offer) or a [`call-answer`](ref:e2e.call-answer) message. It is an optional object containing the below defined fields. If the object is not provided, assume an empty features object. - Video Support (`'video'`): Set this field to `null` or an empty object if video calls are enabled. If either side omits this field, video support is disabled for the upcoming call. ### Application Entrypoints #### Work Credentials URL The following steps are defined as _Application Work Credentials URL Entrypoint Steps_ and must be run when the application is built for the _Work_ flavour and is invoked by a Work credentials URL: 1. Decode the Work credentials URL and let `work-credentials` be the result. 2. Run the _Common Application Entrypoint Steps_ with `work-credentials`. #### OnPrem Server/License URL The following steps are defined as _Application OnPrem Server/License URL Entrypoint Steps_ and must be run when the application is built for the _OnPrem_ flavour and is invoked via an OnPrem server/license URL: 1. Decode the OnPrem server/license URL¹ and let `on-prem-server-url` and `work-credentials` be the result. 2. Run the _Common Application Entrypoint Steps_ with `on-prem-server-url` and `work-credentials`. ¹: While the OnPrem server URL only provides the path to the OPPF file, the OnPrem license URL additionally provides the Work credentials. #### Default The following steps are defined as the _Common Application Entrypoint Steps_ and resemble the default entrypoint if no specific entrypoint was invoked by a user interaction: 1. If identity data exists: 1. (OnPrem: Refresh the OPPF file and apply its configuration to the application. TODO(SE-137): Specify more clearly.) 2. [...] 3. Abort these steps. 2. (Identity data is missing at this point.) 3. (If the application is built for the _Consumer_ flavour, request/verify the license. TODO(SE-137): Specify more clearly.) 4. If the application is built for the _Work_ flavour: 1. Let `work-credentials` be the provided parameters. 2. If `work-credentials` is not defined, request the user to provide this information and update `work-credentials` with the result. 3. (Verify the Work license. TODO(SE-137): Specify more clearly.) 5. If the application is built for the _OnPrem_ flavour: 1. Let `on-prem-server-url` and `work-credentials` be the provided parameters. 2. If the application is built for the regular _OnPrem_ flavour: 1. If the MDM parameter `th_onprem_server` is defined: 1. If `on-prem-server-url` is defined and its canonical¹ representation does not equal the canonical¹ representation of the MDM parameter `th_onprem_server`, show an error to the user that an incorrect OnPrem server/license URL has been used and abort these steps. 2. Set `on-prem-server-url` to the MDM parameter `th_onprem_server`. 2. If `on-prem-server-url` or `work-credentials` is not defined, request the user to provide the missing information and update `on-prem-server-url` and `work-credentials` with the result. 3. If the application is built for the _White-Labeled OnPrem_ flavour²: 1. If `on-prem-server-url` is defined and its canonical¹ representation does not equal the canonical¹ representation of the preconfigured OnPrem server URL, show an error to the user that an incorrect OnPrem server/license URL has been used and abort these steps. 2. Set `on-prem-server-url` to the preconfigured OnPrem server URL. 3. If `work-credentials` is not defined, request the user to provide the Work credentials and update `work-credentials` with the result. 4. (Verify the OnPrem/Work license. TODO(SE-137): Specify more clearly.) 6. Run the _Application Setup Steps_. ¹: The canonical URL is constructed by appending `/prov/config.oppf` if the URL does not end with `.oppf`. ²: Note that the `th_onprem_server` parameter is intentionally being ignored in this case. ### Application Setup The following steps are defined as _Application Setup Steps_ and must be run when no identity data exists (i.e. the application is installed for the first time or the Threema ID and associated identity data has been removed): 1. [...] 2. (The application allows to create a new Threema ID or restore a backup here. TODO(SE-137): Specify more clearly.) 3. If OnPrem sub-flavour and the MDM parameter `th_enable_remote_secret` is `true`, run the _Remote Secret Activate Steps_. 4. If application state has not been set up by the _Device Join Protocol_ (meaning that multi-device is deactivated), run the following steps: 1. [...] 2. Update the user's feature mask on the directory server. 3. Let `contacts` be the list of all contacts, including those with an acquaintance level different than _direct_. 4. Call the _Work Sync_ endpoint with `contacts` and update `contacts` and the settings with the result. 5. Refresh the state, type and feature mask of all `contacts` from the directory server and make any changes persistent. 6. Let `solicited-contacts` be a copy of `contacts` filtered in the following way. For each `contact`: 1. If the `contact`'s activity state is _invalid_ (i.e. it does not exist or has been revoked), remove `contact` from the list and abort these sub-steps. 2. If `contact` is part of a group that is not marked as _left_, add `contact` to the list and abort these sub-steps. 3. Lookup the 1:1 conversation with `contact` and let `last-update` be the associated _last update_ timestamp. 4. If `last-update` is defined, add `contact` to the list and abort these sub-steps. 5. Remove `contact` from the list. 7. If FS is supported by the client, run the _FS Refresh Steps_ with `solicited-contacts`. 8. For each `contact` of `solicited-contacts` run the _Bundled Messages Send Steps_ with the following properties: - `id` being a random message ID, - `created-at` set to the current timestamp, - `receivers` set to `contact`, - to construct a [`contact-request-profile-picture`](ref:e2e.contact-request-profile-picture) 9. For each group not marked as _left_: 1. If the user is the creator of the group, trigger a _group sync_ for that group. 2. If the user is not the creator of the group, run the _Group Sync Request Steps_ for the group. 10. [...] 5. Commit the application state with the updated `contacts` and settings, outer storage potentially protected by a passphrase (if provided) and inner storage potentially protected by RS (if created). ### Application Update The following steps are defined as _Application Update Steps_ and must be run as a persistent task when the application has just been updated to a new version or downgraded to a previous version: 1. [...] 2. Update the user's feature mask on the directory server. 3. Let `contacts` be the list of all contacts (regardless of the acquaintance level). 4. Refresh the state, type and feature mask of all `contacts` from the directory server and make any changes persistent. 5. For each `contact` of `contacts`: 1. If an associated FS session with `contact` exists and any of the FS states is unknown or any of the stored FS versions (local or remote) is unknown, terminate the FS session by running the _Bundled Messages Send Steps_ with the following properties: - `id` being a random message ID, - `created-at` set to the current timestamp, - `receivers` set to `contact`, - to construct a `csp-e2e-fs.Terminate` message with cause `RESET`. 6. [...] Note: Reactivation of FS due to disabling multi-device should run the _Application Setup Steps_ step 2.2. through 2.6. TODO(SE-199): This note will be removed once multi-device supports FS. ### Application Start The following steps are defined as _Application Start Steps_ and must be run as a blocking task when the application starts before running any further sequences: 1. [...] 2. Attempt to unlock the outer storage, potentially protected by a passphrase (if provided). 3. If the Remote Secret feature is active, run the _Remote Secret Monitor Steps_ until it yields RS and let it continue as a volatile background task bound to the application. 4. Unlock the inner storage, optionally protected by RS (if activated). 5. [...] 6. Initialise the application from the unlocked storage. container: _group: Header _doc: |- Contains an end-to-end encrypted message. fields: - _doc: |- Type of the message (`common.CspE2eMessageType`). name: type type: u8 - _doc: |- Inner message. Needs to be parsed according to the `type` field. Padded with a random amount from 1 to 255 bytes in [PKCS#7 format](https://datatracker.ietf.org/doc/html/rfc5652#section-6.3). Additionally, for security reasons, the total size of `padded-data` should be at least 32 bytes, to avoid leaking information about the contents. Example padding (hex representation): - 1 byte: `01` - 3 bytes: `030303` - 10 bytes: `0A0A0A0A0A0A0A0A0A0A` To add padding without information leaks, run the following steps: 1. Let `data` be the data to be padded. 2. Let `pad-length` be a random number between (inclusive) 1 and 255. 3. If the sum of the byte length of `data` and `pad-length` is less than 32, update `pad-length` so the sum is precisely 32. 4. Let `pad-byte` be the encoded unsigned 8-bit integer representation of `pad-length`. 5. Let `padded-data` be the padded data by adding `pad-length` trailing `pad-byte` bytes to `data`. To remove padding: 1. Let `pad-length` be the decoded unsigned 8-bit integer representation of the last byte of `padded-data`. 2. Let `data` be the unpadded data by ignoring the trailing `pad-length` bytes of `padded-data`. name: padded-data type: b* group-creator-container: _group: Header _doc: |- Container that is wrapped around some special group messages sent by the creator to normal group members and vice versa. fields: - _doc: |- 8 byte random group ID. Uniquely identifies the group when combined with the creator's Threema ID. name: group-id type: *group-id - _doc: |- Inner message struct. name: inner-data type: b* group-member-container: _group: Header _doc: |- Container that is wrapped around most messages sent by group members to other group members. fields: - _doc: |- The group creator's Threema ID. name: creator-identity type: *identity - _doc: |- 8 byte random group ID assigned to the group by the creator. name: group-id type: *group-id - _doc: |- Inner message struct. name: inner-data type: b* empty: _doc: |- An empty message (duh). Only used when encapsulated by an `csp_e2e_fs.Envelope` to announce a new FS version without explicit renegotiation. **Properties**: - Kind: 1:1 - Flags: None - User profile distribution: No - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: No - Outgoing: No - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Send to Threema Gateway ID group creator: N/A When the user submits this message in a 1:1, group or distribution list, _Rick Astley - Never Gonna Give You Up_ must be played and looped indefinitely on the user's device. When receiving this message, a mechanical finger must be unlatched from the device and boop the receiving user on the nose. text: _group: Conversation Messages _doc: |- A text message. **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: Text - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: Text - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled When the user submits this message in a 1:1, group or distribution list conversation: 1. Split the provided text into multiple segments with at most 6000 UTF-8 encoded bytes while preserving grapheme clusters at boundaries and let `text-segments` be the result.¹ 2. Run the _Messages Submit Steps_ with `messages` set from `text-segments` (for a group, wrapped by [`group-member-container`](ref:e2e.group-member-container)). ¹: The UI may warn the user that the message will be split if the UTF-8 encoded text exceeds 6000 bytes and request confirmation before submission. When reflected from another device as an incoming or outgoing 1:1 message: 1. Add the message to the associated 1:1 conversation. When receiving this message as a 1:1 message: 1. Add the message to the associated 1:1 conversation. When reflected from another device as an incoming or outgoing group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Add the message to the associated group conversation. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Add the message to the associated group conversation. fields: - _doc: |- UTF-8 encoded text. name: text type: b* deprecated-image: _group: Conversation Messages _doc: |- An image message. Note: This message is deprecated and may be phased out eventually. When sending images, use the [`file`](ref:e2e.file) message with the rendering type `0x01` (media). **Properties**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: N/A - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: N/A (deprecated message is not being sent) - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: N/A The image must be in JPEG format, is uploaded to the blob server and encrypted by: XSalsa20-Poly1305( key=X25519HSalsa20(.secret, .public), nonce=, ) This message is deprecated and may no longer be submitted. When reflected from another device as an incoming message: 1. Run the _Common Deprecated Image Receive Steps_. When receiving this message: 1. Run the _Common Deprecated Image Receive Steps_. The following steps are defined as the _Common Deprecated Image Receive Steps_: 1. Add the message to the associated 1:1 conversation. 2. If this message is eligible for auto-download, schedule downloading the image data from the blob server and request the blob to be removed. fields: - _doc: |- Blob ID to obtain the image data. name: image-blob-id type: *blob-id - _doc: |- Image size in bytes. name: image-size type: u32-le - _doc: |- Random nonce used to encrypt the image data. name: nonce type: *nonce deprecated-group-image: _group: Conversation Messages _doc: |- An image message (only used by groups). Note: This message is deprecated and may be phased out eventually. When sending images, use the [`file`](ref:e2e.file) message with the rendering type `0x01` (media). **Properties**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: N/A - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: N/A (deprecated message is not being sent) - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled The image must be in JPEG format, is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..01) This message is deprecated and may no longer be submitted. When reflected from another device as an incoming message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the _Common Deprecated Group Image Receive Steps_. When receiving this message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Run the _Common Deprecated Group Image Receive Steps_. The following steps are defined as the _Common Deprecated Group Image Receive Steps_: 1. Add the message to the associated group conversation. 2. If this message is eligible for auto-download, schedule downloading the image data from the blob server but do not request the blob to be removed. fields: - _doc: |- Blob ID to obtain the image data. name: image-blob-id type: *blob-id - _doc: |- Image size in bytes. name: image-size type: u32-le - _doc: |- Random symmetric key used to encrypt the image data. name: key type: *key location: _group: Conversation Messages _doc: |- A location message. **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled When the user submits this message in a 1:1, group or distribution list conversation: 1. Let `location` be the provided location with the following properties: - `latitude` being a WGS-84 string, - `longitude` being a WGS-84 string, - `accuracy` being a floating point number or undefined, - `address` being an address of a point of interest or undefined, - `name` being a name for the point of interest or undefined, 2. If `location.name` is defined but `location.address` is not defined, discard `location`, log an error and abort these steps.¹ 3. If the UTF-8 encoded bytes of `location.address` or `location.name` exceed 512 bytes, discard `location`, log an error and abort these steps.¹ 4. Run the _Messages Submit Steps_ with `messages` set from `location` (for a group, wrapped by [`group-member-container`](ref:e2e.group-member-container)). ¹: The UI should prevent submission in these cases. When reflected from another device as an incoming or outgoing 1:1 message: 1. Add the message to the associated 1:1 conversation. When receiving this message as a 1:1 message: 1. Add the message to the associated 1:1 conversation. When reflected from another device as an incoming or outgoing group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Add the message to the associated group conversation. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Add the message to the associated group conversation. fields: - _doc: |- Location coordinates and meta information encoded in comma- and line-separated UTF-8: ,[,] or ,[,]
or ,[,]
Values: - `latitude` and `longitude` are the geographic coordinates represented in a WGS-84 string. - `accuracy` is the accuracy in meters represented by a floating point number formatted as a string. - `address` is a full address. If it contains multiple lines, each line feed must be escaped (literal `\n`). - `name` is the name of a point of interest. _Latitude_ and _longitude_ must always be present while _accuracy_ is optional and should only be provided when the current location of the device is being sent. These values are comma-separated. Following values are optional and separated by line feeds (`\n`). This may be either: - a single line containing the _address_ representing the closest address matching the coordinates, or - two lines containing a point of interest _name_ and _address_ (which means that the coordinates refer to the point of interest). name: location type: b* deprecated-audio: _group: Conversation Messages _doc: |- An audio message. Note: This message is deprecated and may be phased out eventually. When sending audio, use the [`file`](ref:e2e.file) message with the rendering type `0x01` (media). **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: N/A - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: N/A (deprecated message is not being sent) - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: N/A - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: N/A (deprecated message is not being sent) - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled The audio is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..01) This message is deprecated and may no longer be submitted. When reflected from another device as an incoming 1:1 message: 1. Run the _Common Deprecated Audio Receive Steps_. When receiving this message as a 1:1 message: 1. Run the _Common Deprecated Audio Receive Steps_. When reflected from another device as an incoming group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the _Common Deprecated Audio Receive Steps_. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Run the _Common Deprecated Audio Receive Steps_. The following steps are defined as the _Common Deprecated Audio Receive Steps_: 1. Add the message to the associated conversation. 2. If this message is eligible for auto-download, schedule downloading the audio data from the blob server. Only request the blob to be removed if the associated conversation is a 1:1 conversation. fields: - _doc: |- Audio duration in seconds. name: duration type: u16-le - _doc: |- Blob ID to obtain the audio data. name: audio-blob-id type: *blob-id - _doc: |- Audio size in bytes. name: audio-size type: u32-le - _doc: |- Random symmetric key used to encrypt the audio data. name: key type: *key deprecated-video: _group: Conversation Messages _doc: |- A video message. Note: This message is deprecated and may be phased out eventually. When sending video, use the [`file`](ref:e2e.file) message with the rendering type `0x01` (media). **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: N/A - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: N/A (deprecated message is not being sent) - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: N/A - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: N/A (deprecated message is not being sent) - Edit applies to: N/A - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled The video is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..01) The thumbnail must be in JPEG format, is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..02) This message is deprecated and may no longer be submitted. When reflected from another device as an incoming 1:1 message: 1. Run the _Common Deprecated Audio Receive Steps_. When receiving this message as a 1:1 message: 1. Run the _Common Deprecated Audio Receive Steps_. When reflected from another device as an incoming group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the _Common Deprecated Audio Receive Steps_. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Run the _Common Deprecated Audio Receive Steps_. The following steps are defined as the _Common Deprecated Video Receive Steps_: 1. Add the message to the associated conversation. 2. If this message is eligible for auto-download, schedule downloading the video data from the blob server. Only request the blob to be removed if the associated conversation is a 1:1 conversation. fields: - _doc: |- Video duration in seconds. name: duration type: u16-le - _doc: |- Blob ID to obtain the video data. name: video-blob-id type: *blob-id - _doc: |- Video size in bytes. name: video-size type: u32-le - _doc: |- Blob ID to obtain the thumbnail in JPEG format. name: thumbnail-blob-id type: *blob-id - _doc: |- Thumbnail size in bytes. name: thumbnail-size type: u32-le - _doc: |- Random symmetric key used to encrypt the video and thumbnail data. name: key type: *key file: _group: Conversation Messages _doc: |- A file or media message. **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: Caption - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: Caption - Deletable by: User and sender - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled The file is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..01) The thumbnail is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..02) When the user submits this message in a 1:1, group or distribution list conversation: 1. Let `file` be all necessary properties to construct this message. 2. If the UTF-8 encoded bytes of `file.name` exceed 256 bytes, discard `file`, log an error and abort these steps.¹ 3. If the UTF-8 encoded bytes of `file.caption` exceed 6000 bytes, discard `file`, log an error and abort these steps.¹ 4. Run the _Messages Submit Steps_ with `messages` set from `file` (for a group, wrapped by [`group-member-container`](ref:e2e.group-member-container)). ¹: The UI should prevent submission in these cases. When reflected from another device as an incoming or outgoing 1:1 message: 1. Run the _Common File Receive Steps_. When receiving this message as a 1:1 message: 1. Run the _Common File Receive Steps_. When reflected from another device as an incoming or outgoing group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the _Common File Receive Steps_. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Run the _Common File Receive Steps_. The following steps are defined as the _Common File Receive Steps_: 1. Add the message to the associated conversation. 2. Schedule auto-downloading the thumbnail data from the blob server. Only request the blob to be removed if the associated conversation is a 1:1 conversation. 3. If this message is eligible for auto-download, schedule downloading the file data from the blob server. Only request the blob to be removed if the associated conversation is a 1:1 conversation. fields: - _doc: |- UTF-8, JSON-encoded object with the following fields: - Rendering type (`'j'`): - `0`: Render as a file. - `1`: Render as media (e.g. an image, audio or video). - `2`: Render as a sticker (for transparent images). If this field is not set, fall back to the value of `'i'`. If no value could be determined or the rendering type is unassigned, assume `0`. - Deprecated (`'i'`): Set this to the integer `1` if the rendering type is `1` or `2`, otherwise set this to the integer `0`. - Encryption key (`'k'`): Random symmetric key used to encrypt the blobs (file and thumbnail data) in lowercase hex string. - File blob ID (`'b'`): Blob ID in lowercase hex string representation to obtain the file data. - File media type (`'m'`): The media type of the file. - File name (`'n'`): Optional filename of the file. - File size (`'s'`): File size in bytes. - Thumbnail Blob ID (`'t'`): Optional blob containing the thumbnail file data. - Thumbnail media type (`'p'`): Media type of the thumbnail. If not set, assume `image/jpeg`. - Caption (`'d'`): Optional caption text. - Correlation ID (`'c'`): Optional random 32 byte ASCII string to collocate multiple media files. - Metadata (`'x'`): An optional metadata object as defined below. Metadata object fields depend on the media type of the file. All fields are optional but recommended to set in order to determine the layout logic while the file is being downloaded. Once the file has been parsed, the parsed data supersedes this object. For images: - Width (`'w'`): The width as an integer in px. - Height (`'h'`): The height as an integer in px. - Animated (`'a'`): Set this to the boolean `true` if the image is animated (e.g. an animated GIF). For audio: - Duration (`'d'`): The duration as a float in seconds. For video: - Width (`'w'`): The width as an integer in px. - Height (`'h'`): The height as an integer in px. - Duration (`'d'`): The duration as a float in seconds. Note that the rendering logic depends on three key fields which should be set accordingly: - Media type, - Rendering type, - Animated flag in the metadata object. name: file type: b* poll-setup: _group: Conversation Messages _doc: |- Creates a new poll or finalises an existing poll. During the lifecycle of a poll, this message will be used exactly twice: Once to create the poll, and once to close it. **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: Yes - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: N/A - Deletable by: User only (TODO(SE-383)) - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: - `0x01`: Send push notification. - User profile distribution: Yes - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: Yes - Delivery receipts: N/A - Reactions: Yes - When rejected: Re-send after confirmation - Edit applies to: N/A - Deletable by: User only (TODO(SE-383)) - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled When the user submits this message in a 1:1 or group conversation: 1. Let `poll` be all necessary properties to construct this message. 2. If the UTF-8 encoded bytes of `poll.description` exceed 256 bytes, discard `poll`, log an error and abort these steps.¹ 3. If the UTF-8 encoded bytes of any of the `poll.choices` exceeds 256 bytes, discard `poll`, log an error and abort these steps.¹ 4. JSON encode `poll` and let `encoded-poll` be the UTF-8 encoded result. 5. If `encoded-poll` exceeds 6000 bytes, discard `poll` (and `encoded-poll`), log an error and abort these steps.¹ 6. Run the _Messages Submit Steps_ with `messages` set from `encoded-poll` (for a group, wrapped by [`group-member-container`](ref:e2e.group-member-container)). ¹: The UI should prevent submission in these cases. When reflected from another device as an incoming or outgoing 1:1 message: 1. Run the _Common Poll Setup Receive Steps_. When receiving this message as a 1:1 message: 1. Run the _Common Poll Setup Receive Steps_. When reflected from another device as an incoming or outgoing group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the _Common Poll Setup Receive Steps_. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Run the _Common Poll Setup Receive Steps_. The following steps are defined as the _Common Poll Setup Receive Steps_: 1. Let `state` be the _State_ field of the message. Let `participants` be the _Participants_ field of the message. 2. Look up the poll with the given ID within the conversation. 3. If no associated poll could be found: 1. If `state` is `1` (closed), discard the message and abort these steps. 2. Add the poll to the associated conversation with the provided fields of the message and abort these steps. 4. If the associated poll is closed, discard the message and abort these steps. 5. If `state` is `0` (open), discard the message and abort these steps. 6. Close the poll with the given `participants`, ignore any other fields of the message. fields: - _doc: |- Random unique (per creator within this conversation) ID of the poll. name: id type: *poll-id - _doc: |- UTF-8, JSON-encoded object with the following fields: - Description (`'d'`): A short description/topic string for the poll. - State (`'s'`): - `0`: Poll is _open_ for votes. - `1`: Poll has been _closed_. A state transition from _closed_ to _open_ is illegal and must be ignored by the receiving client. - Answer type (`'a'`): - `0`: Single choice poll. - `1`: Multiple choice poll. Any transition from one of the types to another is illegal and must be ignored by the receiving client. - Announce type (`'t'`): - `0`: Announce votes in form of the `poll-vote` message only to the creator of the ballot. Results are invisible until the creator closes the vote and reports the final results. - `1`: Announce votes in form of the `poll-vote` message to everyone in the conversation. Interim results are therefore visible to everyone. Any transition from one of the types to another is illegal and must be ignored by the receiving client. - Display mode (`'u'`): - `0`: List mode. List choices of all participants as presented by this message. - `1`: Summary mode. Only display the total amount of votes per choice and the user's vote (if any). If the field is not present, assume _list_ mode (`0`). Any transition from one of the modes to another is illegal and must be ignored by the receiving client. - Choices type (`'o'`, DEPRECATED): Always set this to the integer `0`. - Participants (`'p'`): A list of Threema IDs that participated in the poll (i.e. they cast a vote). This field must only be present if the poll is being _closed_. In display mode _summary_, this field should be an empty list and must be ignored by the receiver. - Choices (`'c'`): A list of choice objects as defined below. Choice object fields: - Choice ID (`'i'`): A per-poll unique ID of the choice in form of an integer. Used when casting a vote. - Description (`'n'`): Choice description in form of a string. - Sort key (`'o'`, DEPRECATED): Set this to the index of the choice object within the _choices_ list. - Participant votes (`'r'`): A list of votes for this choice in the same order as the `participants` (i.e. mapped by their associated index). The integer `0` indicates that the participant did not vote for this choice. Any integer value other than `0` indicates that the participant voted for this choice. Must be of same length as `participants`. This field must only be present if the poll is being _closed_. In display mode _summary_ this should be an empty list and must be ignored by the receiver. - Total amount of votes (`'t'`): The total amount of votes for this choice. This field must only be present if the poll is being _closed_. In display mode _normal_ this field should not be present and must be ignored by the receiver. name: poll type: b* poll-vote: _group: Conversation Messages _doc: |- Cast a vote on a poll. **Properties (1:1)**: - Kind: 1:1 - Flags: None - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No¹ - Bump _last update_: No¹ - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A (can just send another `poll-vote`) - Deletable by: User only (TODO(SE-383)) - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: None - User profile distribution: Yes - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No¹ - Bump _last update_: No¹ - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A (can just send another `poll-vote`) - Deletable by: User only (TODO(SE-383)) - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled ¹: A [`poll-vote`](ref:e2e.poll-vote) updates the poll state initiated by a corresponding [`poll-setup`](ref:e2e.poll-setup), meaning it does not produce a new visible message. When the user submits this message in a 1:1 or group conversation by casting a vote: 1. Let `vote` be all necessary properties to construct this message. 2. JSON encode `vote` and let `encoded-vote` be the UTF-8 encoded result. 3. If `encoded-vote` exceeds 6000 bytes, discard `vote` (and `encoded-vote`), log an error and abort these steps.¹ 4. Run the _Messages Submit Steps_ with `messages` set from `encoded-vote` (for a group, wrapped by [`group-member-container`](ref:e2e.group-member-container)). ¹: The UI should prevent submission in these cases. When reflected from another device as an incoming or outgoing 1:1 message: 1. Run the _Common Poll Vote Receive Steps_. When receiving this message as a 1:1 message: 1. Run the _Common Poll Vote Receive Steps_. When reflected from another device as an incoming or outgoing group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the _Common Poll Vote Receive Steps_. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Run the _Common Poll Vote Receive Steps_. The following steps are defined as the _Common Poll Vote Receive Steps_. 1. Look up the poll with the given ID within the conversation. 2. If no associated poll could be found or if the associated poll is closed, discard the message and abort these steps. 3. Update the poll with the provided choices of the sender. fields: - _doc: |- The Threema ID of the creator of the poll. name: creator-identity type: *identity - _doc: |- ID of the associated poll. name: poll-id type: *poll-id - _doc: |- UTF-8, JSON-encoded list containing one or more choice tuples. Each choice tuple contains the following two integer values: - Choice ID, referring to the Choice ID defined in the `poll-setup` message. - Selected: - `0`: The choice has not been selected. - `1`: The choice has been selected. Note: For protocol simplicity, a vote must always include all possible choices, whether or not they have been selected. name: choices type: b* call-offer: _group: Conversation Messages _doc: |- Initiates a call. **Properties**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - `0x20`: Short-lived server queuing. - User profile distribution: Yes - Exempt from blocking: No - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: Yes - Bump _last update_: Yes - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: Abort call - Edit applies to: N/A - Deletable by: User only (TODO(SE-384)) - Include in history: No - Send to Threema Gateway ID group creator: N/A [//]: # "When submit / receiving / reflected: TODO(SE-102)" fields: - _doc: |- UTF-8, JSON-encoded object with the following fields: - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0 that uniquely identifies a call throughout its lifetime. Assume `0` if not set. - WebRTC Offer (`'offer'`): An offer object. - Feature negotiation (`'features'`): Optional Call Features object. Offer object fields: - WebRTC Offer SDP type (`'sdpType'`): Set this to `'offer'` and ignore offers with other types. - WebRTC Offer SDP (`'sdp'`): Opaque string containing the SDP. name: offer type: b* call-answer: _group: Conversation Messages _doc: |- Answer or reject a call. **Properties**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - `0x20`: Short-lived server queuing. - User profile distribution: Only if accepted (`action`: `1`) - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No¹ - Bump _last update_: No¹ - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: Abort call - Edit applies to: N/A - Deletable by: User only (TODO(SE-384)) - Include in history: No - Send to Threema Gateway ID group creator: N/A ¹: A [`call-answer`](ref:e2e.call-answer) updates the call state initiated by a corresponding [`call-offer`](ref:e2e.call-offer), meaning it does not produce a new visible message. [//]: # "When submit / receiving / reflected: TODO(SE-102)" fields: - _doc: |- UTF-8, JSON-encoded object with the following fields: - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0 that uniquely identifies a call throughout its lifetime. Assume `0` if not set. - Required action (`'action'`): - `0`: The call has been rejected and needs to be aborted. - `1`: The call has been accepted and a connection needs to be established. - Rejection reason (`'rejectReason'`): If the call has been rejected, this field contains a reject reason: - `0`: Generic or unspecified rejection. - `1`: The callee is busy (another call is active). - `2`: The callee did not accept the call in time. - `3`: The callee explicitly rejected the call. - `4`: The callee disabled calls. - `5`: The callee was called during an off-hour period. - WebRTC Answer (`'answer'`): An answer object. - Feature negotiation (`'features'`): Optional Call Features object. Answer object fields: - WebRTC Answer SDP type (`'sdpType'`): Set this to `'answer'` and ignore answers with other types. - WebRTC Answer SDP (`'sdp'`): Opaque string containing the SDP. name: answer type: b* call-ice-candidate: _group: Conversation Messages _doc: |- An ICE candidate for an ongoing call. **Properties**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - `0x20`: Short-lived server queuing. - User profile distribution: No - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: No¹ - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: No - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A - Deletable by: N/A - Include in history: No - Send to Threema Gateway ID group creator: N/A ¹: This message does not trigger any kind of reaction and adding ICE candidates again has no ill-effect. [//]: # "When submit / receiving / reflected: TODO(SE-102)" fields: - _doc: |- UTF-8, JSON-encoded object with the following fields: - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0 that uniquely identifies a call throughout its lifetime. Assume `0` if not set. - Deprecated (`'removed'`): Always set this to `false` and ignore messages with this field set to `true`. - WebRTC Candidates (`'candidates'`): An array of candidate objects. Candidate object fields: - WebRTC Candidate SDP (`'candidate'`): Opaque string containing the ICE candidate SDP. - WebRTC MID (`'sdpMid'`): Media stream identification string or `null`. - WebRTC Media Line Index (`'sdpMLineIndex'`): Media description line index integer or `null`. - WebRTC Username Fragment (`'ufrag'`): ICE username fragment or `null`. name: candidates type: b* call-hangup: _group: Conversation Messages _doc: |- Hang up a call. **Properties**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - User profile distribution: No - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: If no corresponding `call-offer` can be found¹ - Bump _last update_: If no corresponding `call-offer` can be found¹ - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A - Deletable by: User only (TODO(SE-384)) - Include in history: No - Send to Threema Gateway ID group creator: N/A ¹: A [`call-hangup`](ref:e2e.call-hangup) usually updates the call state initiated by a corresponding [`call-offer`](ref:e2e.call-offer), meaning it does not produce a new visible message. However, the [`call-offer`](ref:e2e.call-offer) uses the _Short-lived server queuing_ flag and therefore may be lost. In that case, the [`call-hangup`](ref:e2e.call-hangup) does create a visible _call missed_ message. [//]: # "When submit / receiving / reflected: TODO(SE-102)" fields: - _doc: |- UTF-8, JSON-encoded object. If this field contains zero bytes, assume an empty object. Contains the following fields: - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0 that uniquely identifies a call throughout its lifetime. Assume `0` if not set. name: hangup type: b* call-ringing: _group: Conversation Messages _doc: |- Sent by the callee to indicate that the call is ringing. **Properties**: - Kind: 1:1 - Flags: - `0x01`: Send push notification. - `0x20`: Short-lived server queuing. - User profile distribution: No - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No¹ - Bump _last update_: No¹ - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: Abort call - Edit applies to: N/A - Deletable by: N/A - Include in history: No - Send to Threema Gateway ID group creator: N/A ¹: A [`call-ringing`](ref:e2e.call-ringing) updates the call state initiated by a corresponding [`call-offer`](ref:e2e.call-offer), meaning it does not produce a new visible message. [//]: # "When submit / receiving / reflected: TODO(SE-102)" fields: - _doc: |- UTF-8, JSON-encoded object. If this field contains zero bytes, assume an empty object. Contains the following fields: - Call ID (`'callId'`): Random 32 bit unsigned integer greater than 0 that uniquely identifies a call throughout its lifetime. Assume `0` if not set. name: hangup type: b* delivery-receipt: _group: Status Updates _doc: |- Confirms reception or delivers detailed status updates of a message. **Properties (1:1)**: - Kind: 1:1 - Flags: None - User profile distribution: Only for reactions - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Only for reactions¹ - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes² - _Sent_ update: No - Delivery receipts: No, that would be silly! - Reactions: No (also silly) - When rejected: N/A (ignored) - Edit applies to: N/A (can just send another `delivery-receipt`) - Deletable by: N/A - Include in history: Yes - Send to Threema Gateway ID group creator: N/A ¹: Repeating a status of type _received_ or _read_ has no ill-effects. ²: When the message is being _read_ and _read_ receipts are disabled, an `d2d.IncomingMessageUpdate` will be reflected instead. **Properties (Group)**: - Kind: Group - Flags: None - User profile distribution: Only for reactions - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Only for reactions¹ - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes. When the message is being _read_ and _read_ receipts are disabled, reflect an `d2d.IncomingMessageUpdate` (since no `delivery-receipt` is sent in this case). - _Sent_ update: No - Delivery receipts: No, that would be silly! - Reactions: No (also silly) - When rejected: N/A (ignored) - Edit applies to: N/A (can just send another `delivery-receipt`) - Deletable by: N/A - Include in history: Yes - Send to Threema Gateway ID group creator: If capture is enabled ¹: Repeating a status of type _received_ or _read_ has no ill-effects. When the user opens a 1:1 conversation or marks it as _read_ by another mechanism: 1. Let `message-ids` be all message IDs associated to messages of the conversation that require _automatic_ delivery receipts and which are not yet marked as _read_. 2. Split the provided `message-ids` into bundles of at most 875 message IDs and let `message-ids-bundles` be the result.¹ 3. Run the _Messages Submit Steps_ with `messages` set to create one or more [`delivery-receipt`](ref:e2e.delivery-receipt)s from `message-ids-bundles` with status `0x02`. ¹: Each message ID has 8 bytes, divided by at most 7000 bytes. When reflected from another device as an incoming 1:1 message: 1. Run the _Common Incoming Delivery Receipt Steps_. When reflected from another device as an outgoing 1:1 message: 1. Run the _Common Outgoing Delivery Receipt Steps_. When receiving this message as a 1:1 message: 1. Run the _Common Incoming Delivery Receipt Steps_. When reflected from another device as an incoming group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. If `status` is not `0x03` or `0x04` (i.e. not a reaction), log a notice, discard the message and abort these steps. 2. Run the _Common Incoming Delivery Receipt Steps_. When reflected from another device as an outgoing group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. If `status` is not `0x03` or `0x04` (i.e. not a reaction), log a notice, discard the message and abort these steps. 2. Run the _Common Outgoing Delivery Receipt Steps_. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. If `status` is not `0x03` or `0x04` (i.e. not a reaction), log a notice, discard the message and abort these steps. 3. Run the _Common Incoming Delivery Receipt Steps_. The following steps are defined as the _Common Incoming Delivery Receipt Steps_: 1. For each `message-id` of `message-ids`: 1. Lookup the associated message for `message-id` in the associated conversation and let `referred-message` be the result. 2. If `referred-message` is not defined, abort these sub-steps. 3. If the associated conversation is a 1:1 conversation and the original sender of `referred-message` is not the user, abort these sub-steps. 4. If `status` is `0x01` or `0x02` (i.e. a delivery receipt) and `referred-message` allows for delivery receipts (see the associated _Delivery receipts_ property), apply and replace the delivery receipt of the sender to `referred-message` with the associated timestamp set to the message's (the `delivery-receipt`'s) `created-at`. 5. If `status` is `0x03` or `0x04` (i.e. a reaction) and `referred-message` is reactable (see the associated _Reactions_ property), apply and replace any existing reaction of the sender to `referred-message` with the reaction timestamp set to the message's (the `delivery-receipt`'s) `created-at`.¹ The following steps are defined as the _Common Outgoing Delivery Receipt Steps_: 1. For each `message-id` of `message-ids`: 1. Lookup the associated message for `message-id` in the associated conversation and let `referred-message` be the result. 2. If `referred-message` is not defined, abort these sub-steps. 3. If the associated conversation is a 1:1 conversation and the original sender of `referred-message` is the user, abort these sub-steps. 4. If `status` is `0x01` or `0x02` (i.e. a delivery receipt) and `referred-message` allows for delivery receipts (see the associated _Delivery receipts_ property), apply and replace the delivery receipt of the user to `referred-message` with the associated timestamp set to the message's (the `delivery-receipt`'s) `created-at`. 5. If `status` is `0x03` or `0x04` (i.e. a reaction) and `referred-message` is reactable (see the associated _Reactions_ property), apply and replace any existing reaction of the user to `referred-message` with the reaction timestamp set to the message's (the `delivery-receipt`'s) `created-at`.¹ ¹: Note that the deprecated reactions transmitted by a `delivery-receipt` always replace **all existing reactions** of the respective sender, including new-style reactions transmitted by a `csp_e2e.Reaction` message. fields: - _doc: |- Message status: - `0x01`: Message was received. - `0x02`: Message was read. - `0x03`: **Deprecated** Maps to the 👍 emoji (`1F44D`). - `0x04`: **Deprecated** Maps to the 👎 emoji (`1F44E`). Note that only the `0x01` and `0x02` variants are considered true _delivery receipts_ whereas the deprecated `0x03` and `0x04` variants are considered _reactions_ (just like `csp_e2e.Reaction`). The following replacement logic is to be applied on a message's status when displayed: 1. `0x02` replaces groups listed below, 2. `0x01` replaces the unlisted _created_ status. name: status type: u8 - _doc: |- One or more `message-id`s whose status should be updated. name: message-ids type: *message-ids typing-indicator: _group: Status Updates _doc: |- Indicates whether a contact is currently typing. **Properties**: - Kind: 1:1 - Flags: - `0x02`: No server queuing. - `0x04`: No server acknowledgement. - User profile distribution: No - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: No¹ - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: No - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A - Deletable by: N/A - Include in history: No - Send to Threema Gateway ID group creator: N/A ¹: It is deemed acceptable if the _typing_ indicator in the UI is replayed since there is no further consequence. When the user is currently _typing_ while composing a **new**¹ message in an associated conversation: 1. Schedule a volatile task to run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the targeted receiver, - to construct this message with `is-typing` set to `1`. 2. Start a _user is typing_ timer in the conversation to rerun these steps in 10s. When the user stopped _typing_ while composing a message in an associated conversation, or when the user left the conversation view: 1. If no _user is typing_ timer is running for the conversation, abort these steps. 2. Stop the _user is typing_ timer of the conversation. 3. Schedule a volatile task to run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the targeted receiver, - to construct this message with `is-typing` set to `0`. When reflected from another device as an incoming message: 1. Run the _Common Typing Indicator Receive Steps_. When receiving this message: 1. Run the _Common Typing Indicator Receive Steps_. The following steps are defined as the _Common Typing Indicator Receive Steps_: 1. If `is-typing` is `1`, start a timer to display that the sender is typing in the associated conversation for the next 15s. 2. If `is-typing` is `0`, cancel any running timer displaying that the sender is typing in the associated conversation. ¹: Editing a message may not trigger _typing_. fields: - _doc: |- Set to `1` in case the contact is currently typing or `0` in case the contact stopped typing. Other values are invalid. name: is-typing type: u8 set-profile-picture: _group: Contact and Group Control _doc: |- Set the profile picture of a contact or a group. **Properties (1:1)**: - Kind: 1:1 - Flags: None - User profile distribution: No (obviously) - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A (can just send another `set-profile-picture`) - Deletable by: N/A (can just send a `delete-profile-picture`) - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: None - User profile distribution: No (obviously) - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A¹ - Edit applies to: N/A (can just send another `set-profile-picture`) - Deletable by: N/A (can just send a `delete-profile-picture`) - Include in history: Yes - Send to Threema Gateway ID group creator: N/A ¹: For the group creator it will be handled as if `group-sync-request` was received, re-sending the group profile picture state, implicitly triggered by FS `Reject` receive steps. The profile picture must be in JPEG format, is uploaded to the blob server and encrypted by: XSalsa20-Poly1305(key=, nonce=00..01) When reflected from another device as an outgoing 1:1 message: 1. Update the most recently distributed profile picture cache for the contact to the enclosed blob ID. When receiving this message as a 1:1 message: 1. Download the picture from the blob server but do not request the blob to be removed. Store the profile picture. 2. Store the picture as the _contact-defined_ profile picture and run the _Contact Profile Picture Selection Steps_. When receiving this message as a group message (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Download the picture from the blob server but do not request the blob to be removed. Let `profile-picture` be the result. 3. Let `group` be a snapshot of the current group state. 4. If `group.profile-picture` is defined and equals `profile-picture` (i.e. no changes), discard the message and abort these steps. 5. (MD) Run the following sub-steps: 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning that a group sync race occurred, discard the message and abort these steps. 2. (MD) Let `group` be a snapshot of the current group state. 3. (MD) If `group.profile-picture` is defined and equals `profile-picture`, log a warning that a group sync race occurred. 4. (MD) Reflect a `GroupSync.Update` with `group` set to contain `profile_picture` set to `profile-picture. 5. (MD) Commit the transaction and await acknowledgement. 6. Store the profile picture and and apply it to the group. fields: - _doc: |- Blob ID to obtain the image data. name: picture-blob-id type: *blob-id - _doc: |- Profile picture size in bytes. name: picture-size type: u32-le - _doc: |- Random symmetric key used to encrypt the image data. name: key type: *key delete-profile-picture: _group: Contact and Group Control _doc: |- Delete the profile picture of a contact. **Properties (1:1)**: - Kind: 1:1 - Flags: None - User profile distribution: No (obviously) - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A (can just send another `delete-profile-picture`) - Deletable by: N/A - Include in history: Yes - Send to Threema Gateway ID group creator: N/A **Properties (Group)**: - Kind: Group - Flags: None - User profile distribution: No (obviously) - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A¹ - Edit applies to: N/A (can just send another `delete-profile-picture`) - Deletable by: N/A - Include in history: Yes - Send to Threema Gateway ID group creator: N/A ¹: For the group creator it will be handled as if `group-sync-request` was received, re-sending the group profile picture state, implicitly triggered by FS `Reject` receive steps. When reflected from another device as an incoming 1:1 message: 1. Remove the _contact-defined_ profile picture and run the _Contact Profile Picture Selection Steps_. When reflected from another device as an outgoing 1:1 message: 1. Update the most recently distributed profile picture cache for the contact to a _remove_ mark with the reflected timestamp. When receiving this message as a 1:1 message: 1. Remove the _contact-defined_ profile picture and run the _Contact Profile Picture Selection Steps_. When receiving this message as a group message (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If `group.profile-picture` is not defined (i.e. no change), discard the message and abort these steps. 4. (MD) Run the following sub-steps: 1. Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning that a group sync race occurred, discard the message and abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If `group.profile-picture` is not defined, log a warning that a group sync race occurred. 4. Reflect a `GroupSync.Update` with `group` set to contain `profile_picture` set to be removed. 5. Commit the transaction and await acknowledgement. 5. Remove the profile picture of the group. contact-request-profile-picture: _group: Contact and Group Control _doc: |- Request a contact's profile picture. Note that this message does not result in the profile picture being sent immediately in reply to this message. Instead, it will be sent the next time that contact sends a message to the user (if one is set, and if the user is eligible for receiving the profile picture). **Properties**: - Kind: 1:1 - Flags: None - User profile distribution: No - Exempt from blocking: No - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: No - _Sent_ update: No - Delivery receipts: No - Reactions: No - When rejected: N/A (ignored) - Edit applies to: N/A - Deletable by: N/A - Include in history: No - Send to Threema Gateway ID group creator: N/A When reflected from another device as an incoming message: 1. Run the _Common Request Profile Picture Receive Steps_. When receiving this message: 1. Run the _Common Request Profile Picture Receive Steps_. The following steps are defined as the _Common Request Profile Picture Receive Steps_: 1. Purge the most recently distributed profile picture cache for the sender. group-setup: _group: Contact and Group Control _doc: |- Announces the group setup to all participants. The group creator is always a member of the group and must not be included in the member list. This is sent by the creator to create a new group, as well as update and disband an existing group. The group creator sends this message to all current (including those to be removed) and newly added group members. The group creator may also send this to a single receiver in special cases. Since the group creator is not allowed to leave the group, the only way for it to stop being a member is by sending a `group-setup` with an empty members list and thereby disbanding the group. **Properties**: - Kind: Group - Flags: None - User profile distribution: Yes - Exempt from blocking: See dedicated steps - Implicit _direct_ contact creation: Yes - Protect against replay: Yes - Unarchive: No¹ - Bump _last update_: No² - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A³ - Edit applies to: N/A (can just send another `group-setup`) - Deletable by: N/A (can just send another `group-setup`) - Include in history: Yes - Send to Threema Gateway ID group creator: N/A ¹: A newly created group's conversation visibility is implicitly _normal_ and therefore not _archived_. For the sake of simplicity and sender/receiver symmetry, further updates to the group should not alter the conversation visibility. ²: A newly created group is implicitly created with _last update_ set to the current timestamp. For the sake of simplicity and sender/receiver symmetry, no further updates to the group should bump _last update_. ³: For the group creator it will be handled as if `group-sync-request` was received, re-sending the group state, implicitly triggered by FS `Reject` receive steps. The following steps are the dedicated blocking exemption steps for this message as a group message (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)): 1. Look up the group. 2. If the group could be found, return that the message passed the blocking check. 3. Run the _Identity Blocked Steps_ for the creator. If the result indicates that the creator is not blocked, return that the message passed the blocking check. Otherwise return that the message needs to be discarded. When receiving this message as a group message (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)): 1. Let `members` be the given member list. Remove all duplicate entries from `members`. Remove the sender (creator) from `members` if present. 2. Look up the group. 3. If the group could be found: 1. Let `group` be a snapshot of the current group state. 2. If the group is marked as _left_ and `members` is empty (i.e. no change), discard the message and abort these steps. 3. If the group is not marked as _left_: 1. Let `current-members` be a copy of `group.members`. 2. Add the user to `current-members`. 3. If `current-members` equals `members` (i.e. no change), discard the message and abort these steps. 4. If `members` does not include the user: 1. If the group could not be found, discard the message and abort these steps. 2. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, discard the message and abort these steps. 3. (MD) Reflect a `GroupSync.Update` with `group` set to contain the `user_state` set to `KICKED`. 4. (MD) Commit the transaction and await acknowledgement. 5. If the user is currently participating in a group call of this group, trigger leaving the call. 6. Mark the group as _left_. 7. Persist the previous member setup so that the group can be cloned. 8. Run the _Rejected Messages Refresh Steps_ for the group. 5. If `members` includes the user. 1. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the sender (creator) contact does not exist, log an error, discard the message and abort these steps. 2. Run the _Valid Contacts Lookup Steps_ for `members` and overwrite `members` with the result. 3. For each `contact-or-init` of `members`: 1. If `contact-or-init` indicates that the _contact is the user_, remove the entry from `members` and abort these sub-steps. 2. If `contact-or-init` indicates that the _contact is invalid_, remove the entry from `members`, log a warning and abort these sub-steps. 4. (MD) Let `pending-reflect-acks` be an empty list. 5. For each `contact-or-init` of `members`: 1. If `contact-or-init` is an existing contact, abort these sub-steps. 2. (MD) Reflect a `ContactSync.Create` with `contact` set from `contact-or-init` and the following additional properties: - `created_at` set to now, - `acquaintance_level` set to `GROUP`, - all policies and categories set to their defaults. 3. (MD) Add the pending reflect acknowledgement to `pending-reflect-acks`. 6. (MD) Await all `pending-reflect-acks`. 7. Let `added-members` be a copy of `members`. 8. Let `group` be a snapshot of the current group state or undefined if the group does not exist. 9. If `group` is not defined: 1. Let `removed-members` be an empty list. 2. (MD) Reflect a `GroupSync.Create` with `group` set to contain: - `group_identity`, - `created_at`, - `name` empty, - `user_state` set to `MEMBER`, - `profile_picture` empty, - `member_identities` from `members`, - all policies and categories set to their defaults. 10. If `group` is defined: 1. Remove all members from `added-members` that are in `group.members`. 2. Let `removed-members` be a copy of `group.members`. 3. Remove all members from `removed-members` that are in `members`. 4. (MD) Reflect a `GroupSync.Update` with `member_state_changes` constructed from `added-members` and `removed-members` and `group` set to contain the following additional properties: - `user_state` set to `MEMBER`, - `member_identities` from `members`. 11. (MD) Commit the transaction and await acknowledgement. 12. If the user is currently participating in a group call of this group, remove all `removed-members` participants from the group call (handle them as if they left the call) and unblock all pending group call flows for `added-members`. 13. Persist newly added contacts from `members`. 14. Persist the newly created group or the member changes to the group. If the group was previously marked as _left_, remove the _left_ mark. 15. TODO(SE-510): Schedule fetching gateway-defined profile picture here for each newly added contact from `members`, if necessary. 16. If `added-members` or `removed-members` is not empty, run the _Rejected Messages Refresh Steps_ for the group. fields: - _doc: |- A set of Threema IDs defining group membership. The creator's Threema ID is always inferred and must not be included in this set. name: members type: *identities group-name: _group: Contact and Group Control _doc: |- Name (or rename) a group. Sent to all group members when the group is being created for the first time or the group is being renamed. May also be sent to a single receiver as a response to a [`group-sync-request`](ref:e2e.group-sync-request) message. **Properties**: - Kind: Group - Flags: None - User profile distribution: No - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A¹ - Edit applies to: N/A (can just send another `group-name`) - Deletable by: N/A (can just send an empty name) - Include in history: Yes - Send to Threema Gateway ID group creator: N/A ¹: For the group creator it will be handled as if `group-sync-request` was received, re-sending the group name, implicitly triggered by FS `Reject` receive steps. When receiving this message as a group message (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)): 1. Run the [_Common Group Receive Steps_](ref:e2e#receiving). If the message has been discarded, abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If `group.name` equals `name` (i.e. no change), discard the message and abort these steps. 4. (MD) Run the following sub-steps: 1. Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning that a group sync race occurred, discard the message and abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If `group.name` equals `name`, log a warning that a group sync race occurred. 4. Reflect a `GroupSync.Update` with `group` set to contain `name` set to `name`. 5. Commit the transaction and await acknowledgement. 5. Update the group's name with `name`. fields: - _doc: |- UTF-8 encoded string containing the group's name. name: name type: b* group-leave: _group: Contact and Group Control _doc: |- Sent by a group member... * that is leaving the group. The message is sent to all other group members and the creator. * in direct reply to a group message for a group that it has marked as left. Note: The group creator is not allowed to leave the group. **Properties**: - Kind: Group - Flags: None - User profile distribution: No - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A¹ - Edit applies to: N/A - Deletable by: N/A - Include in history: Yes - Send to Threema Gateway ID group creator: Yes ¹: Re-send of `group-leave` implicitly triggered by FS `Reject` receive steps due to _Common Group Receive Steps_ invocation. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. If the sender is the creator of the group, log a warning, discard the message and abort these steps. 2. Look up the group. 3. If the group could not be found or is marked as _left_: 1. If the user is the creator of the group (as alleged by the message), discard the message and abort these steps. 2. Run the _Identity Blocked Steps_ for the creator of the group. If the result indicates that the creator is blocked, discard the message and abort these steps. 3. Run the _Group Sync Request Steps_ for the group, discard the message and abort these steps. 4. Let `group` be a snapshot of the current group state. 5. If `group.members` does not include the sender, discard the message and abort these steps. 6. (MD) Run the following sub-steps: 1. Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist or the group is marked as _left_, log a warning that a group sync race occurred, discard the message and abort these steps. 2. Let `group` be a snapshot of the current group state. 3. If `group.members` does not include the sender, log a warning that a group sync race occurred. 4. Let `updated-members` be a copy of `group.members`. 5. Remove the sender from `updated-members`. 6. Reflect a `GroupSync.Update` with `member_state_changes` set to the single entry of the sender leaving and `group` set to contain `member_identities` set from `updated-members`. 7. Commit the transaction and await acknowledgement. 7. If the user is currently participating in a group call of this group, remove the sender from the group call (handle it as if the sender left the call). 8. Remove the sender from the group. 9. Run the _Rejected Messages Refresh Steps_ for the group. group-sync-request: _group: Contact and Group Control _doc: |- Sent by a group member (or a device assuming to be part of the group) to the group creator. **Properties**: - Kind: Group - Flags: None - User profile distribution: No - Exempt from blocking: Yes - Implicit _direct_ contact creation: No - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: Yes - Outgoing: Yes - _Sent_ update: No - Delivery receipts: N/A - Reactions: No - When rejected: N/A¹ - Edit applies to: N/A - Deletable by: N/A - Include in history: Yes - Send to Threema Gateway ID group creator: Yes ¹: Implicitly ignored by FS `Reject` receive steps. The following steps are defined as the _Group Sync Request Steps_: 1. If the user is the creator of the group, log an error and abort these steps. 2. If the group is marked as _recently resynced_ for the user, log a notice and abort these steps.¹ 3. Run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the creator of the group, - to construct this message (wrapped by [`group-member-container`](ref:e2e.group-member-container)) 4. Mark the group as _recently resynced_ for the user for 1h. When receiving this message as a group message (wrapped by [`group-member-container`](ref:e2e.group-member-container)): 1. Look up the group. If the group could not be found, discard the message and abort these steps. 2. If the user is not the creator of the group, discard the message and abort these steps. 3. If the group is marked as _recently resynced_ for the sender, log a notice, discard the message and abort these steps.¹ 4. (MD) Begin a transaction with scope `GROUP_SYNC` and the following precondition: 1. If the group does not exist, log a warning that a group sync race occurred, discard the message and abort these steps. 5. If the group is marked as _left_ or the sender is not a member of the group, run the _Bundled Messages Send Steps_ with the following properties: - `id` set to a random message ID, - `created-at` set to the current timestamp, - `receivers` set to the sender, - to construct a [`group-setup`](ref:e2e.group-setup) (wrapped by [`group-creator-container`](ref:e2e.group-creator-container)) with an empty members set. 6. If the group is not marked as _left_ and the sender is a member of the group, run the _Active Group State Resync Steps_ with four random message IDs and `target-members` set to the sender. 7. (MD) Commit the transaction and await acknowledgement. ¹: This is a precaution since a `group-sync-request` is automatically triggered and creates an automatic response. This can easily lead to message loops. Limiting `group-sync-request`s to once an hour per group per sender/receiver breaks a potential infinite loop. web-session-resume: _group: Push Control _doc: |- A control message from Threema Web, requesting a session to be resumed. **Properties (1:1)**: - Kind: 1:1 - Flags: - `0x20`: Short-lived server queuing. - User profile distribution: N/A (not sent by apps) - Exempt from blocking: Yes - Implicit _direct_ contact creation: N/A (blocking is circumvented) - Protect against replay: Yes - Unarchive: No - Bump _last update_: No - Reflect: - Incoming: No - Outgoing: No - _Sent_ update: No - Delivery receipts: N/A - Reactions: N/A - When rejected: N/A (not sent by clients) - Edit applies to: N/A - Deletable by: N/A - Include in history: No - Send to Threema Gateway ID group creator: N/A When receiving this message: 1. If the sender is not `*3MAPUSH`, discard the message and abort these steps. 2. Lookup the web client session associated to `wcs` and attempt to resume it. fields: - _doc: |- UTF-8, JSON-encoded object with the following fields: - Webclient session (`'wcs'`): SHA256 hash (hex encoded) of the public permanent key of the session initiator, string. - Affiliation ID (`'wca'`): An optional identifier for affiliating consecutive pushes, `string` or `null`. - Affiliation ID (`'wct'`): Unix epoch timestamp of the request in seconds, `i64`. - Protocol version (`'wcv'`): Version of the Threema Web protocol, `u16`. All fields must be part of the JSON object, even if their values are nullable. name: push-payload type: b* # Parsed struct namespaces (mapped into separate files) namespaces: index: *index handshake: *handshake payload: *payload e2e: *e2e