Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/threema-protocols/src/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 75 kB image not shown  

Quelle  group-call.proto   Sprache: Unknown

 

Spracherkennung für: .proto vermutete Sprache: Abap {ABAP[102] C[100] Corba[100]} [Methode: maximale Elemente, drei Dimensionen]

// # Group Call Protocol
//
// Note that group calls are not necessarily bound to a Threema group. _Group_
// refers to a group of call participants and is a way to distinguish from 1:1
// Threema calls.
//
// There are two primary variants which use the same technology underneath:
//
// - A group call is scoped to a (Threema) group. It does not have any advanced
//   functionality such as moderation or external guests. Only one group call is
//   intended to run within a group.
// - A conference call is a more advanced type of group call and delivers more
//   advanced functionality such as moderation and join policies (e.g. requiring
//   moderator approval to join). It optionally also allows for external guests
//   to join.
//
// The theoretical maximum amount of participants is 790 (due to the way we
// derive WebRTC MIDs) but the practical limit is way below that.
//
// ## Terminology
//
// - `GCK`: Group Call Key, only used for key derivation
// - `GCKH`: Group Call Key Hash
// - `GCNHAK`: Group Call Normal Handshake Authentication Key
// - `GCHK`: Group Call Handshake Key
// - `GCSK`: Group Call State Key
// - `GCMK`: Group Call Moderation Key, only used for key derivation
// - `GCMMK`: Group Call Moderator Message Key
// - `PCK`: Participant Call Key
// - `PCMK`: Participant Call Media Key, only used for key derivation
// - `PCMK`': Ratchet iteration of PCMK
// - `PCMFK`: Participant Call Media Frame Key
// - `PCCK`: Participant Call Cookie
// - `PCSN`: Participant Call Sequence Number
// - `MFSN`: Media Frame Sequence Number
//
// ## General Information
//
// **Endianness**: All integers use little-endian encoding.
//
// **Encryption cipher**: XSalsa20-Poly1305, unless otherwise specified.
//
// **Nonce format**:
//
// - a 16 byte cookie (PCCK), followed by
// - a monotonically increasing sequence number (PCSN, 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 sender and one for the receiver). We will use `PCSN+` 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).
//
// Note: This format is equivalent to the CSP transport encryption.
//
// ## Key Derivation
//
// Note: All keys that are not derived from `GCK` directly will be derived using
// `GCKH` as input. This ensures that exchanged secret keys are useless if the
// Group Call ID has been exposed (unless `GCK` is also known to the attacker).
//
//     GCKH = BLAKE2b(key=GCK, salt='#', personal='3ma-call')
//
//     GCHK = BLAKE2b(key=GCK, salt='h', personal='3ma-call')
//     GCSK = BLAKE2b(key=GCK, salt='s', personal='3ma-call')
//
//     GCMMK = BLAKE2b(key=GCMK, salt='mm', personal='3ma-call', input=GCKH)
//
//     PCMK' = BLAKE2b(key=PCMK, salt="m'", personal='3ma-call')
//     PCMFK = BLAKE2b(key=PCMK, salt='mf', personal='3ma-call', input=GCKH)
//
// ## Group Call ID Derivation
//
// For group calls scoped to groups, the Group Call ID is derived by running
// BLAKE2b on specific data provided by the `GroupCallStart`:
//
//     group-call-id = BLAKE2b(
//       out-length=32,
//       salt='i',
//       personal='3ma-call',
//       input=
//            group-creator-identity
//         || group-id
//         || u8(GroupCallStart.protocol_version)
//         || GroupCallStart.gck
//         || utf8-encode(GroupCallStart.sfu_base_url),
//     )
//
// For conference calls, the Group Call ID is derived by running BLAKE2b on
// specific data provided by the `ConferenceCallInvite`:
//
//     group-call-id = BLAKE2b(
//       out-length=32,
//       salt='ic',
//       personal='3ma-call',
//       input=
//         || u8(ConferenceCallInvite.protocol_version)
//         || ConferenceCallInvite.gck
//         || ConferenceCallInvite.gcmmk_public
//         || u8(ConferenceCallInvite.guest_policy),
//         || u8(ConferenceCallInvite.join_policy),
//         || utf8-encode(ConferenceCallInvite.sfu_base_url)
//     )
//
// ## Protocol Flow
//
// ### Obtain SFU Information
//
// Before a call can be created, SFU information and an authentication token
// need to be obtained via the Directory Server API. The obtained information
// includes the following items referenced in subsequent sections:
//
// - _SFU Base URL_: Base URL used to create and distribute new calls.
// - _Allowed SFU Hostname Suffixes_: A set of allowed hostname suffixes to be
//   applied against the _SFU Base URL_ when joining calls.
// - _SFU Token_: An opaque token used to authenticate against the SFU.
//
// When receiving the SFU information, ensure the _SFU Base URL_ uses the scheme
// `https` and the included hostname ends with one of the _Allowed SFU Hostname
// Suffixes_.
//
// ### Call Peeking
//
// The following steps are defined as the _Group Call Peek Steps_:
//
// 1. Let `call` be the targeted group call.
// 2. Let `token` be the provided SFU token or be explicitly undefined (for
//    external guest usage).
// 3. If the user is currently participating in `call`, abort these steps and
//    return the current status.
// 4. _Peek_ the `call` via a `SfuHttpRequest.Peek` request with `token`. If
//    this does not result in a response within 15s, abort these steps and
//    return that the status is _unknown_.
// 5. If the server could not be reached or the received status code is not
//    `200` or if the `SfuHttpResponse.Peek` response could not be decoded:
//    1. If the received status code is `401`, abort these steps and return the
//       status _unauthorized_.
//    2. If the received status code is `404`, abort these steps and return the
//       status _not running_.
//    3. Abort these steps and return that the status is _failed_.
// 6. If the protocol version of the `call` is not supported, abort these steps
//    and return the status _unsupported_.
// 7. Return the status as provided by the `SfuHttpResponse.Peek` response.
//
// ### Scoped to Group
//
// #### Periodic Refresh
//
// The following steps are defined as the _Group Call Refresh Steps_ and will be
// applied to update the group calls that are currently considered running
// within a group, determining which one of them is the chosen call and
// potentially join the chosen call:
//
// 1.  Let `running` be the list of group calls that are currently considered
//     running within the group.
// 2.  Let `calls` be a copy of `running`.
// 3.  If the _SFU token_ would expire in the next 30s, refresh it and let
//     `token` be the result. If the _SFU Token_ refresh fails or does not yield
//     an _SFU Token_ within 15s, set `calls` to an empty list.
// 4.  For each `call` of `calls`, run the following steps (labelled
//     _peek-call_) concurrently and wait for them to return:
//     1. Run the _Group Call Peek Steps_ for `call` and `token` and let
//        `status` be the result.
//     2. If `status` contains a valid status, reset the `call`'s _failed_
//        counter to `0`, update `call` with `status` and abort the _peek-call_
//        sub-steps.
//     3. If `status` is _unauthorized_ or _not running_, remove `call` from
//        `calls` and from `running` and abort the _peek-call_ sub-steps.
//     4. If `status` is _unknown_, remove `call` from `calls` and abort the
//        _peek-call_ sub-steps.
//     5. If `status` is _unsupported_, reset the `call`'s _failed_ counter to
//        `0`, remove `call` from `calls`, log a warning that a group call with
//        an unsupported version is currently running and abort the _peek-call_
//        sub-steps.
//     6. If `status` is _failed_:
//        1. Remove `call` from `calls`.
//        2. If the `call`'s _failed_ counter is `>= 3` and the `call` was
//           received more than 10h ago, remove `call` from `running` and abort
//           the _peek-call_ sub-steps.
//        3. Increase the _failed_ counter for `call` by `1` and abort the
//           _peek-call_ sub-steps.
// 5.  If `running` is empty, cancel the timer to periodically re-run the _Group
//     Call Refresh Steps_ of this group. Otherwise, restart or schedule the
//     timer to re-run the _Group Call Refresh Steps_ of this group in 10s.
// 6.  Let `chosen-call` be any call of `calls` with the highest `started_at`
//     value (i.e. the most recently created call) as provided by the _peek_
//     result.
// 7.  If `chosen-call` is not defined, signal that no group call is currently
//     running within the group, abort these steps and return `chosen-call`.
// 8.  Signal `chosen-call` as the currently running group call within the
//     group.
// 9.  If the _Group Call Join Steps_ are currently running with a different (or
//     new) group call than `chosen-call`, cancel and restart the _Group Call
//     Join Steps_ asynchronously with the same `intent` but with the
//     `chosen-call`.
// 10. If the user is currently participating in a group call of this group that
//     is different to `chosen-call`, exit the running group call and run the
//     _Group Call Join Steps_ asynchronously with the `intent` to _only join_
//     `chosen-call`.
// 11. Return `chosen-call`.
//
// Note: The above steps have been carefully crafted to gracefully handle cases
// where the SFU of one call cannot be reached for a short period of time.
//
// When the user leaves a group call, run the _Group Call Refresh Steps_ for the
// respective group.
//
// The above described timer may be cancelled when the Threema app is inactive.
// The timer interval may be increased to 30s in case the group conversation is
// currently not visible to the user.
//
// #### Create or Join
//
// The following steps are to be run when a user wants to join a group call of a
// group where a group call is currently considered running (e.g. the user hits
// _join_ in the UI) or when the user intents to create a group call for a group
// where no group call is currently considered running (e.g. the user hits the
// _call_ button in the UI):
//
// 1. Let `intent` be the user's intent, i.e. to either _only join_ or _create
//    or join_ a group call.
// 2. Run the _Group Call Refresh Steps_ for the respective group and let `call`
//    be the result.
// 3. If `call` is undefined and `intent` is to _only join_, abort these steps
//    and notify the user that no group call is running / the group call is no
//    longer running.
// 4. If `call` is undefined, create (but don't send) a `GroupCallStart`
//    message, apply it to `call` and mark `call` as _new_.
// 5. Run the _Group Call Join Steps_ with the `intent` and `call`.
//
// The following steps are defined as the _Group Call Join Steps_ (also applied
// for creating a group call):
//
// 1.  Let `intent` be either _only join_ or _create or join_. Let `call` be the
//     given group call to be joined (or created).
// 2.   _Join_ (or implicitly create) the group call via a `SfuHttpRequest.Join`
//     request. If this does not result in a response within 10s, abort these
//     steps and notify the user.
// 3.  If the received status code is `503`, notify the user that the group call
//     is full and abort these steps.
// 4.  If the server could not be reached or the received status code is not
//     `200` or if the `SfuHttpResponse.Join` response could not be decoded,
//     abort these steps and notify the user.
// 5.  Let `response` be the decoded ``SfuHttpResponse.Join` response.
// 6.  If `response.participant_uid` is empty, abort these steps and notify the
//     user that the server does not support conference calls.
// 7.  Establish a WebRTC connection to the SFU with the information provided in
//     the `SfuHttpResponse.Join` response. Wait until the SFU sent the initial
//     `SfuToParticipant.Hello` message via the associated data channel. Let
//     `hello` be that message.
// 8.  If the `hello.participants` contains less than 4 items, set the initial
//     capture state of the microphone to _on_.
// 9.  If `call` is marked as _new_:
//     1. Optionally add an artificial wait period of 2s minus the time elapsed
//        since step 1.¹
//     2. Let `message-id` be a random message ID.
//     3. Schedule a persistent task to run he _Bundled Messages Send Steps_
//        with the following properties:
//        - `id` set to `message-id`,
//        - `receivers` set to all group members that have `GROUP_CALL_SUPPORT`,
//        - to construct a `GroupCallStart` message from `call`.
//     4. Add the created `call` to the list of group calls that are currently
//        considered running.
//     5. Asynchronously run the _Group Call Refresh Steps_.²
// 10. The group call is now considered established and should asynchronously
//     invoke the SFU to Participant and Participant to Participant flows.
//
// ¹: This prevents butter-fingered user from accidentally starting a group
// call.
//
// ²: This will initiate the refresh timer for a newly created call and signal
// it to the UI.
//
// Note: Implementations need to ensure that only one group call can be active
// at the same time in the application. This means that only one invocation of
// the _Create or Join_ flow and only one invocation of the _Group Call Join
// Steps_ can be active. Be aware that these steps can be cancelled by the user
// and by the _Group Call Refresh Steps_.
//
// ### Conference Call
//
// #### Overview: Periodic Refresh
//
// While the Conference Call Overview screen UI is visible, schedule a timer to
// run the following steps every 10s (after the previous run returned):
//
// 1. Let `calls` be a copy of the set of stored conference calls.
// 2. If the _SFU token_ would expire in the next 30s, refresh it and let
//    `token` be the result. If the _SFU Token_ refresh fails or does not yield
//    an _SFU Token_ within 15s, update the status of all `calls` to _unknown_.
// 3. For each `call` of `calls`, run the following steps (labelled _peek-call_)
//    concurrently and wait for them to return:
//    1. Run the _Group Call Peek Steps_ for `call` and `token` and let `status`
//       be the result.
//    2. If `status` contains a valid status, set `call`'s display status to
//       `status` and abort the _peek-call_ sub-steps.
//    3. If `status` is _not running_, set `call`'s display status respectively
//       and abort the _peek-call_ sub-steps.
//    4. If `status` is _unsupported_, log a warning that a conference call with
//       an unsupported version is currently running.
//    5. Set `call`'s display status to unknown.
//
// #### Join Pre-Screen: Periodic Refresh
//
// While the Join Pre-Screen UI is visible as a Threema user, schedule a timer
// to run the following steps every 10s (after the previous run returned):
//
// 1. Let `call` be the targeted conference call.
// 2. If the _SFU token_ would expire in the next 30s, refresh it and let
//    `token` be the result. If the _SFU Token_ refresh fails or does not yield
//    an _SFU Token_ within 15s, abort these steps and return that the status is
//    _unknown_.
// 3. Run the _Group Call Peek Steps_ for `call` and `token` and let `status` be
//    the result.
// 4. If `status` contains a valid status, display the current `status` and
//    abort these steps.
// 5. If `status` is _not running_, display that the conference call is not
//    currently running (but may be started by the user) and abort these steps.
// 6. Display that the current status is unknown.
//
// While the Join Pre-Screen UI is visible as an external guest, schedule a
// timer to run the following steps every 10s (after the previous run returned):
//
// 1. Let `call` be the targeted conference call.
// 2. Run the _Group Call Peek Steps_ for `call` without a token and let
//    `status` be the result.
// 3. If `status` contains a valid status, display the current `status` and
//    abort these steps.
// 4. If `status` is _not running_ or _unauthorized_, display that the
//    conference call is not currently running and abort these steps.
// 5. Display that the current status is unknown.
//
// #### Create, Start or Join
//
// When a Threema users wants to create a conference invite:
//
// 1. If the _SFU token_ is expired, refresh it and let `token` be the result.
//    If the _SFU Token_ refresh fails or does not yield an _SFU Token_ within
//    15s, abort these steps and notify the user.
// 2. Create a `ConferenceCallInvite` message with `token` and encode it as a
//    URL as defined by that message.
//
// The following steps are to be run when a Threema user wants to start or join
// a conference call:
//
// 1. If the _SFU token_ would expire in the next 15s, refresh it and let
//    `token` be the result. If the _SFU Token_ refresh fails or does not yield
//    an _SFU Token_ within 15s, abort these steps and notify the user.
// 2.  _Join_ (or implicitly start) the conference call via a
//    `SfuHttpRequest.Join` request using `token`. If this does not result in a
//    response within 10s, abort these steps and notify the user.
// 3. If the received status code is `503`, notify the user that the conference
//    call is full and abort these steps.
// 4. If the server could not be reached or the received status code is not
//    `200` or if the _Join_ response could not be decoded, abort these steps
//    and notify the user.
// 5. Establish a WebRTC connection to the SFU with the information provided in
//    the _Join_ response. Wait until the SFU sent the initial
//    `SfuToParticipant.Hello` message via the associated data channel. Let
//    `hello` be that message.
// 6. The conference call is now considered established and should
//    asynchronously invoke the SFU to Participant and Participant to
//    Participant flows.
//
// The following steps are to be run when an external guest wants to join a
// conference call:
//
// 1.  _Join_ the conference call via a `SfuHttpRequest.Join` request without
//    any SFU token. If this does not result in a response within 10s, abort
//    these steps and notify the user.
// 2. If the received status code is `401`, notify the user that the conference
//    call is currently not running and abort these steps.
// 3. If the received status code is `503`, notify the user that the conference
//    call is full and abort these steps.
// 4. If the server could not be reached or the received status code is not
//    `200` or if the _Join_ response could not be decoded, abort these steps
//    and notify the user.
// 5. Establish a WebRTC connection to the SFU with the information provided in
//    the _Join_ response. Wait until the SFU sent the initial
//    `SfuToParticipant.Hello` message via the associated data channel. Let
//    `hello` be that message.
// 6. The conference call is now considered established and should
//    asynchronously invoke the SFU to Participant and Participant to
//    Participant flows.
//
// ### SFU to Participant Flow
//
// Upon successful joining via `SfuHttpRequest.Join`, the SFU waits for the
// client to establish a WebRTC connection and then announces all participants
// to the newly joined participant in its `SfuToParticipant.Hello` message.
//
// When another participant joins or leaves, a `ParticipantJoined` or
// `ParticipantLeft` message will be sent.
//
// At any time, participants may subscribe and unsubscribe receiving microphone,
// camera and screen data from other participants.
//
// If the user is alone in a call for more than 3 minute, the call should be
// left to save resources. The SFU will automatically drop such calls after 5
// minutes but this results in non-ideal UX.
//
// ### Participant to Participant Flow
//
// Unlike the other flows, this one is more complicated and needs to be done
// separately for each other participant. During the handshake, ephemeral
// encryption keys will be established.
//
// Note that multiple participants with the same Threema ID in the same call are
// **explicitly allowed**. Not only can this happen in case the connection has
// been lost (e.g. the client already reconnected but the SFU has not detected
// connection loss yet), but it is also a feature for multi-device capable
// clients.
//
// #### Handshake
//
// When a new participant (NP) joins, it must authenticate each other existing
// participant (EP) and establish an ephemeral shared secret (`PCK`). The
// message variant used depends on the participant's role. Normal participants
// send `Hello` and `Auth` while guest participants send `GuestHello` and
// `GuestAuth`, exemplary:
//
// If both are normal participants:
//
//     NP ------ Hello -----> EP
//     NP <----- Hello ------ EP
//     NP <----- Auth ------- EP
//     NP ------ Auth ------> EP
//
// If NP is a normal participant and EP is a guest participant:
//
//     NP ------ Hello -----> EP
//     NP <--- GuestHello --- EP
//     NP <--- GuestAuth ---- EP
//     NP ------ Auth ------> EP
//
// For group calls scoped to groups:
//
// - Only handshake messages from Threema IDs that are part of the group are
//   allowed.
// - External guests are not allowed and therefore the guest handshake is not
//   allowed.
//
// For conference calls, external guests are allowed if the _guest policy_ is
// `GuestPolicy.ALLOW_GUESTS`.
//
// #### Extended Handshake
//
// In case the call is a conference call and the _join policy_ is
// `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`, the handshake period is extended
// such that both participants need to provide `Approval`, exemplary:
//
// If both NP and EP are moderators:
//
//     (Handshake until both sent `Auth`/`GuestAuth`)
//     NP ---- Approval ----> EP
//     NP <--- Approval ----- EP
//
// If EP is a moderator and EP approves NP (by user interaction):
//
//     (Handshake until both sent `Auth`/`GuestAuth`)
//     NP <--- Approval ----- EP
//     NP <- GrantApproval -- EP
//     NP ---- Approval ----> EP
//
// #### Post-Handshake
//
// After the handshake, **both** sides run the following steps:
//
// 1. Subscribe to the other participant's microphone feed (i.e. send a
//    `ParticipantMicrophone` message to the SFU).
// 2. If the user is a moderator, send a `Moderator.ReportAsModerator` message
//    to the other participant.
//
// #### Join/Leave of Other Participants
//
// The following steps are defined as the _Participant Joined Steps_ and are run
// by existing participants once a new participant joined¹:
//
// 1. Set the _handshake state_ of this participant to `await-np-hello`.
// 2. Run the _Reschedule State Task Steps_.
//
// The following steps are defined as the _Participant Active Steps_ and are run
// by existing participants once a new participant's _handshake state_ moved
// into `done`:
//
// 1. Let `pcmk` be the currently _applied_ PCMK with the associated context.
// 2. If the amount of ratchet rounds for `pcmk` is `255`, abort the call with
//    an error condition and abort these steps.
// 3. Advance the ratchet of `pcmk` once (i.e. replace the key by deriving
//    PCMK') and apply for media encryption immediately. Note: Do **not** reset
//    the MFSN!
// 4. Send a _rekey_ message to the participant containing the updated `pcmk`
// 5. If the currently _pending_ PCMK of the associated context is defined, send
//    a _rekey_ message to the participant containing the _pending_ PCMK.
// 6. Send a `CaptureState` message for each device whose capturing state is not
//    _off_.
// 7. Add the participant to the call as an _active_ participant.
//
// The following steps are defined as the _Participant Left Steps_ and are run
// by existing participants when a participant leaves²:
//
// 1. Let `pending-pcmk` be the currently _pending_ PCMK of the associated
//    context.
// 2. If `pending-pcmk` exists, additionally mark `pending-pcmk` as _stale_ and
//    abort these steps.
// 3. Let `current-pcmk` be the currently _applied_ PCMK with the associated
//    context.
// 4. Set `pending-pcmk` in the following way:
//    1. Generate a new cryptographically secure random PCMK and assign it to
//       `pending-pcmk`.
//    2. Set `pending-pcmk.epoch` to `current-pcmk.epoch + 1`, wrap back to `0`
//       if it would be `256`.
//    3. Set `pending-pcmk.ratchet_counter` to `0`.
//    4. Do **not** reset the MFSN! Continue the existing MFSN counter of the
//       previous PCMK.
// 5. Send `pending-pcmk` to all participants with a _handshake state_ of `done`
//    via a _rekey_ message.
// 6. Schedule a volatile task bound to the call to run the following steps
//    after 2s:
//    1. Apply `pending-pcmk` for media encryption. This means that
//       `pending-pcmk` now replaces the _applied_ PCMK and is no longer
//       _pending_.
//    2. If `pending-pcmk` is marked as _stale_, run the parent steps from the
//       beginning.
// 7. Run the _Reschedule State Task Steps_.
//
// The following steps are defined as the _Participant Rekey Steps_ and are run
// when a participant receives a `MediaKey` message from another participant:
//
// 1. Let `current-pcmk` be the PCMK and its associated context used for the
//    participant.
// 2. Let `new-pcmk` be the media keys (PCMK) of the received message.
// 3. Store `new-pcmk` as a successor to `current-pcmk` (and any other successor
//    already stored on `current-pcmk`) and follow the description of the media
//    frame on when to apply it.
//
// ¹: The announcement of the new participant is guaranteed to be sent prior to
// any handshake messages of the new participant.
//
// ²: Re-keying after a participant leaving is therefore throttled but always
// catches up to the current participant state with a maximum delay of 4s.
//
// #### State Update
//
// One of the participants is deterministically designated to update the
// peekable call state every 10s and additionally every time a participant joins
// or leaves. If the call state has not been updated/refreshed for 30s, the SFU
// will delete it.
//
// The following steps are defined as the _Reschedule State Task Steps_:
//
// 1. Cancel any running timer to update the call state.
// 2. Let `candidates` be a list of all other participants (in any _handshake
//    _state).
// 3. Compare the user's participant ID against all `candidates`. If any of the
//    participant IDs in `candidates` is lower than the participant ID of the
//    user, abort these steps.
// 4. Send a `ParticipantToSfu.UpdateCallState` message to the SFU and schedule
//    a repetitive timer to repeat this step every 10s.
//
// Note: The above algorithm is prone to races since the handshake is
// asynchronous for each participant pair. However, this should not be an issue
// as they'd essentially post the same status (eventually).

syntax = "proto3";

package group_call;

option java_package = "ch.threema.protobuf.group_call";
option java_multiple_files = true;
option php_namespace = "Threema\\Protocols\\GroupCall";

import "common.proto";

// Current call state as announced by the designated client.
//
// Note: The `CallState` accurateness must not be relied upon as it can be out
// of date and can be replayed by the SFU.
//
// When creating this message:
//
// 1. Set `state_created_by` to the user's participant ID.
// 2. Set `state_created_at` to the current UNIX-ish timestamp in milliseconds.
// 3. Add the user to the `participants` map:
//    1. Set the `ParticipationState` to `PARTICIPATING`.
//    2. If the call is a conference call and the _join policy_ is set to
//       `JoinPolicy.REQUIRES_MODERATOR_APPROVAL` and the user does not have a
//       local _approval_ signature from a moderator, set the
//       `ParticipationState` to `AWAITING_MODERATOR_APPROVAL`.
// 4. For each other participant:
//    1. If the _handshake state_ of the participant is `post-auth` add it to
//       the `participants` map with `ParticipationState.PARTICIPATING` and
//       abort these sub-steps.
//    2. If the _handshake state_ of the participant is `await-approval`:
//       1. If the _remote approval received_ flag is present, add it to the
//          `participants` map with `ParticipationState.PARTICIPATING` and abort
//          these sub-steps.
//       2. Add it to the `participants` map with
//          `ParticipationState.AWAITING_MODERATOR_APPROVAL` and abort these
//          sub-steps.
message CallState {
  // Random amount of padding, ignored by the receiver.
  bytes padding = 1;

  // Participant ID of the designated client that created this message.
  uint32 state_created_by = 2;

  // UNIX-ish timestamp in milliseconds the designated client created this
  // message.
  uint64 state_created_at = 3;

  // Information for a single participant.
  message Participant {
    reserved 1; // Redundant participant ID

    // A participation state.
    //
    // Note: Participants that are not yet authenticated will not appear in the
    // `CallState` at all.
    enum ParticipationState {
      // Participating (i.e. active).
      PARTICIPATING = 0;

      // Waiting for approval by a moderator to participate (i.e. authenticated
      // but not active, yet).
      AWAITING_MODERATOR_APPROVAL = 1;
    }
    ParticipationState participation_state = 4;

    // A _normal_ participant, i.e. a Threema client.
    message Normal {
      // Threema ID of the sender.
      string identity = 1;

      // Nickname associated to the Threema ID (without `~` prefix).
      string nickname = 2;
    }

    // A _guest_ participant.
    message Guest {
      // The guest's self-assigned name.
      string name = 1;
    }

    // Type-specific information.
    oneof participant {
      Normal threema = 2;
      Guest guest = 3;
    }
  }

  // Information for each participant of the group call that is authenticated.
  map<uint32, Participant> participants = 4;
}

// Supported feature (to be used as a bitmask).
enum SupportedFeature {
  // Base feature support (always present).
  BASE = 0x0000;

  // Support for screen sharing.
  SCREEN_SHARE = 0x0001;
}

// Request payloads sent to the SFU as part of an HTTP request.
message SfuHttpRequest {
  // Peeks for the current state of the group call for the given Group Call ID.
  //
  // IMPORTANT: The _peek_ process is considered stable across different
  // protocol versions. Therefore, the message **should** maintain backwards
  // compatibility!
  //
  // The URL is formed in the following way:
  //
  //     <sfu_base_url>/v1/peek/<call_id-as-hex>
  //
  // When sending this request:
  //
  // 1. Use `POST` as method.
  // 2. If an SFU token has been provided, set the `Authorization` header to
  //    `ThreemaSfuToken <sfu-token>`.
  // 3. Set the encoded `SfuHttpRequest.Peek` message as body.
  //
  // When receiving this request:
  //
  // 1. If `call_id-as-hex` is not a valid Call ID, respond with status code
  //    `400` and abort these steps.
  // 2. If the `Authorization` header is present and the provided `sfu-token` in
  //    the `Authorization` header is invalid or expired, respond with status
  //    code `401` and abort these steps.
  // 3. If the `Authorization` header is not present and no call for the given
  //    decoded `call_id-as-hex` is currently running, respond with status code
  //    `401` and abort these steps.
  // 4. If the provided data is invalid, respond with status code `400` and
  //    abort these steps.
  // 5. If `call_id` does not equal the decoded `call_id-as-hex`, respond with
  //    status code `400` and abort these steps.
  // 6. If no group call for the given `call_id` is currently running, respond
  //    with status code `404` and abort these steps.
  // 7. Respond with status code `200` and an encoded `SfuHttpResponse.Peek`
  //    message as body.
  message Peek {
    // Group Call ID associated to the group call.
    bytes call_id = 1;
  }

  // Requests to join the group call with the given Group Call ID.
  //
  // The URL is formed in the following way:
  //
  //     <sfu_base_url>/v1/join/<call_id-as-hex>
  //
  // When sending this request:
  //
  // 1. Use `POST` as method.
  // 2. If an SFU token has been provided, set the `Authorization` header to
  //    `ThreemaSfuToken <sfu-token>`.
  // 3. Set the encoded `SfuHttpRequest.Join` message as body.
  //
  // When receiving this request:
  //
  // 1. If `call_id-as-hex` is not a valid Call ID, respond with status code
  //    `400` and abort these steps.
  // 2. If the `Authorization` header is present and the provided `sfu-token` in
  //    the `Authorization` header is invalid or expired, respond with status
  //    code `401` and abort these steps.
  // 3. If the `Authorization` header is not present and no call for the given
  //    decoded `call_id-as-hex` is currently running, respond with status code
  //    `401` and abort these steps.
  // 4. If the provided data is invalid, respond with status code `400` and
  //    abort these steps.
  // 5. If `call_id` does not equal the decoded `call_id-as-hex`, respond with
  //    status code `400` and abort these steps.
  // 6. If the `protocol_version` is unsupported by the SFU, respond with status
  //    code `419` and abort these steps.
  // 7. If no more participants can join the group call for the given `call_id`,
  //    respond with status code `503` and abort these steps.
  // 8. Respond with status code `200` and an encoded `SfuHttpResponse.Join`
  //    message as body.
  // 9. Once the WebRTC connection has been established, announce the newly
  //    joined participant to all other participants via the corresponding data
  //    channel. If no WebRTC connection is being established within 30s, the
  //    participant ID is no longer reserved for the client and the group call
  //    must be teared down if no other participant started joining this group
  //    call.
  message Join {
    // Group Call ID associated to the group call.
    bytes call_id = 1;

    // Protocol version the call was announced with.
    uint32 protocol_version = 2;

    // DTLS fingerprint of the x509 certificate that will be used by the client.
    //
    // Note: This is the authentication anchor for the WebRTC connection towards
    // the SFU.
    bytes dtls_fingerprint = 3;
  }
}

// Response payloads sent back from the SFU as part of an HTTP request.
message SfuHttpResponse {
  // Information returned for a running group call.
  //
  // IMPORTANT: The _peek_ process is considered stable across different
  // protocol versions. Therefore, the message **should** maintain backwards
  // compatibility!
  //
  // Note: The included `CallState` information may not be accurate and should
  // not be relied upon.
  message Peek {
    // Unix-ish timestamp in milliseconds for when the first participant joined
    // the Group Call ID and therefore started the group call.
    uint64 started_at = 1;

    // Maximum amount of participants allowed in the group call.
    uint32 max_participants = 2;

    // Call state (`CallState`), encrypted by `GCSK.secret` and prefixed with a
    // random nonce.
    //
    // Not provided in case the call is currently running but no participant has
    // sent a call state to the SFU, or if the call state expired.
    //
    // The content of the call state is protocol version dependent and should
    // therefore be ignored if a client does not support the particular protocol
    // version the group call is associated with.
    optional bytes encrypted_call_state = 3;
  }

  // Information returned when joining a group call.
  //
  // When receiving this response, initiate the WebRTC connection to the SFU and
  // consider the connection established when the `SfuToParticipant.Hello`
  // message has been received on the associated data channel.
  message Join {
    // Unix-ish timestamp in milliseconds for when the first participant joined
    // the Group Call ID and therefore started the group call.
    uint64 started_at = 1;

    // Maximum amount of participants allowed in the group call.
    uint32 max_participants = 2;

    // Participant ID assigned to the client.
    //
    // Note: The client needs to know the participant ID early to derive MIDs
    // required to be present in the O/A SDP.
    uint32 participant_id = 3;

    // Participant UID, 16 byte unique random id, randomly generated by the SFU
    // and assigned to the client.
    //
    // Since SFU version v1.4.0. Not present (i.e. empty) otherwise.
    //
    // Note: Unused for group calls scope to groups, required for conference
    // calls.
    bytes participant_uid = 10;

    // Address the SFU is listening for a WebRTC connection.
    message Address {
      // Protocol.
      enum Protocol { UDP = 0; }
      Protocol protocol = 1;

      // Port.
      uint32 port = 2;

      // IPv4 or IPv6 address.
      string ip = 3;
    }

    // List of addresses the SFU listens for a WebRTC connection.
    //
    // Note: One UDP IPv4 address is mandatory! One IPv6 address is recommended.
    repeated Address addresses = 4;

    // ICE username fragment for the WebRTC connection.
    string ice_username_fragment = 5;

    // ICE password for the WebRTC connection.
    string ice_password = 6;

    // DTLS fingerprint of the x509 certificate that will be used by the SFU.
    //
    // Note: This is the authentication anchor for the WebRTC connection towards
    // the SFU.
    bytes dtls_fingerprint = 7;

    // RTP header extension ID mapping.
    //
    // Since SFU version v1.3.0: Not present otherwise. Fall back to the
    // previous hard-coded RTP header extension ID parameter mapping if not
    // provided.
    //
    // Note: The same ID is applied for header extensions used for both
    // microphone and camera/screen.
    message RtpHeaderExtensionIds {
      // urn:ietf:params:rtp-hdrext:sdes:mid
      uint32 mid = 1;

      // urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
      uint32 rtp_stream_id = 2;

      // urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
      uint32 repaired_rtp_stream_id = 3;

      // http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
      uint32 absolute_send_time = 4;

      // http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
      uint32 transport_wide_congestion_control_01 = 5;

      // urn:3gpp:video-orientation
      uint32 video_orientation = 6;

      // urn:ietf:params:rtp-hdrext:toffset
      uint32 time_offset = 7;
    }
    RtpHeaderExtensionIds rtp_header_extension_ids = 9;

    // Supported features bitmask (see `SupportedFeature`).
    uint64 supported_features = 8;
  }
}

// Messages sent from the SFU to a participant via a data channel.
//
// Data Channel Parameters:
//
// - `ordered`: `true`
// - `negotiated`: `true`
// - `id`: `0`
message SfuToParticipant {
  // The enveloped message from the SFU.
  //
  // When relaying a message from one participant to another, omit any
  // additional padding.
  //
  // IMPORTANT: The format of the `SfuToParticipant.Envelope` and
  // `ParticipantToSfu.Envelope` must be compatible for the relay case, so the
  // SFU can forward the data without having to re-encode.
  //
  // When receiving this message, handle the message according to the content.
  // If an unknown content variant has been received, log a warning and discard
  // the message.
  message Envelope {
    // Random amount of padding, ignored by the receiver.
    bytes padding = 1;

    oneof content {
      ParticipantToParticipant.OuterEnvelope relay = 2;
      Hello hello = 3;
      Timestamp timestamp_response = 6;
      ParticipantJoined participant_joined = 4;
      ParticipantLeft participant_left = 5;
    }
  }

  // Announces all other participants to a newly joined participant.
  //
  // When receiving this message:
  //
  // 1. If a `Hello` was received before (i.e. if the receiver is not a newly
  //    joined participant), log a warning and abort these steps.
  // 2. If the `SfuHttpResponse.Join.participant_uid` was present while joining,
  //    let `participants` be the `participants` map field.
  // 3. If the `SfuHttpResponse.Join.participant_uid` was not present while
  //    joining:
  //    1. If the group call is a conference call, abort the call with an error
  //       condition and abort these steps.
  //    2. Let `participants` be provided by the `deprecated_participant_ids`
  //       field.
  // 4. Initiate the participant to participant handshake for all
  //    `participants`.
  // 5. Run the _Reschedule State Task Steps_.
  message Hello {
    // All participants in the group call. This **excludes** the client's
    // participant ID.
    //
    // DEPRECATED: Superseded by `participants` map.
    repeated uint32 deprecated_participant_ids = 1 [deprecated = true];

    // Additional participant information of other participants.
    message Participant {
      // Participant UID, 16 byte unique random id.
      bytes participant_uid = 1;
    }

    // A map of all participant IDs to additional `Participant` information in
    // the group call. This **excludes** the client.
    //
    // Since SFU version v1.4.0. Not present (i.e. empty) otherwise. Required
    // for conference calls. Fall back to the `deprecated_participant_ids` if
    // not present for group calls scoped to groups.
    map<uint32, Participant> participants = 4;
  }

  // Announces that a new participant joined to existing participants.
  //
  // When receiving this message:
  //
  // 1. Look up the participant. If it already exists (i.e. never _left_), log a
  //    warning and abort these steps.
  // 2. If the group call is a conference call and `participant_uid` is empty,
  //    abort the call with an error condition and abort these steps.
  // 3. Run the _Participant Joined Steps_.
  message ParticipantJoined {
    // Participant ID assigned to the newly joined participant.
    uint32 participant_id = 1;

    // Participant UID assigned to the newly joined participant, 16 byte unique
    // random id.
    //
    // Since SFU version v1.4.0. Not present (i.e. empty) otherwise.
    bytes participant_uid = 2;
  }

  // Announces that a participant left to existing participants.
  //
  // When receiving this message:
  //
  // 1. Look up the participant. If it was never announced to have _joined_ by
  //    an associated `ParticipantJoined` message, log a warning and abort these
  //    steps.
  // 2. Run the _Participant Left Steps_.
  message ParticipantLeft {
    // Participant ID referring to the participant that left.
    uint32 participant_id = 1;
  }

  // Current UNIX-ish timestamp in milliseconds of the SFU.
  //
  // When receiving this message:
  //
  // 1. Resolve the first pending timestamp request with the response.
  message Timestamp { uint64 ms = 1; }
}

// Messages sent from a participant to the SFU via a data channel.
//
// Data Channel Parameters:
//
// - `ordered`: `true`
// - `negotiated`: `true`
// - `id`: `0`
message ParticipantToSfu {
  // The enveloped message towards the SFU.
  //
  // When relaying a message from one participant to another, omit any
  // additional padding.
  //
  // IMPORTANT: The format of the `SfuToParticipant.Envelope` and
  // `ParticipantToSfu.Envelope` must be compatible for the relay case, so the
  // SFU can forward the data without having to re-encode.
  //
  // When receiving this message, handle the message according to the content.
  // If an unknown content variant has been received, log a warning and discard
  // the message.
  message Envelope {
    // Random amount of padding, ignored by the receiver.
    bytes padding = 1;

    oneof content {
      ParticipantToParticipant.OuterEnvelope relay = 2;
      UpdateCallState update_call_state = 3;
      RequestTimestamp request_timestamp = 7;
      ParticipantMicrophone request_participant_microphone = 6;
      ParticipantCamera request_participant_camera = 4;
      ParticipantScreen request_participant_screen = 5;
    }
  }

  // Request the current timestamp of the SFU.
  //
  // When receiving this message:
  //
  // 1. Respond with a `TimestampResponse` message.
  message RequestTimestamp {}

  // Update the call state that can be retrieved via a _peek_.
  //
  // Note: Only the currently designated client should send this to the SFU.
  //
  // When receiving this message:
  //
  // 1. Store the encrypted call state and make it accessible via _peek_ HTTP
  //    requests.
  // 2. Start a timer to purge the call state after 30s. Subsequent
  //    `UpdateCallState` messages will update the call state and reset the
  //    timer.
  message UpdateCallState {
    // Call state (`CallState`), encrypted by `GCSK` and prefixed with
    // a random nonce.
    bytes encrypted_call_state = 1;
  }

  // Subscribe or unsubscribe to a participant's microphone feed.
  //
  // When receiving this message:
  //
  // 1. If the `participant_id` refers to the sender's participant ID or an
  //    unknown participant ID, discard the message and abort these steps.
  // 2. If `subscribe` is set, forward the microphone feed to the client that
  //    fits best to the provided parameters.
  // 3. If `unsubscribe` is set, stop forwarding microphone feed of this
  //    participant to the client.
  message ParticipantMicrophone {
    // Participant ID whose microphone feed should be subscribed or unsubscribed
    // from.
    uint32 participant_id = 1;

    // Subscribe to a participant's microphone feed.
    message Subscribe {}

    // Unsubscribe a participant's microphone feed.
    message Unsubscribe {}

    oneof action {
      Subscribe subscribe = 2;
      Unsubscribe unsubscribe = 3;
    }
  }

  // Subscribe or unsubscribe to a participant's camera feed.
  //
  // When receiving this message:
  //
  // 1. If the `participant_id` refers to the sender's participant ID or an
  //    unknown participant ID, discard the message and abort these steps.
  // 2. If `subscribe` is set, forward the camera feed to the client that fits
  //    best to the provided parameters.
  // 3. If `unsubscribe` is set, stop forwarding camera feed of this participant
  //    to the client.
  message ParticipantCamera {
    // Participant ID whose camera feed should be subscribed or unsubscribed
    // from.
    uint32 participant_id = 1;

    // Subscribe to a participant's camera feed.
    message Subscribe {
      // Desired resolution. The client should use the canvas' resolution the
      // camera feed be displayed in. The SFU will select the spatial layer that
      // fits best.
      common.Resolution desired_resolution = 1;

      // Desired frame rate. The SFU will select the temporal layer that fits
      // best.
      uint32 desired_fps = 2;
    }

    // Unsubscribe a participant's camera feed.
    message Unsubscribe {}

    oneof action {
      Subscribe subscribe = 2;
      Unsubscribe unsubscribe = 3;
    }
  }

  // Subscribe or unsubscribe to a participant's screen feed.
  //
  // Availability: If the SFU announced support for
  // `SupportedFeature.SCREEN_SHARE`.
  //
  // When receiving this message:
  //
  // 1. If the `participant_id` refers to the sender's participant ID or an
  //    unknown participant ID, discard the message and abort these steps.
  // 2. If `subscribe` is set, forward the screen feed to the client that fits
  //    best to the provided parameters.
  // 3. If `unsubscribe` is set, stop forwarding screen feed of this participant
  //    to the client.
  message ParticipantScreen {
    // Participant ID whose screen feed should be subscribed or unsubscribed
    // from.
    uint32 participant_id = 1;

    // Subscribe to a participant's screen feed.
    message Subscribe {
      // Desired resolution. The client should use the canvas' resolution the
      // screen feed be displayed in. The SFU will select the spatial layer that
      // fits best.
      common.Resolution desired_resolution = 1;

      // Desired frame rate. The SFU will select the temporal layer that fits
      // best.
      uint32 desired_fps = 2;
    }

    // Unsubscribe a participant's screen feed.
    message Unsubscribe {}

    oneof action {
      Subscribe subscribe = 2;
      Unsubscribe unsubscribe = 3;
    }
  }
}

// Messages sent from one participant to another.
//
// Note that these are relayed via `SfuToParticipant.Envelope` and
// `ParticipantToSfu.Envelope` in order to prevent races with
// `ParticipantJoined`/`ParticipantLeft`.
message ParticipantToParticipant {
  // Signature context, used as a prefix when signing to separate different use cases.
  enum SignatureContext {
    RESERVED = 0x00;
    JOIN_APPROVAL = 0x01;
    POST_AUTH_MESSAGE = 0x02;
  }

  // Used for all messages that are relayed from one participant to another via
  // the SFU.
  //
  // When receiving a relayed message:
  //
  // 1. If the `receiver` is not the user's assigned participant id, discard the
  //    message and abort these steps.
  // 2. If the `sender` is unknown, discard the message and abort these steps.
  // 3. Decrypt `encrypted_data` according to the current _handshake state_ and
  //    handle the inner envelope:
  //    - `await-ep-hello` or `await-np-hello`: Expect a
  //      `Handshake.HelloEnvelope`.
  //    - `await-auth`: Expect a `Handshake.AuthEnvelope`.
  //    - `await-approval`: Expect a `Handshake.ApprovalEnvelope`.
  //    - `done`: Expect a post-auth `Envelope`.
  message OuterEnvelope {
    // Participant ID of the sender. Checked by the SFU to be correct, dropped
    // if not.
    uint32 sender = 1;

    // Participant ID of the receiver. Checked by the SFU to exist, dropped if
    // not.
    uint32 receiver = 2;

    // The inner envelope. Always encrypted. Key and nonce are to be inferred
    // from the current _handshake state_ towards the sending participant.
    bytes encrypted_data = 4;
  }

  // Messages required for the initial lock-step handshake between participants.
  message Handshake {
    // The first message (`HelloEnvelope(Hello)` or `HelloEnvelope(GuestHello)`)
    // of both sides is always encrypted by `GCHK`, prefixed with a
    // random nonce.
    //
    // When receiving this message, handle the message according to the content.
    // If an unknown content variant has been received, log a warning, discard
    // the message and remove the participant by running the _Participant Left
    // Steps_.
    message HelloEnvelope {
      // Random amount of padding, ignored by the receiver
      bytes padding = 1;

      oneof content {
        Hello hello = 2;
        GuestHello guest_hello = 3;
      }
    }

    // The second message is encrypted in the following way:
    //
    // 1. If both user and the other participant are Threema users, let
    //    `auth-key` be the result of the following operation:
    //
    // ```text
    // Blake2b(
    //   key=X25519HSalsa20(<user.CK>.secret, <participant.CK>.public),
    //   salt='nha', personal='3ma-call', input=GCKH,
    // )
    // ```
    // 2. If the user is a Threema user and the other participant is a guest
    //    participant, let `auth-key` be the result of the following operation:
    //
    // ```text
    // Blake2b(
    //   key=X25519HSalsa20(<user.CK>.secret, <participant.PCK>.public),
    //   salt='gha', personal='3ma-call', input=GCKH,
    // )
    // ```
    // 3. If the user is a guest participant and the other participant is a
    //    Threema user, let `auth-key` be the result of the following operation:
    //
    // ```text
    // Blake2b(
    //   key=X25519HSalsa20(<user.PCK>.secret, <participant.CK>.public),
    //   salt='gha', personal='3ma-call', input=GCKH,
    // )
    // ```
    // 4. If both user and the other participant are guest participants, let
    //    `auth-key` be undefined.
    // 5. If `auth-key` is defined:
    //    1. If the sender is a Threema user, let `inner-data` be an
    //       `AuthEnvelope` with an `Auth` message. Otherwise, let `inner-data`
    //       be an `AuthEnvelope` with a `GuestAuth` message.
    //    2. Let `inner-nonce` be a random nonce.
    //    3. Let `inner-data` be encrypted by:
    //
    //    ```text
    //    XSalsa20-Poly1305(
    //      key=auth-key,
    //      nonce=inner-nonce,
    //      data=<AuthEnvelope(Auth)>,
    //    )
    //    ```
    //    3. Let `outer-data` be encrypted by:
    //
    //    ```text
    //    XSalsa20-Poly1305(
    //      key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
    //      nonce=<sender.PCCK> || <sender.PCSN+>,
    //      data=inner-nonce || inner-data,
    //    )
    //    ```
    //    4. Return `outer-data`.
    // 6. If `auth-key` is undefined, return an `AuthEnvelope` with a
    //    `GuestAuth` message encrypted in the following way:
    //
    // ```text
    // XSalsa20-Poly1305(
    //   key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
    //   nonce=<sender.PCCK> || <sender.PCSN+>,
    //   data=<AuthEnvelope(GuestAuth)>,
    // )
    // ```
    //
    // When receiving this message, handle the message according to the content.
    // If an unknown content variant has been received, log a warning, discard
    // the message and remove the participant by running the _Participant Left
    // Steps_.
    message AuthEnvelope {
      // Random amount of padding, ignored by the receiver
      bytes padding = 1;

      oneof content {
        Auth auth = 2;
        GuestAuth guest_auth = 3;
      }
    }

    // Initial handshake message.
    //
    // When creating this message as a newly joined participant towards another
    // participant:
    //
    // 1. Set the participant's _handshake state_ to `await-ep-hello`.
    // 2. Send this message.
    //
    // When receiving this message:
    //
    // 1. If the group call is scoped to a (Threema) group:
    //    1. Lookup `identity` in the associated group (including the user
    //       itself) and let `contact` be the result.
    //    2. If `contact` is undefined or marked as _invalid_, log a warning,
    //       discard the message, remove the participant by running the
    //       _Participant Left Steps_ and abort these steps.
    // 2. If the group call is a conference call:
    //    1. Run the _Valid Contacts Lookup Steps_ with `identities` set to
    //       `identity` and let `contact` be the result.
    //    2. If `contact` is marked as _invalid_, log a warning, discard the
    //       message, remove the participant by running the _Participant Left
    //       Steps_ and abort these steps.
    // 3. If the sender is a newly joined participant and therefore the
    //    _handshake state_ was set to `await-np-hello` (as described by the
    //    _Join/Leave_ section):
    //    1. If the user is a Threema user, respond by sending a `Hello`
    //       message, immediately followed by an `Auth` message.
    //    2. If the user is a guest participant, respond by sending a
    //       `GuestHello` message, immediately followed by a `GuestAuth`
    //       message.
    //    3. Set the participant's _handshake state_ to `await-auth` and abort
    //       these steps.
    // 4. If the participant's _handshake state_ is `await-ep-hello`:
    //    1. If the `pck` reflects the local PCK.public or the `pcck` reflects
    //       the local PCCK, log a warning, discard the message, remove the
    //       participant by running the _Participant Left Steps_ and abort these
    //       steps.
    //    2. If the user is a Threema user, respond by sending an `Auth`
    //       message.
    //    3. If the user is a guest participant, respond by sending a
    //       `GuestAuth` message.
    //    4. Set the participant's _handshake state_ to `await-auth` and abort
    //       these steps.
    // 5. Log a warning, discard the message, remove the participant by running
    //    the _Participant Left Steps_ and abort these steps.
    message Hello {
      // Threema ID of the sender.
      string identity = 1;

      // Nickname associated to the Threema ID (without `~` prefix).
      string nickname = 2;

      // 32 byte ephemeral public key (`PCK.public`) towards the remote
      // participant.
      //
      // Note: It is allowed to use the same `PCK` for multiple participants.
      bytes pck = 3;

      // 16 byte random cookie used for nonces by the sender in subsequent
      // messages.
      bytes pcck = 4;
    }

    // Second handshake message.
    //
    // When receiving this message:
    //
    // 1. If the participant's _handshake state_ is not `await-auth`, log a
    //    warning, discard the message, remove the participant by running the
    //    _Participant Left Steps_ and abort these steps.
    // 2. If the other participant initiated a guest handshake via a
    //    `GuestHello`, log a warning, discard the message, remove the
    //    participant by running the _Participant Left Steps_ and abort these
    //    steps.
    // 3. If the repeated `pck` does not equal the local `PCK.public` used
    //    towards this participant, log a warning, discard the message, remove
    //    the participant by running the _Participant Left Steps_ and abort
    //    these steps.
    // 4. If the repeated `pcck` does not equal the local `PCCK` used towards
    //    this participant, log a warning, discard the message, remove the
    //    participant by running the _Participant Left Steps_ and abort these
    //    steps.
    // 5. Run the _Participant Rekey Steps_ for each media key of
    //    `deprecated_early_media_keys`.
    // 6. If the call is not a conference call or the _join policy_ is not set
    //    to `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`:
    //    1. Set the participant's handshake state to `done`.
    //    2. Run the _Participant Active Steps_.
    // 7. If the call is a conference call and the _join policy_ is set to
    //    `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`:
    //    1. Set the participant's _handshake state_ to `await-approval`.
    //    2. If the user is a moderator or has a local _approval_ signature from
    //       a moderator, respond by sending an `Approval` message and update
    //       the _handshake state_ of `await-approval` with the _local approval
    //       sent_ flag.
    // 8. (Unreachable)
    message Auth {
      // 32 byte repeated ephemeral public key from the `Hello` message.
      //
      // Note: Repeating the sender's `PCK.public` prevents replay attacks.
      bytes pck = 1;

      // 32 byte repeated random cookie from the `Hello` message.
      //
      // Note: Repeating the sender's `PCCK` prevents replay attacks while
      // allowing the sender to use the same `PCK` for multiple participants.
      bytes pcck = 2;

      // The currently applied PCMK and any _pending_ PCMK used for media
      // encryption, specifically in that order.
      //
      // DEPRECATED: The idea behind sending PCMKs early was that participants
      // should become visible in the UI once the `Auth`/`GuestAuth` is being
      // sent, even though they are not mutually authenticated yet. This would
      // make it apparent to the user that the other participant is now able to
      // hear and see everything said and done in the call. However, it turns
      // out that in practice it is quite complicated to have a pre-full-auth
      // state that affects the UI, making this a footgun. For the sake of
      // implementation simplicity and to avoid any other weird kind of
      // pre-full-auth attacks, newer implementations MUST sent an empty list
      // and follow up by sending a `Rekey` message after mutual authentication
      // (and join approval).
      repeated MediaKey deprecated_early_media_keys = 3 [deprecated = true];
    }

    // Initial guest handshake message.
    //
    // When creating this message as a newly joined guest participant towards
    // another participant:
    //
    // 1. Set the participant's _handshake state_ to `await-ep-hello`.
    // 2. Send this message.
    //
    // When receiving this message:
    //
    // 1. If guest participants are not allowed for this call, log a warning,
    //    discard the message, remove the participant by running the
    //    _Participant Left Steps_ and abort these steps.
    // 3. If the sender is a newly joined participant and therefore the
    //    _handshake state_ was set to `await-np-hello` (as described by the
    //    _Join/Leave_ section):
    //    1. If the user is a Threema user, respond by sending a `Hello`
    //       message, immediately followed by an `Auth` message.
    //    2. If the user is a guest participant, respond by sending a
    //       `GuestHello` message, immediately followed by a `GuestAuth`
    //       message.
    //    3. Set the participant's _handshake state_ to `await-guest-auth` and
    //       abort these steps.
    // 4. If the participant's _handshake state_ is `await-ep-hello`:
    //    1. If the `pck` reflects the local PCK.public or the `pcck` reflects
    //       the local PCCK, log a warning, discard the message, remove the
    //       participant by running the _Participant Left Steps_ and abort these
    //       steps.
    //    2. If the user is a Threema user, respond by sending an `Auth`
    //       message.
    //    3. If the user is a guest participant, respond by sending a
    //       `GuestAuth` message.
    //    4. Set the participant's _handshake state_ to `await-guest-auth` and
    //       abort these steps.
    // 5. Log a warning, discard the message, remove the participant by running
    //    the _Participant Left Steps_ and abort these steps.
    message GuestHello {
      // The guest's self-assigned name.
      string name = 1;

      // 32 byte ephemeral public key (`PCK.public`) towards the remote
      // participant.
      //
      // Note: It is allowed to use the same `PCK` for multiple participants.
      bytes pck = 2;

      // 16 byte random cookie used for nonces by the sender in subsequent
      // messages.
      bytes pcck = 3;
    }

    // Second handshake message.
    //
    // When receiving this message:
    //
    // 1. If the participant's _handshake state_ is not `await-guest-auth`, log
    //    a warning, discard the message, remove the participant by running the
    //    _Participant Left Steps_ and abort these steps.
    // 2. If the other participant initiated the (normal) handshake via a
    //    `Hello`, log a warning, discard the message, remove the participant by
    //    running the _Participant Left Steps_ and abort these steps.
    // 3. If the repeated `pck` does not equal the local `PCK.public` used
    //    towards this participant, log a warning, discard the message, remove
    //    the participant by running the _Participant Left Steps_ and abort
    //    these steps.
    // 4. If the repeated `pcck` does not equal the local `PCCK` used towards
    //    this participant, log a warning, discard the message, remove the
    //    participant by running the _Participant Left Steps_ and abort these
    //    steps.
    // 5. If the call is not a conference call or the _join policy_ is not set
    //    to `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`:
    //    1. Set the participant's handshake state to `done`.
    //    2. Run the _Participant Active Steps_.
    // 6. If the call is a conference call and the _join policy_ is set to
    //    `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`:
    //    1. Set the participant's _handshake state_ to `await-approval`.
    //    2. If the user is a moderator or has a local _approval_ signature from
    //       a moderator, respond by sending an `Approval` message and update
    //       the _handshake state_ of `await-approval` with the _local approval
    //       sent_ flag.
    // 7. (Unreachable)
    message GuestAuth {
      // 32 byte repeated ephemeral public key from the `GuestHello` message.
      //
      // Note: Repeating the sender's `PCK.public` prevents replay attacks.
      bytes pck = 1;

      // 32 byte repeated random cookie from the `GuestHello` message.
      //
      // Note: Repeating the sender's `PCCK` prevents replay attacks while
      //       allowing the sender to use the same `PCK` for multiple
      //       participants.
      bytes pcck = 2;
    }

    // Final handshake phase for conference calls with _join policy_
    // `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`, encrypted by:
    //
    // ```text
    // XSalsa20-Poly1305(
    //   key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
    //   nonce=<sender.PCCK> || <sender.PCSN+>,
    // )
    // ```
    //
    // When receiving this message:
    //
    // 1. If the participant's _handshake state_ is not `await-approval`, log a
    //    warning, discard the message, remove the participant by running the
    //    _Participant Left Steps_ and abort these steps.
    // 2. Handle the message according to the content. If an unknown content
    //    variant has been received, log a warning, discard the message and
    //    remove the participant by running the _Participant Left Steps_.
    message ApprovalEnvelope {
      // Random amount of padding, ignored by the receiver
      bytes padding = 1;

      oneof content {
        GrantApproval grant_approval = 2;
        Approval approval = 3;
      }
    }

    // Grants approval to join a conference call with _join policy_
    // `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`.
    //
    // Note: While only a moderator should be able to grant approval, this
    // message can technically be sent by any participant.
    //
    // When the user intends to grant another participant an approval to join:
    //
    // 1. If the participant's _handshake state_ is not `await-approval`, log a
    //    warning and abort these steps.
    // 2. If the user is not a moderator, log a warning and abort these steps.
    // 3. Create and send an approval signature.
    //
    // When receiving this message:
    //
    // 1. Verify the `signature`. If it is invalid, log a warning, discard the
    //    message and abort these steps.
    // 2. If the user already has a local _approval_ signature from another
    //    moderator, discard the message and abort these steps.
    // 3. Store the `signature` as the local _approval_.
    // 4. For each other participant with a _handshake state_ of
    //    `await-approval` (including the sender of this message)¹:
    //    1. Send an `Approval` message and update the _handshake state_ of
    //       `await_approval` with the _local approval sent_ flag.
    //    2. If the _handshake state_ of `await-approval` also has the _remote
    //       approval received_ flag, set the _handshake state_ for this
    //       participant to `done` and run the _Participant Active
    //       Steps_.
    //
    // ¹: Due to step 2 and the steps defined for `Auth` and `GuestAuth`, it
    // must be assumed that the other participant's _handshake state_ of
    // `await-approval` does not have the _local approval sent_ flag.
    message GrantApproval {
      // Signature over the participant's UID:
      //
      // ```text
      // Ed25519(
      //   key=GCMMK,
      //   message=
      //        u8(SignatureContext.JOIN_APPROVAL)
      //     || <receiver.participant-uid>
      //   )
      // ```
      //
      // IMPORTANT: The `ParticipantToParticipant.ApprovalEnvelope` that
      // encapsulates this message shall be encrypted by the same `PCSN` as
      // used to sign this message.
      bytes signature = 1;
    }

    // Present approval to join a conference call with _join policy_
    // `JoinPolicy.REQUIRES_MODERATOR_APPROVAL`.
    //
    // When receiving this message:
    //
    // 1. Verify the `signature`. If it is invalid, log a warning, discard the
    //    message, remove the participant by running the _Participant Left
    //    Steps_ and abort these steps.
    // 2. Update the _handshake state_ of `await-approval` with the _remote
    //    approval received_ flag.
    // 3. If the _handshake state_ of `await-approval` also has the _local
    //    approval sent_ flag, set the _handshake state_ for this participant to
    //    `done` and run the _Participant Active Steps_.
    message Approval {
      // Signature over the participant's UID:
      //
      // ```text
      // Ed25519(
      //   key=GCMMK,
      //   message=
      //        u8(SignatureContext.JOIN_APPROVAL)
      //     || <receiver.participant-uid>
      //   )
      // ```
      //
      // IMPORTANT: The `ParticipantToParticipant.ApprovalEnvelope` that
      // encapsulates this message shall be encrypted by the same `PCSN` as
      // used to sign this message.
      bytes signature = 1;
    }
  }

  // After fulfilling either the (normal) handshake or the guest handshake, all
  // following messages are encoded in `Envelope` and encrypted by:
  //
  // ```text
  // XSalsa20-Poly1305(
  //   key=X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public),
  //   nonce=<sender.PCCK> || <sender.PCSN+>,
  // )
  // ```
  //
  // Note: Since guest to guest handshakes are TOFU, an attacker knowing `GCK`
  // having control over the SFU may apply a MITM attack between a guest
  // participant and another guest participant. The attacker would be able to
  // silently eavesdrop all media traffic between the two participants. This is
  // repeatable for all other participants and means the attacker is able to
  // silently eavesdrop the whole call. Therefore, if a call is not open for
  // guests, guest handshakes **must not** be accepted.
  //
  // When receiving this message:
  //
  // 1. If the participant's _handshake state_ is not `done`, log a warning,
  //    discard the message, remove the participant by running the _Participant
  //    Left Steps_ and abort these steps.
  // 2. Handle the message according to the content. If an unknown content
  //    variant has been received, log a warning and discard the message.
  message Envelope {
    // Random amount of padding, ignored by the receiver
    bytes padding = 1;

    oneof content {
      // Announces new media keys a participant will apply soon.
      MediaKey rekey = 3;

      // Announces capture state changes of a participant.
      CaptureState capture_state = 4;

      // A moderator message.
      Moderator.Envelope moderator_envelope = 2;
    }
  }

  // Messages from moderators towards participants (including other moderators).
  message Moderator {
    // A message from a moderator.
    //
    // When receiving this message:
    //
    // 1. Verify the `signature`. If it does not match, log a warning, discard
    //    the message and abort these steps.
    // 2. Handle the message according to the content. If an unknown content
    //    variant has been received, log a warning and discard the message.
    message Envelope {
      // An `Content` message. After encoding, it is signed by:
      //
      // ```text
      // Ed25519(
      //   key=GCMMK,
      //   message=
      //        u8(SignatureContext.POST_AUTH_MESSAGE)
      //     || X25519HSalsa20(<sender.PCK>.secret, <receiver.PCK>.public)
      //     || <sender.PCCK>
      //     || <sender.PCSN+>
      //     || <message>
      //   )
      // ```
      //
      // IMPORTANT: The `ParticipantToParticipant.Envelope` that encapsulates this message shall be
      // encrypted by the same `PCSN` as used to sign this message.
      message Content {
        oneof content {
          ReportAsModerator report_as_moderator = 1;
          PromoteToModerator promote_to_moderator = 2;
          ForceLeave force_leave = 3;
          ForceCaptureStateOff force_capture_state_off = 4;
        }
      }
      bytes content = 1;

      // Signature over the encoded `Content` message.
      bytes signature = 2;
    }

    // Report as a moderator.
    //
    // When receiving this message, mark the sender as a moderator in the UI.
    message ReportAsModerator {}

    // Promote the receiver to a moderator.
    //
    // Note: This is final for the duration of the Group Call. A moderator
    // cannot be demoted.
    //
    // When receiving this message:
    //
    // 1. If the user already is a moderator, abort these steps.
    // 2. Derive GCMMK and calculate the associated public key from the received
    //    `gcmk`. If it does not match the known `GCMMK.public`, log a warning
    //    and abort these steps.
    // 3. Send a `Moderator.ReportAsModerator` message to all other participants
    //    (including the sender who promoted the user to a moderator).
    // 4. Notify the user of its moderator status and enable moderation
    //    functionality in the UI.
    message PromoteToModerator { bytes gcmk = 1; }

    // Force the receiver to leave the call.
    //
    // When receiving this message, immediately leave the call.
    message ForceLeave {}

    // Force the receiver's capture device to be turned off.
    //
    // Note: This is a momentary enforcement. A participant may immediately
    //       restart capturing a device (e.g. unmute itself) and the message is
    //       not repeated towards newly joined participants.
    //
    // When receiving this message:
    //
    // 1. Look up the corresponding device. If none could be found, abort these
    //    steps.
    // 2. If the device's capture state is already _off_, abort these steps.
    // 3. Send a `CaptureState` message for the device and follow the creation
    //    steps of that message (i.e. stop capturing, etc.).
    message ForceCaptureStateOff {
      enum Device {
        // Stop capturing all devices
        ALL = 0;
        // Stop capturing the microphone (i.e. mute)
        MICROPHONE = 1;
        // Stop capturing the camera
        CAMERA = 2;
        // Stop capturing the screen
        SCREEN = 3;
      }
      Device device = 1;
    }
  }

  // Media keys a participant will use for sending.
  //
  // When receiving this message, run the _Participant Rekey Steps_.
  message MediaKey {
    // The current epoch reflecting the PCMK state.
    //
    // Initially, epoch is `0` and increases each time a participant leaves. The
    // concrete mechanism is explained in the _Join/Leave_ section.
    uint32 epoch = 1;

    // The current ratchet counter reflecting the PCMK state.
    //
    // Initially (or when a participant leaves), the ratchet counter is `0` and
    // increases each time a participant joins. The ratcheting mechanism is
    // explained in the _Join/Leave_ section.
    uint32 ratchet_counter = 2;

    // The current state of the PCMK with the applied ratchet counter.
    //
    // Initially (or when a participant leaves), PCMK is a random 32 byte secret
    // key. The concrete mechanism is explained in the _Join/Leave_ section.
    //
    // This key must be identical **towards** all participants.
    bytes pcmk = 3;
  }

  // Signals a participant's device capturing state.
  //
  // When creating this message:
  //
  // 1. Let `device` be the device whose state is to be updated.
  // 2. If `device` is to be turned _off_:
  //    1. Stop capturing from the device.
  //    2. Pause the corresponding media track.
  // 3. If `device` is to be turned _on_:
  //    1. Start capturing from the device.
  //    2. Resume the corresponding media track.
  // 4. If `device` is of type _screen_:
  //    1. Send a `RequestTimestamp` to the SFU.
  //    2. Let `context` be the corresponding `Timestamp` response.
  // 5. Send the `CaptureState` message for the `device` with the provided
  //    `context`.
  //
  // When receiving this message:
  //
  // 1. Let `device` be the device of the sender whose state has been updated.
  // 2. If `device` was turned _off_ and the user is subscribed to the given
  //    `device`'s feed:
  //    1. Stop displaying the corresponding media feed in the UI.
  //    2. Pause the corresponding media track.
  //    3. If `device` is `Microphone`, no further action is necessary.
  //    4. If `device` is `Camera`, send a `ParticipantCamera.Unsubscribe`
  //       message to the SFU.
  //    5. If `device` is `Screen`, send a `ParticipantScreen.Unsubscribe`
  //       message to the SFU.
  // 3. If `device` was turned _on_ and the user is not subscribed to the given
  //    `device`'s feed:
  //    1. Resume the corresponding media track.
  //    2. Start displaying the corresponding media feed in the UI.
  //    3. If `device` is `Microphone`, no further action is necessary.
  //    4. If `device` is `Camera`, send a `ParticipantCamera.Subscribe` message
  //       to the SFU.
  //    5. If `device` is `Screen`:
  //       1. Send a `ParticipantScreen.Subscribe` message to the SFU.
  //       2. If `device`'s `started_at` timestamp is the most recent timestamp
  //          from all currently active screen shares of all participants, set
  //          the focus to this participant's screen.
  message CaptureState {
    // Capture state of the microphone.
    message Microphone {
      oneof state {
        common.Unit on = 1;
        common.Unit off = 2;
      }
    }

    // Capture state of the camera.
    message Camera {
      oneof state {
        common.Unit on = 1;
        common.Unit off = 2;
      }
    }

    // Capture state of the screen.
    message Screen {
      message On {
        // UNIX-ish timestamp in milliseconds retrieved from the SFU for when
        // the screen share was initiated.
        uint64 started_at = 1;
      }

      oneof state {
        On on = 1;
        common.Unit off = 2;
      }
    }

    oneof state {
      Microphone microphone = 1;
      Camera camera = 2;
      Screen screen = 3;
    }
  }
}

¤ Dauer der Verarbeitung: 0.20 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.