Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/JAVA/Threema/domain/protocol/src/     Datei vom 25.3.2026 mit Größe 35 kB image not shown  

Quelle  csp-e2e-fs.proto   Sprache: unbekannt

 
Spracherkennung für: .proto vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

// # Forward Security Subprotocol
//
// This protocol specifies forward security for end-to-end encrypted chat server
// messages. It is based on sessions established between two parties, where each
// session has a unique ID and is associated with ephemeral key material
// negotiated between the parties using a key exchange mechanism and a hash
// chain based key derivation.
//
// Each party is either an initiator or a responder in a given session. Once
// established, a session will be used bidirectionally.
//
// Content messages can take any other type that could normally be sent without
// Forward Security, and wrap the message contained within using a separate
// cryptographic layer that provides Forward Security.
//
// ## Terminology
//
// - `CK`: Client Key (permanent secret key associated to the Threema ID)
// - `FS`: Forward Security
// - `SI`: Session Initiator
// - `SR`: Session Responder
// - `2DH`: Unidirectional forward security with two DH calculations
// - `4DH`: Bidirectional forward security with four DH calculations
// - `FSSK`: Forward Security Session Key (ephemeral key pair generated by both
//   sides for each session)
// - `2DHK`: 2DH key (current iteration)
// - `4DHK`: 4DH key (current iteration)
// - `XDHK`: 2DH or 4DH key (current iteration)
// - `XDHMK`: 2DH or 4DH message key (current iteration)
//
// ## Modes
//
// Note: This is an informational section. The concrete steps surrounding modes
// are described in the steps for the `Envelope`.
//
// A key negotiation normally needs active participation by both involved
// parties before any actual messages can be exchanged. This is not practical in
// a messaging app, as the other party may not be online at the time when the
// first message is sent.
//
// Thus, the protocol specifies two modes, called 2DH and 4DH which will be
// described briefly in the following sections.
//
// ### 2DH Mode
//
// 2DH mode can be used immediately, even in a new session, as it does not
// involve any additional key material from the responder (i.e. remote party).
// However, it only protects against a compromise of the initiator's permanent
// secret key, not of the responder's permanent secret key. It is still better
// than sending all messages without Forward Security until a bidirectional 4DH
// session has been negotiated.
//
// ### 4DH Mode
//
// A session enters 4DH mode when the initiator has processed the responder's
// `Accept` message and sent an `Encapsulated.Confirm` message back to the
// responder. At this point, both sides have the required key material to derive
// the 4DH root chain key from. Messages sent from this point on are secure even
// in the event of a future compromise of the permanent secret key of either
// party.
//
// ## Keys
//
// Each party uses two different asymmetric X25519 key pairs:
//
// - `CK`: Client Key, the permanent secret key associated to the user's Threema
//   ID.
// - `FSSK`: Forward Security Session Key, an ephemeral key pair for the FS
//   session.
//
// Each party also learns the remote's `FSSK.public` from the `Init` or `Accept`
// message. The remote's `CK.public` of the peer's Threema ID is assumed to be
// already known.
//
// ## Key Derivation
//
// The two parties maintain the following keys during a session:
//
// - Local 2DHK (initiator only)
// - Remote 2DHK (responder only)
// - Local 4DHK (both parties)
// - Remote 4DHK (both parties)
//
// For each key, the current iteration of the key and a counter (see below) must
// be stored.
//
// The initiator derives the initial keys as follows:
//
//     local-2DHK = BLAKE2b(
//       input=
//            X25519HSalsa20(<local.CK>.secret, <remote.CK>.public)
//         || X25519HSalsa20(<local.FSSK>.secret, <remote.CK>.public),
//       salt='ke-2dh-<local-threema-id>',
//       personal='3ma-e2e',
//     )
//
//     local-4DHK = BLAKE2b(
//       key=BLAKE2b(
//         out-length=64,
//         input=
//              X25519HSalsa20(<local.CK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.CK>.secret, <remote.FSSK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.FSSK>.public)
//       ),
//       salt='ke-4dh-<local-threema-id>',
//       personal='3ma-e2e',
//     )
//
//     remote-4DHK = BLAKE2b(
//       key=BLAKE2b(
//         out-length=64,
//         input=
//              X25519HSalsa20(<local.CK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.CK>.secret, <remote.FSSK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.FSSK>.public)
//       ),
//       salt='ke-4dh-<remote-threema-id>',
//       personal='3ma-e2e',
//     )
//
// The responder calculates the initial keys as follows:
//
//     remote-2DHK = BLAKE2b(
//      input=
//              X25519HSalsa20(<local.CK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.CK>.secret, <remote.FSSK>.public),
//       salt='ke-2dh-<remote-threema-id>',
//       personal='3ma-e2e',
//     )
//
//     local-4DHK = BLAKE2b(
//       key=BLAKE2b(
//         out-length=64,
//         input=
//              X25519HSalsa20(<local.CK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.CK>.secret, <remote.FSSK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.FSSK>.public)
//       ),
//       salt='ke-4dh-<local-threema-id>',
//       personal='3ma-e2e',
//     )
//
//     remote-4DHK = BLAKE2b(
//       key=BLAKE2b(
//         out-length=64,
//         input=
//              X25519HSalsa20(<local.CK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.CK>.secret, <remote.FSSK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.CK>.public)
//           || X25519HSalsa20(<local.FSSK>.secret, <remote.FSSK>.public)
//       ),
//       salt='ke-4dh-<remote-threema-id>',
//       personal='3ma-e2e',
//     )
//
// Each of the `*DHK` keys will be superseded after a message has been encrypted
// in the respective direction as follows:
//
//     XDHK' = BLAKE2b(
//       key=<local|remote>-XDHK,
//       salt='kdf-ck',
//       personal='3ma-e2e',
//     )
//
// However, actual messages are encrypted by:
//
//     XDHMK = BLAKE2b(
//       key=<local|remote>-XDHK,
//       salt='kdf-aek',
//       personal='3ma-e2e',
//     )
//
// So, for example, when an outgoing 4DH message is being sent, it will be
// encrypted by deriving `XDHMK` from `local-4DHK` and then `local-4DHK` will be
// replaced by applying `XDKH'` to `local-4DHK`.
//
// ### Key Counters
//
// Note: This is an informational section. The concrete processing steps
// required for keys and key counters are deeply integrated into the processing
// steps of the `Envelope`.
//
// Each local/remote 2DHK/4DHK is associated with a counter, which describes how
// many times the XDHK' has been applied since the initial key derivation.
// Whenever a new message has been sent in a session, the corresponding counter
// must be incremented and the key replaced by applying the XDHK' derivation on
// it. As this operation cannot be reversed, counter values cannot go back.
//
// To account for lost messages (e.g. when the recipient has been offline for an
// extended period of time or when a short-lived message such as
// `typing-indicator` or `call-offer` has been removed from the chat server),
// either party must be prepared to accept counters that have skipped a few
// values, and apply XDHK' as many times as is needed to reach the new counter
// value. To limit the CPU impact, the permissible counter delta is limited to
// 25'000.
//
// Note: A counter delta of 25'000 allows for roughly 100 hours of typing while
// the recipient is offline. Users who exceed that are considered insane and out
// of scope!
//
// ## Session States
//
// This section explains the various possible session states.
//
// Note: This is an informational section. The concrete state creation and
// transformation steps are deeply integrated into the processing steps of the
// `Envelope`.
//
// The _local_ session states are only valid for SI while the _remote_ session
// states are only valid for SR.
//
// A session is considered _bidirectional_ 4DH if the session state is
// `local-out-4dh-in-4dh` (`L44`) or `remote-in-4dh-out-4dh` (`R44`).
//
// ### `local-out-2dh-in-none` (`L20`)
//
// - An `Init` has been sent to remote.
// - Outgoing messages are protected with 2DH.
// - Incoming messages are not allowed by this state.
// - Once an `Accept` has been received for this session, we move into
//   `local-out-4dh-in-4dh` (`L44`) state.
//
// ### `local-out-4dh-in-4dh` (`L44`)
//
// - An `Accept` has been received for this session.
// - Outgoing and incoming messages are protected with 4DH.
//
// ### `remote-in-2dh-out-none` (`R20`)
//
// - An `Init` has been received and the session has been created. An `Accept`
//   is supposed to be sent with the next outgoing message to remote.
// - Incoming messages are protected with 2DH.
// - Outgoing messages are not allowed by this state.
// - Once the `Accept` has been sent, we move into `remote-in-2dh-out-4dh`
//   (`R24`).
//
// ### `remote-in-2dh-out-4dh` (`R24`)
//
// - An `Accept` has been sent to remote.
// - Incoming messages are protected with 2DH.
// - Outgoing messages are protected with 4DH.
// - Once a 4DH encrypted `Encapsulated` message has been received for this
//   session, we move into `remote-in-4dh-out-4dh` (`R44`).
//
// ### `remote-in-4dh-out-4dh` (`R44`)
//
// - An incoming 4DH protected `Encapsulated` message has been received for this
//   session.
// - Outgoing and incoming messages are protected with 4DH.
//
// ## Protocol Flow
//
// Note: This is an informational section. The concrete flows are deeply
// integrated into the incoming/outgoing message processing steps.
//
// ### Session Establishment
//
// An FS session negotiation is typically started when a user sends the first
// message to a remote Threema ID. The user assumes the role of SI, creates a
// new `L20` session by sending an `Init` message, followed by any number of
// `Encapsulated` messages in 2DH mode.
//
//     SI ------------- Init -------------> SR   [1]
//     SI ------ Encapsulated (2DH) ------> SR   [0..N]
//
// Note that `Encapsulated` messages in 2DH mode are limited to the minimum
// version that is announced in the `Init` because the initiator does not know
// yet which version is supported by the responder.
//
// SR will create a corresponding `R20` session. Once an outgoing message
// towards SI is being triggered by SR, it will send an `Accept` and move the
// session into `R24` state, followed by any number of `Encapsulated` messages
// in 4DH mode.
//
//     SI <----------- Accept ------------- SR   [1]
//     SI <----- Encapsulated (4DH) ------- SR   [1..N]
//
// When SI receives the `Accept`, it will move the session into `L44` state,
// followed by any number of `Encapsulated` messages in 4DH mode.
//
//     SI ------ Encapsulated (4DH) ------> SR   [0..N]
//
// With the first `Encapsulated` message received, SR will move the session into
// `R44` state.
//
// At this point, the session has been fully established as a _bidirectional_
// 4DH session and either side may send any number of `Encapsulated` messages in
// 4DH mode.
//
// At any point either party may `Terminate` a session, for example when the
// user is about to delete their account (or FS session information) and wants
// to notify the peer that the key material should be discarded.
//
// If any party receives a `Encapsulated` message that it cannot decrypt (e.g.
// due to having lost the FS key material), it eventually sends a `Reject`
// message to inform the other party that it can no longer use this particular
// FS session, and also tells it the ID of the message that could not be
// decrypted. The sender can then (after manual confirmation by the user)
// re-send the message in a new FS session.
//
// ## Handling Session Loss
//
// A party could lose FS sessions information, e.g. due to reinstallation or
// relocation of the app to a new device. FS sessions are not included in
// backups, as this would conflict with their ephemeralness. When a party
// receives a message that it cannot decrypt due to a missing session, it
// signals this to the other party using a `Reject` message.
//
// ## Race Conditions
//
// It is possible for both parties to create an FS session independently, before
// having received the other party's `Init`. In that case, both parties
// determinstically run a conflict resolve mechanism based on the session ID.
//
// Each party will proceed normally to ensure any messages sent while the race
// is ongoing will be received. When choosing a session for sending a new
// message and there is more than one _bidirectional_ 4DH session available for
// the desired peer, the one with the lowest session ID will be chosen and the
// other one removed. This will ensure that both parties will eventually start
// using the same session in both directions.
//
// ## Notifying the User
//
// To prevent undetected MITM attacks by a third party that has gained access to
// the permanent secret key of either party, the user will be informed whenever
// a new session is negotiated or a non-FS message has been received even though
// an FS session was available.
//
// ## Session Implementation
//
// Note: This is a normative section. The steps will imply that these rules are
// being applied!
//
// Session uniqueness is determined by the following tuple:
//
// - the remote Threema ID,
// - a session ID.
//
// When looking up an FS session, this tuple must always be provided!
//
// An FS session has the following properties:
//
// - a remote Threema ID,
// - a session ID, and
// - a state and all necessary properties associated to that state.
//
// An FS session must be stored in a standalone storage and not as part of the
// contact because FS sessions can be created prior to the contact being
// created.
//
// When an FS session state _transitions_, _moves_ or _advances_, all keys that
// are no longer required must be erased securely.
//
// When an XDHK key iteration is being _replaced_, it must be erased securely.
//
// When an FS session is being _removed_ or _replaced_, it and all associated
// properties must be erased securely.
//
// When a contact is being removed, all associated FS sessions must be erased
// securely as well.
//
// ### State Properties
//
// The following properties, and **only** those, must be associated to the
// state.
//
// `L20` properties:
//
// - `version-range`: Supported version range as announced in the `Init` by
//   local (immutable)
// - `local-fssk`: Local FSSK (secret key, immutable)
// - `local-2dhk`: Current key state of the `local-2DHK` for outgoing messages
//   (using version `version-range.min`, immutable)
//
// `R20` properties:
//
// - `version-range`: Supported version range as announced in the `Init` by
//   remote (immutable)
// - `remote-fssk`: Remote FSSK (public key, immutable)
// - `remote-2dhk`: Current key state of the `remote-2DHK` for incoming messages
//   (using version `version-range.min`, mutable)
//
// `R24` properties:
//
// - `remote-2dhv`: Current version used for incoming messages (immutable)
// - `remote-2dhk`: Current key state of the `remote-2DHK` for incoming messages
//   still in flight (mutable).
// - `local-4dhv`: Current version used for outgoing messages (immutable)
// - `local-4dhk`: Current key state of the `local-4DHK` for outgoing messages
//   (mutable).
// - `remote-4dhv`: Version used for incoming messages in `R44` state
//   (immutable)
// - `remote-4dhk`: Key for incoming messages in `R44` state (immutable).
//
// `L44` and `R44` properties:
//
// - `local-4dhv`: Current version used for outgoing messages (mutable).
// - `local-4dhk`: Current key state of the `local-4DHK` for outgoing messages
//   (mutable).
// - `remote-4dhv`: Current version used for incoming messages (mutable).
// - `remote-4dhk`: Current key state of the `remote-4DHK` for incoming messages
//   (mutable).
//
// ### Key State
//
// A key state contains a key iteration, i.e. a local or remote `XDHK`, and the
// associated counter.
//
// The key state is bound to a specific direction (i.e. _local_ for outgoing
// messages or _remote_ for incoming messages).
//
// The following steps are defined as the _XDHK Incoming Advance Steps_:
//
// 1. Let `target-counter` be the targeted `XDHK` key counter.
// 2. Let `xdhk` be the current `XDHK` iteration of the state.
// 3. Let `distance` be the counter distance between the counter associated to
//    `xdhk` and `target-counter`.
// 4. If `distance` is negative or if `distance` is greater than `1000`, log a
//    warning and return undefined.
// 5. Loop `distance` times:
//    1. Set `xdhk` to the output of XDHK' applied to `xdhk`.
// 6. Return the tuple of `xdhk` and a context handle that if run, runs the
//    _XDHK Replace Steps_ with `xdhk`.
//
// The following steps are defined as the _XDHK Replace Steps_:
//
// 1. Let `xdhk` be the `XDHK` that has just been used (e.g. most likely because
//    an incoming message has been processed).
// 2. Apply XDHK' to `xdhk` and replace the current `XDHK` iteration of the
//    state with the result of that derivation.

syntax = "proto3";

package csp_e2e_fs;

option java_package = "ch.threema.protobuf.csp.e2e.fs";
option java_multiple_files = true;

import "common.proto";

// A forward security version.
//
// Note: The most significant byte is the major version and the least
// significant byte is the minor version.
//
// IMPORTANT: Don't remove any versions. Apps may serialize and deserialize this
// enum into the database.
enum Version {
  // The version is unspecified.
  UNSPECIFIED = 0;

  // V1.0
  //
  // - The initial FS release with some backwards compatible adjustments.
  // - Encapsulates only the following E2E messages:
  //   - [`text`](ref:e2e.text)
  //   - [`location`](ref:e2e.location)
  //   - [`file`](ref:e2e.file)
  //   - [`poll-setup`](ref:e2e.poll-setup)
  //   - [`poll-vote`](ref:e2e.poll-vote)
  V1_0 = 0x0100;

  // V1.1
  //
  // - Builds on V1.0 with backwards compatibility.
  // - If the remote side offered support for V1.1, all local 1:1 E2E messages
  //   will be encapsulated.
  V1_1 = 0x0101;

  // V1.2
  //
  // - Builds on V1.1 with backwards compatibility.
  // - If the remote side offered support for V1.2, all local group E2E messages
  //   will be encapsulated (i.e. all messages are now encapsulated).
  V1_2 = 0x0102;
};

// Forward security version range.
message VersionRange {
  // Minimum supported version.
  //
  // Note: This is the version that will be applied to 2DH message of the
  // sender.
  uint32 min = 1;

  // Maximum supported version.
  uint32 max = 2;
}

// A forward security envelope associated to an FS session, containing session
// signalling or an `Encapsulated` message.
//
// When receiving this message with FS unsupported by the client¹:
//
// 1. Let `sender` be the sender of this message.
// 2. If the `content` variant is not `Encapsulated`, discard the message and
//    abort these steps.
// 3. Run the steps to send a `Reject` with cause `DISABLED_BY_LOCAL`.
//
// ¹: The steps require basic support for FS (at least decoding the `Envelope`).
// Outdated apps may not provide that in which case case we rely on the feature
// mask correctly excluding FS.
//
// When receiving this message with FS supported by the client:
//
// 1. Let `sender` be the sender of this message.
// 2. Lookup a session with the sender with `envelope.session_id` and let
//    `session` be the result.
// 3. If the `content` variant is unknown, discard the message, log a warning
//    and abort these steps.
// 4. Invoke the steps associated to receiving the `content` variant. If
//    `content` is `Encapsulated`, return the inner message type as
//    `inner-type`, the decapsulated message as `inner-message` and the
//    `fs-commit-fn` function.
message Envelope {
  // Forward security session ID, 16 bytes
  bytes session_id = 1;

  oneof content {
    Init init = 2;
    Accept accept = 3;
    Reject reject = 4;
    Terminate terminate = 5;
    Encapsulated encapsulated = 6;
  }
}

// Initialises a new session.
//
// When receiving this variant:
//
// 1. Let `session` be the associated FS session or undefined.
// 2. If `session` is defined, log a warning that the `Init` has been repeated,
//    discard the message and abort these steps.
// 3. If the `supported_version` range does not include a supported version, log
//    a warning, discard the message and abort these steps.
// 4. Ensure that `fssk` contains a valid Curve25519 public key, otherwise log a
//    warning, discard the message and abort these steps.
// 5. [...]
// 6. Send an `Accept` and await acknowledgement of the `Accept` message.
// 7. _Acknowledge_ the message that contained this variant.
// 8. Commit the R24 session to storage.
message Init {
  // The version range supported by the initiator.
  //
  // If not provided, assume V1.0 (`0x0100`) for both `min` and `max`.
  VersionRange supported_version = 2;

  // Ephemeral X25519 public key.
  bytes fssk = 1;
}

// Accepts a session.
//
// When receiving this variant:
//
// 1. [...]
// 2. If the `supported_version` range does not include a supported version, log
//    a warning, discard the message and abort these steps.
// 3. Ensure that `fssk` contains a valid Curve25519 public key, otherwise log a
//    warning, discard the message and abort these steps.
// 4. [...]
// 5. _Acknowledge_ the message that contained this variant.
// 6. Commit the session transition to L44 to storage.
message Accept {
  // The version range supported by the responder.
  //
  // If not provided, assume V1.0 (`0x0100`) for both `min` and `max`.
  VersionRange supported_version = 2;

  // Ephemeral X25519 public key.
  bytes fssk = 1;
}

// Sent when receiving a `Encapsulated` message that cannot be decrypted (e.g.
// because the recipient has lost the session information).
//
// When creating this variant:
//
// 1. Let `encapsulated` be the `Encapsulated` message that triggers this
//    reject.
// 2. Set `message_id` to `encapsulated.message_id`.`
// 3. Set `group_identity` to `encapsulated.group_identity`.
//
// When sending this variant:
//
// 1. Let `session` be the associated session (if any).
// 2. Await acknowledgement of this message.
// 3. If `session` is defined, terminate `session` due to `cause` and remove
//    `session` from storage.
//
// When receiving this variant:
//
// 1. Let `session` be the associated session (if any).
// 2. If `session` is defined or `cause` is `DISABLED_BY_LOCAL`, schedule a task
//    to refresh the feature mask of the sender.
// 3. If `session` is defined, terminate `session` due to `cause` and remove
//    `session` from storage.
// 4. If `group_identity` has not been provided:
//    1. Lookup the message for `message_id` in the associated 1:1 conversation
//       and let `message` be the result.
//    2. If `message` is not defined or the user is not the sender of `message`,
//       abort these steps.
//    3. If the _when rejected_ property associated to `message` allows to
//       re-send after confirmation, mark `message` with _re-send requested_.
// 5. If `group_identity` has been provided:
//    1. Run the _Common Group Receive Steps_ and let `group` be the associated
//       group to `group_identity`. If the message has been discarded, abort
//       these steps.
//    2. Lookup the message for `message_id` in the `group` and let `message` be
//       the result.
//    3. If `message` is not defined:
//       1. If the user is the creator of the group, assume that a
//          `group-sync-request` has been received from the sender and run the
//          associated steps.
//       2. Abort these steps.
//    4. If the user is not the sender of (the original) `message`, abort these
//       steps.
//    5. If the _when rejected_ property associated to `message` allows to
//       re-send after confirmation, mark `message` with _re-send requested_ and
//       add `sender` to the list of group members requesting a re-send for
//       `message`.
//
// When the user signals that a message marked with _re-send requested_ should
// be re-sent, run the following steps¹:
//
// 1. Let `message` be the message to be re-sent.
// 2. If `message` is part of a 1:1 conversation:
//    1. Let `receivers` be a list of a single entry with the other party.
//    2. Remove the _re-send requested_ mark on `message`
// 3. If `message` is part of a group conversation:
//    1. Let `receivers` be a copy of the list of group members requesting a
//       re-send of `message`.
//    2. Request consent from the user to re-send the message to all `receivers`
//       and (asynchronously) wait for the response. If the request was denied,
//       abort these steps.
//    3. If `receivers` includes the list of group members requesting a re-send
//       of `message`², remove the _re-send requested_ mark on `message` (but
//       retain the list of group members requiring a re-send).
// 4. Schedule a persistent task that runs the following steps with `message`
//    and `receivers`:
//    1. If `message` has since been removed from the device, discard the task
//       and abort these steps.
//    2. Let `receivers` be the provided (snapshot of) receivers that requested
//       a re-sent.
//    3. Run the _Common Send Steps_ with `message` and `receivers` (and discard
//       the resulting receivers list).
//    4. Remove all `receivers` from the list of receivers requiring a re-send
//       for `message`.
//
// The following steps are defined as the _Rejected Messages Refresh Steps_ and
// will be run every time the group members are being updated:
//
// 1. Let `group` be the group conversation.
// 2. If `group` is marked as _left_:
//    1. For each `message` of `group` that has a _re-send requested_ mark,
//       remove the mark and the list of receivers requiring a re-send.
// 3. If `group` is not marked as _left_:
//    1. Let `members` be the current list of members for `group`.
//    2. For each `message` of `group` that has a _re-send requested_ mark:
//       1. Let `receivers` be the list of receivers requiring a re-send for
//          `message`.
//       2. Remove all entries from `receivers` that are not present in
//          `members`.
//       3. If `receivers` is now empty, remove the _re-send requested_ mark on
//          `message`.
//
// ¹: The steps ensure that multiple tasks scheduled to re-send the same message
// but with potential different receivers have no ill-effect. In edge cases,
// this may result in sending the same message more than once to a remote party
// which requested a re-send. This is deemed acceptable.
//
// ²: While requesting consent from the user, the list of group members
// requesting a re-send for the message may be asynchronously updated, making
// this check necessary so that a subsequent re-send for the remaining group
// members can be triggered.
message Reject {
  // Message ID of the `Encapsulated` message that could not be decrypted and
  // that should be sent again in a new session or without a session.
  fixed64 message_id = 1;

  // The group in which the message has been sent (if any).
  common.GroupIdentity group_identity = 3;

  // Cause for the reject.
  enum Cause {
    // General state mismatch. Explicitly includes the following cases:
    //
    // - The message could not be decrypted.
    // - The DH type of the message does not match the expected DH type.
    // - An unexpected (major) version has been used.
    STATE_MISMATCH = 0;

    // No session could be found matching the given Session ID.
    UNKNOWN_SESSION = 1;

    // The active local feature set does not include support for forward
    // security.
    DISABLED_BY_LOCAL = 2;
  }
  Cause cause = 2;
}

// Signals that the sender will not send any further `Encapsulated` messages in
// this session. The recipient should discard all key material related to this
// session.
//
// When sending this variant:
//
// 1. Let `session` be the associated session.
// 2. Await acknowledgement of this message.
// 3. Terminate `session` due to `cause` and remove `session` from storage.
// 4. If `cause` is `UNKNOWN_SESSION` or `RESET`, schedule initiating a new
//    session with the receiver.
//
// When receiving this variant:
//
// 1. Let `session` be the associated session (if any).
// 2. If `session` is defined or `cause` is `DISABLED_BY_LOCAL`, schedule a task
//    to refresh the feature mask of the sender.
// 3. If `session` is defined, terminate `session` due to `cause` and remove
//    `session` from storage.
message Terminate {
  // Cause for termination.
  enum Cause {
    // No session could be found matching the given Session ID.
    UNKNOWN_SESSION = 0;

    // The session is being reset
    RESET = 1;

    // The active local feature set does not include support for forward
    // security.
    DISABLED_BY_LOCAL = 2;

    // The remote feature set (fetched from the Directory) does not indicate
    // support for forward security.
    DISABLED_BY_REMOTE = 3;
  }
  Cause cause = 1;
}

// A forward security encapsulated CSP E2EE message or a control message.
//
// The following steps are defined as the _FS Refresh Steps_:
//
// 1. Let `contacts` be the provided list of contacts.
// 2. For each `contact` of `contacts`:
//    1. If the `contact` does not support FS, abort these sub-steps.
//    2. Lookup a session with `contact` and let `session` be the result.
//    3. If `session` is undefined, initiate a new `L20` session and set
//       `session` to the newly created session. Set `message` to the `Init`
//       message for `session` with type `0xa0`.
//    4. If `session` is not a newly created session, create an `Encapsulated`
//       message using `session` from inner type `0xfc` (_empty_) and set
//       `message` to the encrypted and encoded result with type `0xa0`.
//    5. Send `message` to `contact` and wait for acknowledgement.
//    6. Set `session`'s _updated_ mark to the current timestamp and commit the
//       `session` changes to storage.
//
// The following steps are defined as the _FS Encapsulation Steps_:
//
// 1. Let `inner-type` be any message type except `0xa0` and `inner-message` be
//    the message data and its nonce. Let `receiver` be the receiver of the
//    message.
// 2. If either sender or receiver do not support FS, return `inner-message` and
//    a stub `fs-commit-fn` function.
// 3. Let `outer-messages` be an empty list.
// 4. Lookup a session with `receiver` and let `session` be the result.
// 5. If `session` is undefined, initiate a new `L20` session and set `session`
//    to the newly created session. Append the `Init` message for `session` to
//    `outer-messages` with type `0xa0` and a random nonce.
// 6. If `inner-type` is not eligible to be encapsulated by `session`:
//    1. If `session` is not a newly created session and the `session`'s
//       _updated_ mark is older than 24h ago, create an `Encapsulated` message
//       using `session` from inner type `0xfc` (_empty_) and append the
//       encrypted and encoded result to `outer-messages` with type `0xa0` and a
//       random nonce.
//    2. Append `inner-message` as a non-encapsulated message to
//       `outer-messages` with type `inner-type` and the nonce of
//       `inner-message`.
// 7. If `inner-type` is eligible to be encapsulated by `session`, create an
//    `Encapsulated` message using `session` from `inner-type` and
//    `inner-message` and append the encrypted and encoded result to
//    `outer-messages` with type `0xa0` and the nonce of `inner-message`.
// 8. Return `outer-messages` and an `fs-commit-fn` function that sets the
//    `session`'s _updated_ mark to the current timestamp and commits the
//    `session` changes to storage when called.
//
// When receiving this variant:
//
// 1. Let `session` be the associated session (if any).
// 2. If `session` is not defined, `Reject` the message with `UNKNOWN_SESSION`
//    and abort these steps.
// 3. [...]
// 4. If `offered_version` is `0`, set it to V1.0.
// 5. If `applied_version` is `0`, set it to `offered_version`.
// 6. If `applied_version` is > `offered_version`, `Reject` the `session` and
//    the message with a `STATE_MISMATCH` and abort these steps.
// 7. If `session.state.dh_type` is `TWODH`:
//    1. If `session.state` is not `R20` and not `R24`, `Reject` the `session`
//       and the message with a `STATE_MISMATCH` and abort these steps.
//    2. If `session.state` is `R20`, let `expected-version` be
//       `session.state.version-range.min`.
//    3. If `session.state` is `R24`, let `expected-version` be
//       `session.state.remote-2dhv`.
//    4. If `offered_version` and `applied_version` are not equal to
//       `expected-version`, `Reject` the `session` and the message with a
//       `STATE_MISMATCH`, and abort these steps.
// 8. If `session.state.dh_type` is `FOURDH`:
//    1. If `session.state` is not any of `L44`, `R44` or `R24`, `Reject` the
//       `session` and the message with a `STATE_MISMATCH` and abort these
//       steps.¹
//    2. Let `pending-versions` be a set of local and remote versions to be
//       filled by the following steps.
//    3. If `offered_version` has a different major version, or is lower than
//       `session.state.local-4dhv`, `Reject` the `session` and the message with
//       a `STATE_MISMATCH`, and abort these steps.
//    4. If `offered_version` has a higher minor version than
//       `session.state.local-4dhv`, let `commonly-supported-version` be the
//       maximum commonly supported version with `offered_version`.
//    5. If `applied_version` has a different major version, or is lower than
//       `session.state.remote-4dhv`, or is not a supported version, `Reject`
//       the `session` and the message with a `STATE_MISMATCH` and abort these
//       steps.
//    6. If `applied_version` has a higher minor version than
//       `session.state.remote-4dhv`, set `pending-versions.remote-4dhv` to
//       `applied_version`.
//    7. If `commonly-supported-version` is defined and different to
//       `pending-versions.local-4dhv`, update it with that value, create an
//       `Encapsulated` message using `session` from inner type `0xfc`
//       (_empty_), send the encrypted and encoded result with type `0xa0` to
//       the sender and wait for acknowledgement of that message.¹
//     8. Update `session.state` with `pending-versions`.
// 9. Return an `fs-commit-fn` function that sets the `session`'s _updated_ mark
//    to the current timestamp and commits the `session` changes to storage when
//    called.
//
// ¹: This an exception to the silent ping rule which ensures that FS sessions
// are updated to use the highest available version ASAP.
message Encapsulated {
  // Encryption scheme applied to the encapsulated message.
  enum DHType {
    // The message is encrypted with the 2DH encryption scheme.
    TWODH = 0;
    // The message is encrypted with the 4DH encryption scheme.
    FOURDH = 1;
  }
  DHType dh_type = 1;

  // A monotonically increasing counter, starting at 1 for the first 2DH or 4DH
  // `Encapsulated` message sent in this session, and incrementing by 1 for each
  // successive `Encapsulated` message.
  //
  // - Counters for 2DH and 4DH are separate, as they are based on different
  //   root keys.
  // - Counters for each direction are separate.
  // - Can be used by the recipient as a hint of how many times to
  //   rotate/ratchet the KDF, in case an intermediate `Encapsulated` message
  //   went missing.
  uint64 counter = 2;

  // The major negotiated version with the _offered_ minor version.
  uint32 offered_version = 4;

  // The major negotiated version with the _applied_ minor version.
  uint32 applied_version = 5;

  // The group in which the message has been sent.
  //
  // Required if the inner message type's _kind_ property is _Group_. Must not
  // be set for _1:1_.
  common.GroupIdentity group_identity = 6;

  // A message as defined by `e2e.container` (but **without** PKCS#7 padding),
  // encrypted by:
  //
  // ```text
  // XSalsa20-Poly1305(
  //   key=XDHMK,
  //   nonce=00..00,
  //   data=<e2e.container>,
  // )
  // ```
  bytes encrypted_inner = 3;
}

[Dauer der Verarbeitung: 0.4 Sekunden, vorverarbeitet 2026-04-27]