/* * Copyright 2025 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ syntax = "proto3"; option java_multiple_files = true; package org.signal.chat.messages; import "google/protobuf/empty.proto"; import "org/signal/chat/common.proto"; import "org/signal/chat/require.proto"; import "org/signal/chat/errors.proto"; import "org/signal/chat/tag.proto"; import "signalpb/SignalService.proto"; // Provides methods for sending "unsealed sender" messages. service Messages { option (require.auth) = AUTH_ONLY_AUTHENTICATED; // Sends an "unsealed sender" message to all devices linked to a single // destination account. // // The destination account must not be the same as the authenticated caller. // Callers should use `SendSyncMessage` to send messages to themselves. rpc SendMessage(SendAuthenticatedSenderMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {} // Sends a "sync" message to all other devices linked to the authenticated // sender's account. rpc SendSyncMessage(SendSyncMessageRequest) returns (SendMessageAuthenticatedSenderResponse) {} // Retrieve messages for the authenticated device. When the caller receives // and successfully processes a message returned in a GetMessagesResponse they // must send a corresponding GetMessagesRequest indicating that the message // has been processed (an ack). Acks should only be sent for messages // delivered via the currently open RPC. // // Only the first GetMessagesRequest may contain request parameters that // configure the stream. The first request must not contain an ack. // // The server will keep the stream open until the client disconnects. Only // one GetMessages stream may be open per device. If a second GetMessages // stream is opened, the server may terminate any of the streams with // a STREAM_CLOSED error reason. A GetMessagesStreamClosed message will be // present in the error details. rpc GetMessages(stream GetMessagesRequest) returns (stream GetMessagesResponse) {} } message GetMessagesRequest { message GetMessageOptions { // If present and true, the server will not deliver any messages with the // story flag set. This flag may only be set on the first GetMessagesRequest // sent from the client to the server in an RPC stream. bool drop_stories = 1; } oneof request { // Configuration options for the message stream. Required for the first // request of the stream, forbidden on subsequent reqeusts. GetMessageOptions options = 1; // The server_guid of an envelope previously returned in a // GetMessagesResponse that has been successfully processed by the caller. // Forbidden on the first request of the stream, required on subsequent // requests. bytes server_guid_ack = 2; } } // The reason why a GetMessages RPC is being closed by the server. message GetMessagesStreamClosed { oneof reason { // Another caller has opened a GetMessages stream for the same device. google.protobuf.Empty conflicting_stream = 1; } } message GetMessagesResponse { oneof response { // A message. On successful receipt of an envelope the caller should ack // the envelope by sending a GetMessagesRequest with the envelope's server // guid. Acks should only be sent for envelopes received on the currently // open RPC. signalservice.Envelope envelope = 1; // An indicator that all outstanding messages for the device have been // drained and acked by the client. The stream will remain open and continue // to deliver newly arrived messages. google.protobuf.Empty queue_empty = 2; } } // Provides methods for sending "sealed sender" messages. service MessagesAnonymous { option (require.auth) = AUTH_ONLY_ANONYMOUS; // Sends a "sealed sender" message to all devices linked to a single // destination account. // // If this RPC is authorized with an unidentified access key, it will fail // with an authorization failure if the credential is invalid OR if the // destination account was not found. If it is authorized using a group send // token, it will fail with an authorization failure if the credential is // invalid and with an destination not found error if the account does not // exist rpc SendSingleRecipientMessage(SendSealedSenderMessageRequest) returns (SendMessageResponse) {} // Sends a "sealed sender" message with a common payload to all devices linked // to multiple destination accounts. rpc SendMultiRecipientMessage(SendMultiRecipientMessageRequest) returns (SendMultiRecipientMessageResponse) {} // Sends a story message to devices linked to a single destination account. rpc SendStory(SendStoryMessageRequest) returns (SendMessageResponse) {} // Sends a story message with a common payload to devices linked to devices // linked to multiple destination accounts. rpc SendMultiRecipientStory(SendMultiRecipientStoryRequest) returns (SendMultiRecipientMessageResponse) {} } message IndividualRecipientMessageBundle { // A message for an individual device linked to a destination account. message Message { // The registration ID for the destination device. uint32 registration_id = 1 [(require.range).max = 0x3fff]; // The content of the message to deliver to the destination device. bytes payload = 2 [(require.size) = {min: 1, max: 262144}]; // 256 KiB // The message type of the message. If this message is part of an // unidentified send, this must be UNIDENTIFIED_SENDER SendMessageType type = 3; } // The time, in milliseconds since the epoch, at which this message was // originally sent from the perspective of the sender. Note that the maximum // allowable timestamp for JavaScript clients is less than Long.MAX_VALUE; see // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date // for additional details and discussion. uint64 timestamp = 1 [(require.range).min = 1, (require.range).max = 8640000000000000]; // A map of device IDs to individual messages. Generally, callers must include // one message for each device linked to the destination account. In cases of // "sync messages" where a sender is distributing information to other devices // linked to the sender's account, senders may omit a message for the sending // device. map messages = 2 [(require.nonEmpty) = true]; } enum SendMessageType { UNSPECIFIED = 0; // A double-ratchet message represents a "normal," "unsealed-sender" message // encrypted using the Double Ratchet within an established Signal session. DOUBLE_RATCHET = 1; // A prekey message begins a new Signal session. The `content` of a prekey // message is a superset of a double-ratchet message's `content` and // contains the sender's identity public key and information identifying the // pre-keys used in the message's ciphertext. PREKEY_MESSAGE = 2; // A plaintext message is used solely to convey encryption error receipts // and never contains encrypted message content. Encryption error receipts // must be delivered in plaintext because encryption/decryption of a prior // message failed and there is no reason to believe that // encryption/decryption of subsequent messages with the same key material // would succeed. // // Critically, plaintext messages never have "real" message content // generated by users. Plaintext messages include sender information. PLAINTEXT_CONTENT = 3; // An unidentified sender message is an encrypted message. No other // information about the type of the encrypted message is known to the server. // // Unidenitfied sender messages require an unidentified access token or a // group send endorsement token to prove the unidentified sender is authorized // to send messages to the destination. UNIDENTIFIED_SENDER = 4; } message SendAuthenticatedSenderMessageRequest { // The service identifier of the account to which to deliver the message. common.ServiceIdentifier destination = 1; // If true, this message will only be delivered to destination devices that // have an active message delivery channel with a Signal server. bool ephemeral = 2; // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server bool urgent = 3; // The messages to send to the destination account. IndividualRecipientMessageBundle messages = 4; } message SendMessageAuthenticatedSenderResponse { // The outcome of the message delivery oneof response { // The message was successfully delivered to all destination devices google.protobuf.Empty success = 1; // A list of discrepancies between the destination devices identified in a // request to send a message and the devices that are actually linked to an // account. MismatchedDevices mismatched_devices = 2 [(tag.reason) = "mismatched_devices"]; // A description of a challenge callers must complete before sending // additional messages. ChallengeRequired challenge_required = 3 [(tag.reason) = "challenge_required"]; // The destination account did not exist errors.NotFound destination_not_found = 4 [(tag.reason) = "destination_not_found"]; } } message SendSyncMessageRequest { // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server bool urgent = 1; // The messages to send to the destination account. IndividualRecipientMessageBundle messages = 2; } message SendSealedSenderMessageRequest { // The service identifier of the account to which to deliver the message. common.ServiceIdentifier destination = 1; // If true, this message will only be delivered to destination devices that // have an active message delivery channel with a Signal server. bool ephemeral = 2; // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server bool urgent = 3; // The messages to send to the destination account. IndividualRecipientMessageBundle messages = 4; // A means to authorize the request. oneof authorization { // The unidentified access key (UAK) for the destination account. bytes unidentified_access_key = 5 [(require.exactlySize) = 16]; // A group send endorsement token for the destination account. bytes group_send_token = 6; // The destination account allows unrestricted unidentified access google.protobuf.Empty unrestricted_access = 7; } } message SendStoryMessageRequest { // The service identifier of the account to which to deliver the message. common.ServiceIdentifier destination = 1; // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server bool urgent = 2; // The messages to send to the destination account. IndividualRecipientMessageBundle messages = 3; } message SendMessageResponse { // The outcome of the message delivery oneof response { // The message was successfully delivered to all destination devices google.protobuf.Empty success = 1; // A list of discrepancies between the destination devices identified in a // request to send a message and the devices that are actually linked to an // account. MismatchedDevices mismatched_devices = 2 [(tag.reason) = "mismatched_devices"]; // The provided unidentified authorization credential was invalid errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3 [(tag.reason) = "failed_unidentified_authorization"]; // The destination account did not exist errors.NotFound destination_not_found = 4 [(tag.reason) = "destination_not_found"]; } } message MultiRecipientMessage { // The time, in milliseconds since the epoch, at which this message was // originally sent from the perspective of the sender. Note that the maximum // allowable timestamp for JavaScript clients is less than Long.MAX_VALUE; see // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date // for additional details and discussion. uint64 timestamp = 1 [(require.range).min = 1, (require.range).max = 8640000000000000]; // The serialized multi-recipient message payload. bytes payload = 2 [(require.size).max = 762144]; // 256 KiB payload + (5000 * 100) of overhead } message SendMultiRecipientMessageRequest { // If true, this message will only be delivered to destination devices that // have an active message delivery channel with a Signal server. bool ephemeral = 1; // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server bool urgent = 2; // The multi-recipient message to send to all destination accounts and // devices. MultiRecipientMessage message = 3; // A group send endorsement token for the destination account. bytes group_send_token = 4 [(require.nonEmpty) = true]; } message SendMultiRecipientStoryRequest { // Indicates whether this message is urgent and should trigger a high-priority // notification if the destination device does not have an active message // delivery channel with a Signal server bool urgent = 1; // The multi-recipient story message to send to all destination accounts and // devices. MultiRecipientMessage message = 2; } message MultiRecipientSuccess { // A list of destination service identifiers that could not be resolved to // registered Signal accounts. The message in the original request was sent // to all service identifiers/devices in the original request except for the // destination devices associated with the service identifiers in this list. repeated common.ServiceIdentifier unresolved_recipients = 1; } message SendMultiRecipientMessageResponse { // The outcome of the message delivery oneof response { // The message was sent to at least some of the destination accounts/devices // identified in the original request. MultiRecipientSuccess success = 1; // A list of sets of discrepancies between the destination devices // identified in a request to send a message and the devices that are // actually linked to a destination account. MultiRecipientMismatchedDevices mismatched_devices = 2 [(tag.reason) = "mismatched_devices"]; // The provided unidentified authorization credential was invalid errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3 [(tag.reason) = "failed_unidentified_authorization"]; } } message MismatchedDevices { // The service identifier to which the devices named in this object are // linked. common.ServiceIdentifier service_identifier = 1; // A list of device IDs that are linked to the destination account, but were // not included in the collection of messages bound for the destination // account. repeated uint32 missing_devices = 2 [(require.each) = {range: {max: 0x7f}}]; // A list of device IDs that were included in the collection of messages bound // for the destination account, but are not currently linked to the // destination account. repeated uint32 extra_devices = 3 [(require.each) = {range: {max: 0x7f}}]; // A list of device IDs that present in the collection of messages bound for // the destination account and are linked to the destination account, but have // a different registration ID than the registration ID presented by the // sender (indicating that the destination device has likely been replaced by // another device). repeated uint32 stale_devices = 4 [(require.each) = {range: {max: 0x7f}}]; } message MultiRecipientMismatchedDevices { // A list of sets of discrepancies between the destination devices identified // in a request to send a message and the devices that are actually linked to // a destination account. repeated MismatchedDevices mismatched_devices = 1; } message ChallengeRequired { enum ChallengeType { UNSPECIFIED = 0; // A challenge that callers can fulfill by completing a captcha. CAPTCHA = 1; // A challenge that callers can fulfill by supplying a token delivered via // push notification. PUSH_CHALLENGE = 2; }; // An opaque token identifying this challenge request. Clients must generally // submit this token when submitting a challenge response. string token = 1; // A list of challenge types callers may choose to complete to resolve the // challenge requirement. May be empty, in which case callers cannot resolve // the challenge by any means other than waiting. repeated ChallengeType challenge_options = 2; // A duration (in seconds) after which the challenge requirement may be // resolved by simply waiting. May not be set if the challenge cannot be // resolved by waiting. optional uint64 retry_after_seconds = 3; }