Add gRPC support for getUploadForm()

This commit is contained in:
marc-signal
2026-03-27 14:39:42 -04:00
committed by GitHub
parent c7e696c536
commit e9ec8dd431
17 changed files with 272 additions and 94 deletions

View File

@@ -1,2 +1,4 @@
v0.90.1
- Support gRPC for getUploadForm()

View File

@@ -1843,7 +1843,7 @@ export interface RegisterAccountResponse { readonly __type: unique symbol; }
export interface RegistrationAccountAttributes { readonly __type: unique symbol; }
export interface BackupStoreResponse { readonly __type: unique symbol; }
export interface BackupRestoreResponse { readonly __type: unique symbol; }
export const NetRemoteConfigKeys = ['chatRequestConnectionCheckTimeoutMillis', 'useH2ForUnauthChat', 'useH2ForAuthChat', 'grpc.AccountsAnonymousLookupUsernameHash', 'grpc.AccountsAnonymousLookupUsernameLink.2', 'grpc.AccountsAnonymousCheckAccountExistence.2', 'grpc.MessagesAnonymousSendMultiRecipientMessage.2', ] as const;
export const NetRemoteConfigKeys = ['chatRequestConnectionCheckTimeoutMillis', 'useH2ForUnauthChat', 'useH2ForAuthChat', 'grpc.AccountsAnonymousLookupUsernameHash', 'grpc.AccountsAnonymousLookupUsernameLink.2', 'grpc.AccountsAnonymousCheckAccountExistence.2', 'grpc.MessagesAnonymousSendMultiRecipientMessage.2', 'grpc.AttachmentsGetUploadForm', ] as const;
export interface TokioAsyncContext { readonly __type: unique symbol; }
export interface ConnectionManager { readonly __type: unique symbol; }
export interface ConnectionProxyConfig { readonly __type: unique symbol; }

View File

@@ -116,6 +116,7 @@ pub enum RemoteConfigKey {
AccountsAnonymousLookupUsernameLink => "grpc.AccountsAnonymousLookupUsernameLink.2",
AccountsAnonymousCheckAccountExistence => "grpc.AccountsAnonymousCheckAccountExistence.2",
MessagesAnonymousSendMultiRecipientMessage => "grpc.MessagesAnonymousSendMultiRecipientMessage.2",
AttachmentsGetUploadForm => "grpc.AttachmentsGetUploadForm",
}
}
@@ -295,6 +296,7 @@ mod tests {
// Add new services as they become relevant.
let all_known_grpc_keys: HashSet<&str> = std::iter::empty()
.chain(services::AccountsAnonymous::iter().map(|x| x.into()))
.chain(services::Attachments::iter().map(|x| x.into()))
.chain(services::KeysAnonymous::iter().map(|x| x.into()))
.chain(services::MessagesAnonymous::iter().map(|x| x.into()))
.collect();

View File

@@ -11,6 +11,7 @@ use libsignal_net_grpc::proto::chat::backup::get_upload_form_request::{
use libsignal_net_grpc::proto::chat::backup::{
GetUploadFormRequest, GetUploadFormResponse, SignedPresentation, get_upload_form_response,
};
use libsignal_net_grpc::proto::chat::common;
use libsignal_net_grpc::proto::chat::errors::{FailedPrecondition, FailedZkAuthentication};
use super::{GrpcServiceProvider, OverGrpc, log_and_send};
@@ -105,7 +106,7 @@ impl TryFrom<GetUploadFormResponse> for UploadForm {
})?;
match outcome {
Outcome::UploadForm(get_upload_form_response::UploadForm {
Outcome::UploadForm(common::UploadForm {
cdn,
key,
headers,
@@ -189,7 +190,7 @@ mod test {
}
#[test_case(ok(GetUploadFormResponse {
outcome: Some(get_upload_form_response::Outcome::UploadForm(get_upload_form_response::UploadForm {
outcome: Some(get_upload_form_response::Outcome::UploadForm(common::UploadForm {
cdn: 123,
key: "abcde".to_owned(),
headers: HashMap::from_iter([
@@ -251,7 +252,7 @@ mod test {
}
#[test_case(ok(GetUploadFormResponse {
outcome: Some(get_upload_form_response::Outcome::UploadForm(get_upload_form_response::UploadForm {
outcome: Some(get_upload_form_response::Outcome::UploadForm(common::UploadForm {
cdn: 123,
key: "abcde".to_owned(),
headers: HashMap::from_iter([

View File

@@ -3,25 +3,30 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::convert::Infallible;
use std::fmt::Formatter;
use async_trait::async_trait;
use itertools::Itertools as _;
use libsignal_core::{DeviceId, ServiceId};
use libsignal_net_grpc::proto::chat::attachments::attachments_client::AttachmentsClient;
use libsignal_net_grpc::proto::chat::common::ServiceIdentifier;
use libsignal_net_grpc::proto::chat::errors;
use libsignal_net_grpc::proto::chat::messages::messages_anonymous_client::MessagesAnonymousClient;
use libsignal_net_grpc::proto::chat::messages::{
MismatchedDevices, MultiRecipientMessage, MultiRecipientMismatchedDevices,
MultiRecipientSuccess, SendMultiRecipientMessageRequest, SendMultiRecipientMessageResponse,
SendMultiRecipientStoryRequest, send_multi_recipient_message_response,
};
use libsignal_net_grpc::proto::chat::{attachments, common, errors};
use libsignal_protocol::Timestamp;
use super::{GrpcServiceProvider, OverGrpc, log_and_send};
use crate::api::messages::{
MismatchedDeviceError, MultiRecipientMessageResponse, MultiRecipientSendAuthorization,
MultiRecipientSendFailure, SealedSendFailure, SingleOutboundSealedSenderMessage,
UserBasedSendAuthorization,
SingleOutboundUnsealedMessage, UnsealedSendFailure, UserBasedSendAuthorization,
};
use crate::api::{RequestError, Unauth};
use crate::api::{Auth, RequestError, Unauth, UploadForm};
use crate::logging::Redact;
#[async_trait]
@@ -208,8 +213,66 @@ impl std::fmt::Display for Redact<SendMultiRecipientMessageRequest> {
}
}
impl std::fmt::Display for Redact<attachments::GetUploadFormRequest> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let Self(attachments::GetUploadFormRequest {}) = self;
f.debug_struct("attachments::GetUploadFormRequest").finish()
}
}
#[async_trait]
impl<T: GrpcServiceProvider> crate::api::messages::AuthenticatedChatApi<OverGrpc> for Auth<T> {
async fn send_message(
&self,
_destination: ServiceId,
_timestamp: Timestamp,
_contents: &[SingleOutboundUnsealedMessage<'_>],
_online_only: bool,
_urgent: bool,
) -> Result<(), RequestError<UnsealedSendFailure>> {
unimplemented!()
}
async fn send_sync_message(
&self,
_timestamp: Timestamp,
_contents: &[SingleOutboundUnsealedMessage<'_>],
_urgent: bool,
) -> Result<(), RequestError<MismatchedDeviceError>> {
unimplemented!()
}
async fn get_upload_form(&self) -> Result<UploadForm, RequestError<Infallible>> {
let mut attachments_service = AttachmentsClient::new(self.0.service());
let request = attachments::GetUploadFormRequest {};
let log_safe_description = Redact(&request).to_string();
let attachments::GetUploadFormResponse { upload_form } =
log_and_send("auth", &log_safe_description, || {
attachments_service.get_upload_form(request)
})
.await?
.into_inner();
let common::UploadForm {
cdn,
key,
headers,
signed_upload_location,
} = upload_form.ok_or_else(|| RequestError::Unexpected {
log_safe: "GetUploadFormResponse missing upload form".to_string(),
})?;
Ok(UploadForm {
cdn,
key,
headers: headers.into_iter().collect(),
signed_upload_url: signed_upload_location,
})
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use futures_util::FutureExt as _;
use libsignal_core::{Aci, Pni, ServiceId};
use libsignal_net_grpc::proto::chat::services;
@@ -218,7 +281,7 @@ mod test {
use uuid::{Uuid, uuid};
use super::*;
use crate::api::messages::UnauthenticatedChatApi as _;
use crate::api::messages::{AuthenticatedChatApi, UnauthenticatedChatApi as _};
use crate::api::testutil::{SERIALIZED_GROUP_SEND_TOKEN, structurally_valid_group_send_token};
use crate::grpc::testutil::{GrpcOverrideRequestValidator, RequestValidator, err, ok, req};
@@ -457,4 +520,46 @@ mod test {
.expect("success");
assert_eq!(unregistered_ids, &[] as &[ServiceId]);
}
#[test]
fn test_attachment_get_upload_form() {
let validator = GrpcOverrideRequestValidator {
message: services::Attachments::GetUploadForm.into(),
validator: RequestValidator {
expected: req(
"/org.signal.chat.attachments.Attachments/GetUploadForm",
attachments::GetUploadFormRequest {},
),
response: ok(attachments::GetUploadFormResponse {
upload_form: Some(common::UploadForm {
cdn: 2,
key: "my key".to_string(),
headers: HashMap::from_iter([
("one".to_string(), "val1".to_string()),
("two".to_string(), "val2".to_string()),
]),
signed_upload_location: "location".to_string(),
}),
}),
},
};
let mut upload_form = Auth(&validator)
.get_upload_form()
.now_or_never()
.expect("sync")
.expect("success");
upload_form.headers.sort(); // HashMap is non-deterministic
assert_eq!(
upload_form,
UploadForm {
cdn: 2,
key: "my key".to_string(),
headers: vec![
("one".to_string(), "val1".to_string()),
("two".to_string(), "val2".to_string()),
],
signed_upload_url: "location".to_string(),
}
);
}
}

View File

@@ -22,7 +22,8 @@ use super::{
use crate::api::messages::{
MismatchedDeviceError, MultiRecipientMessageResponse, MultiRecipientSendAuthorization,
MultiRecipientSendFailure, SealedSendFailure, SingleOutboundSealedSenderMessage,
SingleOutboundUnsealedMessage, UnsealedSendFailure, UserBasedSendAuthorization,
SingleOutboundUnsealedMessage, UnauthenticatedChatApi, UnsealedSendFailure,
UserBasedSendAuthorization,
};
use crate::api::{Auth, RequestError, Unauth, UploadForm};
use crate::logging::Redact;
@@ -121,7 +122,7 @@ struct SendMessageRequest<'a> {
}
#[async_trait]
impl<T: WsConnection> crate::api::messages::UnauthenticatedChatApi<OverWs> for Unauth<T> {
impl<T: WsConnection> UnauthenticatedChatApi<OverWs> for Unauth<T> {
async fn send_message(
&self,
destination: ServiceId,
@@ -398,6 +399,11 @@ impl<T: WsConnection> crate::api::messages::AuthenticatedChatApi<OverWs> for Aut
}
async fn get_upload_form(&self) -> Result<UploadForm, RequestError<Infallible>> {
if let Some(grpc) =
self.grpc_service_to_use_instead(services::Attachments::GetUploadForm.into())
{
return Auth(grpc).get_upload_form().await;
}
let response = self
.send(
"auth",
@@ -557,7 +563,7 @@ mod test {
use uuid::Uuid;
use super::*;
use crate::api::messages::{AuthenticatedChatApi as _, UnauthenticatedChatApi as _};
use crate::api::messages::AuthenticatedChatApi as _;
use crate::api::testutil::{
SERIALIZED_GROUP_SEND_TOKEN, TEST_SELF_ACI, structurally_valid_group_send_token,
};

View File

@@ -10,6 +10,7 @@ fn main() {
"proto/google/rpc/error_details.proto",
"proto/google/rpc/status.proto",
"proto/org/signal/chat/account.proto",
"proto/org/signal/chat/attachments.proto",
"proto/org/signal/chat/backups.proto",
"proto/org/signal/chat/calling.proto",
"proto/org/signal/chat/call_quality.proto",

View File

@@ -6,6 +6,7 @@ package org.signal.chat.account;
import "org/signal/chat/common.proto";
import "org/signal/chat/errors.proto";
import "org/signal/chat/require.proto";
// Provides methods for working with Signal accounts.
service Accounts {
@@ -87,7 +88,7 @@ message DeleteAccountResponse {
message SetRegistrationLockRequest {
// The new registration lock secret for the authenticated account.
bytes registration_lock = 1;
bytes registration_lock = 1 [(require.exactlySize) = 32];
}
message SetRegistrationLockResponse {
@@ -100,8 +101,9 @@ message ClearRegistrationLockResponse {
}
message ReserveUsernameHashRequest {
// A prioritized list of username hashes to attempt to reserve.
repeated bytes username_hashes = 1;
// A prioritized list of username hashes to attempt to reserve. Each hash must
// be exactly 32 bytes.
repeated bytes username_hashes = 1 [(require.size) = {min: 1, max: 20}];
}
message UsernameNotAvailable {}
@@ -119,15 +121,15 @@ message ReserveUsernameHashResponse {
message ConfirmUsernameHashRequest {
// The username hash to claim for the authenticated account.
bytes username_hash = 1;
bytes username_hash = 1 [(require.exactlySize) = 32];
// A zero-knowledge proof that the given username hash was generated by the
// Signal username algorithm.
bytes zk_proof = 2;
bytes zk_proof = 2 [(require.nonEmpty) = true];
// The ciphertext of the chosen username for use in public-facing contexts
// (e.g. links and QR codes).
bytes username_ciphertext = 3;
bytes username_ciphertext = 3 [(require.size) = {min: 1, max: 128}];
}
message ConfirmUsernameHashResponse {
@@ -160,7 +162,7 @@ message DeleteUsernameHashResponse {
message SetUsernameLinkRequest {
// The username ciphertext for which to generate a new link handle.
bytes username_ciphertext = 1;
bytes username_ciphertext = 1 [(require.size) = {min: 1, max: 128}];
// If true and the account already had an encrypted username stored, the
// existing link handle will be reused. Otherwise a new link handle will be
@@ -214,7 +216,7 @@ message SetDiscoverableByPhoneNumberResponse {
message SetRegistrationRecoveryPasswordRequest {
// The new registration recovery password for the authenticated account.
bytes registration_recovery_password = 1;
bytes registration_recovery_password = 1 [(require.exactlySize) = 32];
}
message SetRegistrationRecoveryPasswordResponse {
@@ -233,7 +235,7 @@ message CheckAccountExistenceResponse {
message LookupUsernameHashRequest {
// A 32-byte username hash for which to find an account.
bytes username_hash = 1;
bytes username_hash = 1 [(require.exactlySize) = 32];
}
message LookupUsernameHashResponse {
@@ -249,7 +251,7 @@ message LookupUsernameHashResponse {
message LookupUsernameLinkRequest {
// The link handle for which to find an encrypted username. Link handles are
// 16-byte representations of UUIDs.
bytes username_link_handle = 1;
bytes username_link_handle = 1 [(require.exactlySize) = 16];
}
message LookupUsernameLinkResponse {

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
syntax = "proto3";
option java_multiple_files = true;
package org.signal.chat.attachments;
import "org/signal/chat/common.proto";
import "org/signal/chat/require.proto";
service Attachments {
option (require.auth) = AUTH_ONLY_AUTHENTICATED;
// Retrieve an upload form that can be used to perform a resumable upload
rpc GetUploadForm(GetUploadFormRequest) returns (GetUploadFormResponse) {}
}
message GetUploadFormRequest {}
message GetUploadFormResponse {
common.UploadForm upload_form = 1;
}

View File

@@ -202,18 +202,18 @@ service BackupsAnonymous {
message SignedPresentation {
// Presentation of a BackupAuthCredential previously retrieved from
// GetBackupAuthCredentials on the authenticated channel
bytes presentation = 1;
bytes presentation = 1 [(require.nonEmpty) = true];
// The presentation signed with the private key corresponding to the public
// key set with SetPublicKey
bytes presentation_signature = 2;
bytes presentation_signature = 2 [(require.nonEmpty) = true];
}
message SetPublicKeyRequest {
SignedPresentation signed_presentation = 1;
// The public key, serialized in libsignal's elliptic-curve public key format.
bytes public_key = 2;
bytes public_key = 2 [(require.nonEmpty) = true];
}
message SetPublicKeyResponse {
@@ -367,23 +367,8 @@ message GetUploadFormRequest {
}
}
message GetUploadFormResponse {
message UploadForm {
// Indicates the CDN type. 3 indicates resumable uploads using TUS
uint32 cdn = 1;
// The location within the specified cdn where the finished upload can be found
string key = 2;
// A map of headers to include with all upload requests. Potentially contains
// time-limited upload credentials
map<string, string> headers = 3;
// The URL to upload to with the appropriate protocol
string signed_upload_location = 4;
}
oneof outcome {
UploadForm upload_form = 1;
common.UploadForm upload_form = 1;
// The provided backup auth credential presentation could not be
// authenticated. Either, the presentation could not be verified, or
@@ -422,7 +407,7 @@ message CopyMediaRequest {
SignedPresentation signed_presentation = 1;
// Items to copy
repeated CopyMediaItem items = 2;
repeated CopyMediaItem items = 2 [(require.size) = {min: 1, max: 1000}];
}
message CopyMediaResponse {
@@ -465,7 +450,7 @@ message ListMediaRequest {
optional string cursor = 2;
// If provided, the maximum number of entries to return in a page
uint32 limit = 3 [(require.range) = {min: 0, max: 10000}];
uint32 limit = 3 [(require.range) = {min: 1, max: 10000}];
}
message ListMediaResponse {
message ListEntry {
@@ -529,13 +514,13 @@ message DeleteMediaItem {
uint32 cdn = 1;
// The media_id of the object to delete
bytes media_id = 2;
bytes media_id = 2 [(require.exactlySize) = 15];
}
message DeleteMediaRequest {
SignedPresentation signed_presentation = 1;
repeated DeleteMediaItem items = 2;
repeated DeleteMediaItem items = 2 [(require.size) = {min: 1, max: 1000}];
}
message DeleteMediaResponse {

View File

@@ -9,6 +9,8 @@ option java_multiple_files = true;
package org.signal.chat.common;
import "org/signal/chat/require.proto";
enum IdentityType {
IDENTITY_TYPE_UNSPECIFIED = 0;
IDENTITY_TYPE_ACI = 1;
@@ -20,7 +22,7 @@ message ServiceIdentifier {
IdentityType identity_type = 1;
// The UUID of the identity represented by this service identifier.
bytes uuid = 2;
bytes uuid = 2 [(require.exactlySize) = 16];
}
message AccountIdentifiers {
@@ -36,35 +38,40 @@ message AccountIdentifiers {
}
message EcPreKey {
// A locally-unique identifier for this key.
uint64 key_id = 1;
// A locally-unique identifier for this key, which will be provided by
// peers using this key to encrypt messages so the private key can be looked
// up.
uint32 key_id = 1;
// The serialized form of the public key.
bytes public_key = 2;
// The public key, serialized in libsignal's elliptic-curve public key format.
bytes public_key = 2 [(require.nonEmpty) = true];
}
message EcSignedPreKey {
// A locally-unique identifier for this key.
uint64 key_id = 1;
// A locally-unique identifier for this key, which will be provided by
// peers using this key to encrypt messages so the private key can be looked
// up.
uint32 key_id = 1;
// The serialized form of the public key.
bytes public_key = 2;
// The public key, serialized in libsignal's elliptic-curve public key format.
bytes public_key = 2 [(require.nonEmpty) = true];
// A signature of the public key, verifiable with the identity key for the
// account/identity associated with this pre-key.
bytes signature = 3;
bytes signature = 3 [(require.nonEmpty) = true];
}
message KemSignedPreKey {
// A locally-unique identifier for this key.
uint64 key_id = 1;
// An locally-unique identifier for this key, which will be provided by peers
// using this key to encrypt messages so the private key can be looked up.
uint32 key_id = 1;
// The serialized form of the public key.
bytes public_key = 2;
// The public key, serialized in libsignal's Kyber1024 public key format.
bytes public_key = 2 [(require.nonEmpty) = true];
// A signature of the public key, verifiable with the identity key for the
// account/identity associated with this pre-key.
bytes signature = 3;
bytes signature = 3 [(require.nonEmpty) = true];
}
enum DeviceCapability {
@@ -87,6 +94,22 @@ message ZkCredential {
/*
* The ZK credential, using libsignal's serialization
*/
bytes credential = 2;
bytes credential = 2 [(require.nonEmpty) = true];
}
// An upload location and credentials which may be used to upload an object
// to an external CDN
message UploadForm {
// Indicates the CDN type. 3 indicates resumable uploads using TUS
uint32 cdn = 1;
// The location within the specified cdn where the finished upload can be found
string key = 2;
// A map of headers to include with all upload requests. Potentially contains
// time-limited upload credentials
map<string, string> headers = 3;
// The URL to upload to with the appropriate protocol
string signed_upload_location = 4;
}

View File

@@ -7,6 +7,8 @@ syntax = "proto3";
option java_multiple_files = true;
import "org/signal/chat/require.proto";
package org.signal.chat.credentials;
// Provides methods for obtaining and verifying credentials for "external" services
@@ -69,7 +71,7 @@ message CheckSvrCredentialsRequest {
// A list of credentials from previously made calls to `ExternalServiceCredentials.GetExternalServiceCredentials()`
// for `EXTERNAL_SERVICE_TYPE_SVR`. This list may contain credentials generated by different users. Up to 10 credentials
// can be checked.
repeated string passwords = 2;
repeated string passwords = 2 [(require.nonEmpty) = true, (require.size) = {max: 10}];
}
// For each of the credentials tokens in the `CheckSvrCredentialsRequest` contains the result of the check.

View File

@@ -83,7 +83,7 @@ message RemoveDeviceRequest {
message SetDeviceNameRequest {
// A sequence of bytes that encodes an encrypted human-readable name for this
// device.
bytes name = 1 [(require.size) = {min: 1, max: 256}];
bytes name = 1 [(require.size) = {min: 1, max: 225}];
// The identifier for the device for which to set a name.
uint32 id = 2;

View File

@@ -9,8 +9,12 @@ option java_multiple_files = true;
package org.signal.chat.keys;
import "google/protobuf/empty.proto";
import "org/signal/chat/common.proto";
import "org/signal/chat/errors.proto";
import "org/signal/chat/require.proto";
import "org/signal/chat/tag.proto";
// Provides methods for working with pre-keys.
service Keys {
@@ -111,6 +115,9 @@ message GetPreKeysAnonymousRequest {
// A group send endorsement token for the targeted account.
bytes group_send_token = 3;
// The destination account allows unrestricted unidentified access
google.protobuf.Empty unrestricted_access = 4;
}
}
@@ -146,7 +153,7 @@ message GetPreKeysResponse {
// Either the target account was not found, no active device with the given
// ID (if specified) was found on the target account.
errors.NotFound target_not_found = 2;
errors.NotFound target_not_found = 2 [(tag.reason) = "not_found"];
}
}
@@ -157,10 +164,10 @@ message GetPreKeysAnonymousResponse {
// Either the target account was not found, no active device with the given
// ID (if specified) was found on the target account.
errors.NotFound target_not_found = 2;
errors.NotFound target_not_found = 2 [(tag.reason) = "not_found"];
// The provided unidentified authorization credential was invalid
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3;
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3 [(tag.reason) = "failed_unidentified_authorization"];
}
}
@@ -170,7 +177,7 @@ message SetOneTimeEcPreKeysRequest {
common.IdentityType identity_type = 1;
// The unsigned EC pre-keys to be stored.
repeated common.EcPreKey pre_keys = 2;
repeated common.EcPreKey pre_keys = 2 [(require.size) = {min: 1, max: 100}];
}
message SetOneTimeKemSignedPreKeysRequest {
@@ -179,7 +186,7 @@ message SetOneTimeKemSignedPreKeysRequest {
common.IdentityType identity_type = 1;
// The KEM pre-keys to be stored.
repeated common.KemSignedPreKey pre_keys = 2;
repeated common.KemSignedPreKey pre_keys = 2 [(require.size) = {min: 1, max: 100}];
}
message SetEcSignedPreKeyRequest {
@@ -187,7 +194,7 @@ message SetEcSignedPreKeyRequest {
common.IdentityType identity_type = 1;
// The signed EC pre-key itself.
common.EcSignedPreKey signed_pre_key = 2;
common.EcSignedPreKey signed_pre_key = 2 [(require.present) = true];
}
message SetKemLastResortPreKeyRequest {
@@ -195,7 +202,7 @@ message SetKemLastResortPreKeyRequest {
common.IdentityType identity_type = 1;
// The signed KEM pre-key itself.
common.KemSignedPreKey signed_pre_key = 2;
common.KemSignedPreKey signed_pre_key = 2 [(require.present) = true];
}
message SetPreKeyResponse {
@@ -205,7 +212,7 @@ message CheckIdentityKeyRequest {
// The service identifier of the account for which we want to check the associated identity key fingerprint.
common.ServiceIdentifier target_identifier = 1;
// The most significant 4 bytes of the SHA-256 hash of the identity key associated with the target account/identity type.
bytes fingerprint = 2;
bytes fingerprint = 2 [(require.exactlySize) = 4];
}
message CheckIdentityKeyResponse {

View File

@@ -70,6 +70,10 @@ message IndividualRecipientMessageBundle {
// 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
@@ -87,7 +91,7 @@ message IndividualRecipientMessageBundle {
map<uint32, Message> messages = 2 [(require.nonEmpty) = true];
}
enum AuthenticatedSenderMessageType {
enum SendMessageType {
UNSPECIFIED = 0;
// A double-ratchet message represents a "normal," "unsealed-sender" message
@@ -102,7 +106,7 @@ enum AuthenticatedSenderMessageType {
// 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
// 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.
@@ -110,6 +114,14 @@ enum AuthenticatedSenderMessageType {
// 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 {
@@ -117,20 +129,17 @@ message SendAuthenticatedSenderMessageRequest {
// The service identifier of the account to which to deliver the message.
common.ServiceIdentifier destination = 1;
// The type identifier for this message.
AuthenticatedSenderMessageType type = 2 [(require.specified) = true];
// If true, this message will only be delivered to destination devices that
// have an active message delivery channel with a Signal server.
bool ephemeral = 3;
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 = 4;
bool urgent = 3;
// The messages to send to the destination account.
IndividualRecipientMessageBundle messages = 5;
IndividualRecipientMessageBundle messages = 4;
}
message SendMessageAuthenticatedSenderResponse {
@@ -159,16 +168,13 @@ message SendMessageAuthenticatedSenderResponse {
message SendSyncMessageRequest {
// The type identifier for this message.
AuthenticatedSenderMessageType type = 1 [(require.specified) = true];
// 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;
bool urgent = 1;
// The messages to send to the destination account.
IndividualRecipientMessageBundle messages = 3;
IndividualRecipientMessageBundle messages = 2;
}
message SendSealedSenderMessageRequest {
@@ -192,10 +198,13 @@ message SendSealedSenderMessageRequest {
oneof authorization {
// The unidentified access key (UAK) for the destination account.
bytes unidentified_access_key = 5;
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;
}
}
@@ -352,7 +361,7 @@ message ChallengeRequired {
// An opaque token identifying this challenge request. Clients must generally
// submit this token when submitting a challenge response.
bytes token = 1;
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

View File

@@ -14,8 +14,9 @@ import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
/*
* Requires a field to have content of non-zero size/length.
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
* it's considered to be empty.
* Applies to both `optional` and regular fields, i.e. if the field is not set
* or has a default value, it's considered to be empty. This does not apply
* to fields that are contained in a `oneof`.
*
* ```
* import "org/signal/chat/require.proto";
@@ -57,8 +58,10 @@ extend google.protobuf.FieldOptions {
/*
* Requires a size/length of a field to be within certain boundaries.
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
* its size considered to be zero.
* Applies to both `optional` and regular fields, i.e. if the field is not set
* or has a default value, its size considered to be zero. However, if the
* field is contained in a `oneof` and is not set, this annotation does not
* apply.
*
* ```
* import "org/signal/chat/require.proto";
@@ -76,9 +79,11 @@ extend google.protobuf.FieldOptions {
optional SizeConstraint size = 70003;
/*
* Requires a size/length of a field to be one of the specified values.
* Applies to both `optional` and regular fields, i.e. if the field is not set or has a default value,
* its size considered to be zero.
* Requires a size/length of a field to be within certain boundaries.
* Applies to both `optional` and regular fields, i.e. if the field is not set
* or has a default value, its size considered to be zero. However, if the
* field is contained in a `oneof` and is not set, this annotation does not
* apply.
*
* ```
* import "org/signal/chat/require.proto";
@@ -130,7 +135,8 @@ extend google.protobuf.FieldOptions {
* Require a value of a message field to be present.
*
* Applies to both `optional` and regular fields (both of which have explicit
* presence for the message type anyways)
* presence for the message type anyways). This does not apply to fields that
* are contained in a `oneof`.
*
* ```
* import "org/signal/chat/require.proto";

View File

@@ -13,10 +13,12 @@ pub mod proto {
pub mod errors {
tonic::include_proto!("org.signal.chat.errors");
}
pub mod account {
tonic::include_proto!("org.signal.chat.account");
}
pub mod attachments {
tonic::include_proto!("org.signal.chat.attachments");
}
pub mod backup {
tonic::include_proto!("org.signal.chat.backup");
}