mirror of
https://github.com/mautrix/signal.git
synced 2026-09-18 00:27:01 -04:00
406 lines
16 KiB
Protocol Buffer
406 lines
16 KiB
Protocol Buffer
|
|
/*
|
|||
|
|
* Copyright 2023 Signal Messenger, LLC
|
|||
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
syntax = "proto3";
|
|||
|
|
|
|||
|
|
option java_multiple_files = true;
|
|||
|
|
|
|||
|
|
package org.signal.chat.profile;
|
|||
|
|
|
|||
|
|
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 profiles and profile-related data.
|
|||
|
|
service Profile {
|
|||
|
|
option (require.auth) = AUTH_ONLY_AUTHENTICATED;
|
|||
|
|
|
|||
|
|
// Sets profile data and, if needed, returns credentials used by clients to upload a v1 avatar.
|
|||
|
|
rpc SetProfile(SetProfileRequest) returns (SetProfileResponse) {}
|
|||
|
|
|
|||
|
|
// Retrieves profile data. Callers with an unidentified access key for the account
|
|||
|
|
// should use the version of this method in `ProfileAnonymous` instead.
|
|||
|
|
rpc GetProfile(GetProfileRequest) returns (GetProfileResponse) {}
|
|||
|
|
|
|||
|
|
// Returns anonymous credentials that may be presented with avatar operations in ProfilesAnonymous
|
|||
|
|
//
|
|||
|
|
// Note: `Accounts.SetZkCredentialKey` is a pre-requisite for this RPC
|
|||
|
|
rpc GetAvatarCredentials(GetAvatarCredentialsRequest) returns (GetAvatarCredentialsResponse) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Provides methods for working with profiles and profile-related data using "unidentified access"
|
|||
|
|
// credentials. Callers must not submit any self-identifying credentials
|
|||
|
|
// when calling methods in this service and must instead present the targeted account's
|
|||
|
|
// unidentified access key as an anonymous authentication mechanism. Callers
|
|||
|
|
// without an unidentified access key should use the equivalent, authenticated
|
|||
|
|
// methods in `Profile` instead.
|
|||
|
|
service ProfileAnonymous {
|
|||
|
|
option (require.auth) = AUTH_ONLY_ANONYMOUS;
|
|||
|
|
|
|||
|
|
// Retrieves profile data.
|
|||
|
|
rpc GetProfile(GetProfileAnonymousRequest) returns (GetProfileAnonymousResponse) {}
|
|||
|
|
|
|||
|
|
// Retrieves a profile key credential.
|
|||
|
|
rpc GetExpiringProfileKeyCredential(GetExpiringProfileKeyCredentialAnonymousRequest) returns (GetExpiringProfileKeyCredentialAnonymousResponse) {}
|
|||
|
|
|
|||
|
|
// Returns credentials to upload a v2 avatar. After uploading the avatar, the client
|
|||
|
|
// must call SetProfile with the new avatar URL in the encrypted `data`.
|
|||
|
|
//
|
|||
|
|
// Because avatars are uploaded anonymously, they have an expiration equal to the
|
|||
|
|
// idle account expiration. Clients must periodically (recommended: every 90 days)
|
|||
|
|
// call ExtendAvatarTTL to extend the TTl.
|
|||
|
|
//
|
|||
|
|
// Note: any existing avatar associated with these credentials will be deleted immediately
|
|||
|
|
rpc GetAvatarUploadForm(GetAvatarUploadFormRequest) returns (GetAvatarUploadFormResponse) {}
|
|||
|
|
|
|||
|
|
// Extends the TTL of the avatar currently associated with the request’s avatar auth credential
|
|||
|
|
rpc ExtendAvatarTTL(ExtendAvatarTTLRequest) returns (ExtendAvatarTTLResponse) {}
|
|||
|
|
|
|||
|
|
// Deletes the avatar currently associated with the request’s avatar auth credential.
|
|||
|
|
//
|
|||
|
|
// Clients must also call SetProfile to remove the avatar from the encrypted `data`.
|
|||
|
|
rpc DeleteAvatar(DeleteAvatarRequest) returns (DeleteAvatarResponse) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message SetProfileV1Request {
|
|||
|
|
enum AvatarChange {
|
|||
|
|
AVATAR_CHANGE_UNCHANGED = 0;
|
|||
|
|
AVATAR_CHANGE_CLEAR = 1;
|
|||
|
|
AVATAR_CHANGE_UPDATE = 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The ciphertext of a name that users must set on the profile.
|
|||
|
|
bytes name = 1 [(require.exactlySize) = 81, (require.exactlySize) = 285];
|
|||
|
|
|
|||
|
|
// An enum to indicate what change, if any, is made to the avatar with this request.
|
|||
|
|
AvatarChange avatar_change = 2;
|
|||
|
|
|
|||
|
|
// The ciphertext of an emoji that users can set on their profile.
|
|||
|
|
bytes about_emoji = 3 [(require.exactlySize) = 0, (require.exactlySize) = 60];
|
|||
|
|
|
|||
|
|
// The ciphertext of a description that users can set on their profile.
|
|||
|
|
bytes about = 4 [(require.exactlySize) = 0, (require.exactlySize) = 156, (require.exactlySize) = 282, (require.exactlySize) = 540];
|
|||
|
|
|
|||
|
|
// The ciphertext of the phone-number sharing setting on the profile. 29-byte encrypted boolean.
|
|||
|
|
bytes phone_number_sharing = 6 [(require.exactlySize) = 29];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message SetProfileRequest {
|
|||
|
|
|
|||
|
|
// The profile version. Required.
|
|||
|
|
bytes version = 1 [(require.exactlySize) = 32];
|
|||
|
|
|
|||
|
|
// The ciphertext of a serialized Profile protobuf. Required.
|
|||
|
|
// 937 max length = 909 plaintext serialization + 28 bytes encryption overhead
|
|||
|
|
bytes data = 2 [(require.nonEmpty) = true, (require.size).max = 937];
|
|||
|
|
|
|||
|
|
// The SHA-256 hash of the Profile ciphertext being replaced.
|
|||
|
|
// This is used an optimistic lock against concurrent changes.
|
|||
|
|
//
|
|||
|
|
// Optional, if this is the initial request to create a version.
|
|||
|
|
bytes expected_current_data_hash = 3 [(require.exactlySize) = 0, (require.exactlySize) = 32];
|
|||
|
|
|
|||
|
|
// The current profile version being updated.
|
|||
|
|
// This is used as an optimistic lock against concurrent changes.
|
|||
|
|
//
|
|||
|
|
// Optional, if there is no current profile version for the account.
|
|||
|
|
bytes expected_current_version = 4 [(require.exactlySize) = 0, (require.exactlySize) = 32];
|
|||
|
|
|
|||
|
|
// The ciphertext of the MobileCoin wallet ID on the profile.
|
|||
|
|
bytes payment_address = 5 [(require.exactlySize) = 0, (require.exactlySize) = 582];
|
|||
|
|
|
|||
|
|
// A list of badge IDs associated with the profile.
|
|||
|
|
repeated string badge_ids = 6;
|
|||
|
|
|
|||
|
|
// The profile key commitment. Used to issue a profile key credential response.
|
|||
|
|
//
|
|||
|
|
// Required during the v1 -> v2 migration period. Afterwards will be optional, if this is an update to an existing version.
|
|||
|
|
bytes commitment = 7 [(require.exactlySize) = 0, (require.exactlySize) = 97];
|
|||
|
|
|
|||
|
|
// An embedded v1 request. Required during the v1 -> v2 migration period.
|
|||
|
|
//
|
|||
|
|
// Because this is a temporary field during the migration, it has the highest
|
|||
|
|
// field number without serialization overhead. This is purely aesthetic.
|
|||
|
|
SetProfileV1Request v1Request = 15 [(require.present) = true];
|
|||
|
|
|
|||
|
|
// next: 8
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Indicates that the account is not permitted to set a payment address,
|
|||
|
|
// due to a disallowed country prefix on the account's phone number.
|
|||
|
|
message PaymentsForbiddenInRegion {}
|
|||
|
|
|
|||
|
|
// Indicates that the account does not have the Profiles v2 capability, which
|
|||
|
|
// is required to call Profiles.SetProfile
|
|||
|
|
message ProfilesV2CapabilityRequired {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
message SetProfileResult {
|
|||
|
|
// If the request included a v1 avatar change, this field contains the policy
|
|||
|
|
// and credential used by clients to upload an avatar to the CDN.
|
|||
|
|
//
|
|||
|
|
// Because this is a temporary field during the migration, it has the highest
|
|||
|
|
// field number without serialization overhead. This is purely aesthetic.
|
|||
|
|
optional common.S3UploadForm v1_avatar_upload_form = 15;
|
|||
|
|
|
|||
|
|
// next: 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message SetProfileResponse {
|
|||
|
|
oneof response {
|
|||
|
|
SetProfileResult result = 1;
|
|||
|
|
// The current data hash did not match the request's expectation, indicating
|
|||
|
|
// another device on the account may have written an update the caller does not know about.
|
|||
|
|
errors.FailedPrecondition expected_data_write_conflict = 2 [(tag.reason) = "expected_data_write_conflict"];
|
|||
|
|
// Payments are not permitted in the account's region, based on its phone
|
|||
|
|
// number or the caller's IP address if the account does not have a phone
|
|||
|
|
// number. The request should be retried without `payment_address.
|
|||
|
|
PaymentsForbiddenInRegion payments_forbidden_in_region = 3 [(tag.reason) = "payments_forbidden_in_region"];
|
|||
|
|
// The current version did not match the request's expectation, indicating
|
|||
|
|
// another device on the account may have created a new version the caller does not know about.
|
|||
|
|
errors.FailedPrecondition expected_version_write_conflict = 4 [(tag.reason) = "expected_version_write_conflict"];
|
|||
|
|
|
|||
|
|
// Because this is a temporary field during the migration, it has the highest
|
|||
|
|
// field number without serialization overhead. This is purely aesthetic.
|
|||
|
|
ProfilesV2CapabilityRequired profiles_v2_capability_required = 15 [(tag.reason) = "profiles_v2_capability_required"];
|
|||
|
|
|
|||
|
|
// next: 4
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetProfileRequest {
|
|||
|
|
// The ACI of the account for which to get profile data.
|
|||
|
|
common.ServiceIdentifier account_identifier = 1 [(require.present) = true, (require.identityType) = IDENTITY_TYPE_ACI];
|
|||
|
|
// The profile version to retrieve.
|
|||
|
|
bytes version = 2 [(require.exactlySize) = 32];
|
|||
|
|
|
|||
|
|
// The `etag` from the previous request for this Profile version. If
|
|||
|
|
// unchanged, the response will omit `profile` and `etag_matched` will be
|
|||
|
|
// `true`.
|
|||
|
|
bytes etag = 3 [(require.exactlySize) = 0, (require.exactlySize) = 10];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetProfileAnonymousRequest {
|
|||
|
|
// Contains the data necessary to request a profile.
|
|||
|
|
GetProfileRequest request = 1 [(require.present) = true];
|
|||
|
|
oneof authentication {
|
|||
|
|
// The unidentified access key for the targeted account.
|
|||
|
|
bytes unidentified_access_key = 2 [(require.exactlySize) = 16];
|
|||
|
|
|
|||
|
|
// A group send endorsement token for the targeted account.
|
|||
|
|
bytes group_send_token = 3 [(require.nonEmpty) = true];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message AccountInfo {
|
|||
|
|
// The account identity key of the targeted account.
|
|||
|
|
bytes identity_key = 1;
|
|||
|
|
|
|||
|
|
// A checksum of the unidentified access key for the targeted account.
|
|||
|
|
bytes unidentified_access_key_fingerprint = 2;
|
|||
|
|
|
|||
|
|
// Whether the account has enabled sealed sender from anyone.
|
|||
|
|
bool unrestricted_unidentified_access = 3;
|
|||
|
|
|
|||
|
|
// A list of the badges ids associated with the account. Metadata to display
|
|||
|
|
// badges may be obtained by cross-referencing badge ids with
|
|||
|
|
// RemoteConfiguration.GetBadges.
|
|||
|
|
repeated string badge_ids = 4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message ProfileResult {
|
|||
|
|
// The ciphertext of the requested version of the profile
|
|||
|
|
bytes data = 1;
|
|||
|
|
|
|||
|
|
// The ciphertext of the MobileCoin wallet ID on the profile.
|
|||
|
|
bytes payment_address = 2;
|
|||
|
|
|
|||
|
|
// Information about the targeted account
|
|||
|
|
AccountInfo account_info = 3;
|
|||
|
|
|
|||
|
|
// An entity tag for the profile. This may be used in subsequent requests
|
|||
|
|
// for this profile version to optimize bandwidth.
|
|||
|
|
//
|
|||
|
|
// Note that this hash will not match the expected_data_hash used on
|
|||
|
|
// SetProfile for concurrency control.
|
|||
|
|
//
|
|||
|
|
// Clients may validate that the value has been correctly calculated by
|
|||
|
|
// calculating a 10-byte truncated TupleHash256 as follows:
|
|||
|
|
//
|
|||
|
|
// ```
|
|||
|
|
// TupleHash256(S = "ProfileETag/v1", L = 80 bits, tuple = [
|
|||
|
|
// data,
|
|||
|
|
// payment_address,
|
|||
|
|
// account_info.identity_key,
|
|||
|
|
// account_info.unidentified_access_key_fingerprint,
|
|||
|
|
// account_info.unrestricted_unidentified_access ? 0x01 : 0x00,
|
|||
|
|
// uint32BE(count(account_info.badge_ids)),
|
|||
|
|
// utf8(id) for each id in sortLexAscendingByUtf8Bytes(account_info.badge_ids),
|
|||
|
|
// ])
|
|||
|
|
//```
|
|||
|
|
//
|
|||
|
|
// Absent/empty fields contribute an empty element (not zero bytes omitted):
|
|||
|
|
// a missing payment_address is still a present, zero-length tuple element.
|
|||
|
|
//
|
|||
|
|
// This etag will always exclusively cover the documented fields. If fields
|
|||
|
|
// are added in the future, a new etag field must be introduced.
|
|||
|
|
bytes etag = 4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message LegacyProfileResult {
|
|||
|
|
|
|||
|
|
// The ciphertext of the name on the profile.
|
|||
|
|
bytes name = 1;
|
|||
|
|
// The ciphertext of the description on the profile.
|
|||
|
|
bytes about = 2;
|
|||
|
|
// The ciphertext of the emoji on the profile.
|
|||
|
|
bytes about_emoji = 3;
|
|||
|
|
// The cdn0 path of the avatar on the profile.
|
|||
|
|
string avatar = 4;
|
|||
|
|
// The ciphertext of the phone-number sharing setting on the profile.
|
|||
|
|
bytes phone_number_sharing = 5;
|
|||
|
|
// The ciphertext of the MobileCoin wallet ID on the profile.
|
|||
|
|
bytes payment_address = 6;
|
|||
|
|
// Information about the targeted account
|
|||
|
|
AccountInfo account_info = 7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
message GetProfileResponse {
|
|||
|
|
oneof response {
|
|||
|
|
// The full profile data. The current profile did not match the provided
|
|||
|
|
// etag (or no etag was provided).
|
|||
|
|
ProfileResult profile = 1;
|
|||
|
|
|
|||
|
|
// The current profile matched the provided etag. If present, this will always be true.
|
|||
|
|
bool etag_matched = 2 [(tag.reason) = "etag_match"];
|
|||
|
|
|
|||
|
|
errors.NotFound not_found = 3 [(tag.reason) = "not_found"];
|
|||
|
|
|
|||
|
|
// Will be present if there is no v2 Profile data for this version.
|
|||
|
|
//
|
|||
|
|
// Because this is a temporary field during the migration, it has the highest
|
|||
|
|
// field number without serialization overhead. This is purely aesthetic.
|
|||
|
|
LegacyProfileResult legacy_profile = 15 [(tag.reason) = "legacy_profile"];
|
|||
|
|
}
|
|||
|
|
// next: 4
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetProfileAnonymousResponse {
|
|||
|
|
oneof response {
|
|||
|
|
// The full profile data. The current profile did not match the provided
|
|||
|
|
// etag (or no etag was provided).
|
|||
|
|
ProfileResult profile = 1;
|
|||
|
|
|
|||
|
|
// The current profile matched the provided etag. If present, this will always be true.
|
|||
|
|
bool etag_matched = 2 [(tag.reason) = "etag_match"];
|
|||
|
|
|
|||
|
|
errors.NotFound not_found = 3 [(tag.reason) = "not_found"];
|
|||
|
|
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 4 [(tag.reason) = "failed_unidentified_authorization"];
|
|||
|
|
|
|||
|
|
// Will be present if there is no v2 Profile data for this version.
|
|||
|
|
//
|
|||
|
|
// Because this is a temporary field during the migration, it has the highest
|
|||
|
|
// field number without serialization overhead. This is purely aesthetic.
|
|||
|
|
LegacyProfileResult profile_v1 = 15 [(tag.reason) = "profile_v1"];
|
|||
|
|
}
|
|||
|
|
// next: 5
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetExpiringProfileKeyCredentialRequest {
|
|||
|
|
// The ACI of the account for which to get a profile key credential.
|
|||
|
|
common.ServiceIdentifier account_identifier = 1 [(require.present) = true, (require.identityType) = IDENTITY_TYPE_ACI];
|
|||
|
|
// A zkgroup request for a profile key credential.
|
|||
|
|
bytes credential_request = 2 [(require.nonEmpty) = true];
|
|||
|
|
// The type of credential being requested.
|
|||
|
|
CredentialType credential_type = 3 [(require.specified) = true];
|
|||
|
|
// The profile version for which to generate a profile key credential.
|
|||
|
|
bytes version = 4 [(require.exactlySize) = 32];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetExpiringProfileKeyCredentialAnonymousRequest {
|
|||
|
|
// Contains the data necessary to request an expiring profile key credential.
|
|||
|
|
GetExpiringProfileKeyCredentialRequest request = 1 [(require.present) = true];
|
|||
|
|
// The unidentified access key for the targeted account.
|
|||
|
|
bytes unidentified_access_key = 2 [(require.exactlySize) = 16];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetExpiringProfileKeyCredentialResult {
|
|||
|
|
// A zkgroup credential used by a client to prove that it has the profile key
|
|||
|
|
// of a targeted account.
|
|||
|
|
bytes profile_key_credential = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetExpiringProfileKeyCredentialAnonymousResponse {
|
|||
|
|
oneof response {
|
|||
|
|
GetExpiringProfileKeyCredentialResult result = 1;
|
|||
|
|
errors.NotFound not_found = 2 [(tag.reason) = "not_found"];
|
|||
|
|
errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3 [(tag.reason) = "failed_unidentified_authorization"];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enum CredentialType {
|
|||
|
|
CREDENTIAL_TYPE_UNSPECIFIED = 0;
|
|||
|
|
CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// avatar auth
|
|||
|
|
|
|||
|
|
message GetAvatarCredentialsRequest {
|
|||
|
|
bytes avatar_credentials_request = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetAvatarCredentialsResponse {
|
|||
|
|
oneof response {
|
|||
|
|
bytes avatar_credentials = 1;
|
|||
|
|
|
|||
|
|
// the client must call Accounts.SetZkCredentialKey to call this method
|
|||
|
|
errors.FailedPrecondition missing_zk_credential_key = 2 [(tag.reason) = "missing_zk_credential_key"];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetAvatarUploadFormRequest {
|
|||
|
|
bytes avatar_credentials_presentation = 1;
|
|||
|
|
|
|||
|
|
// The length of the attachment for the requested upload form. Uploads
|
|||
|
|
// performed with this form will be limited to the provided length.
|
|||
|
|
uint32 upload_length = 2 [(require.range) = {min: 1, max: 10485760 /* 10 MiB */}];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetAvatarUploadFormResponse {
|
|||
|
|
oneof response {
|
|||
|
|
common.S3UploadForm avatar_upload_form = 1;
|
|||
|
|
|
|||
|
|
errors.FailedZkAuthentication invalid_credentials_presentation = 2 [(tag.reason) = "invalid_credentials_presentation"];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message ExtendAvatarTTLRequest {
|
|||
|
|
bytes avatar_credentials_presentation = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message ExtendAvatarTTLResponse {
|
|||
|
|
oneof response {
|
|||
|
|
// The avatar that was extended. May be used as a check against state de-synchronization.
|
|||
|
|
string path = 1;
|
|||
|
|
errors.FailedZkAuthentication invalid_credentials_presentation = 2 [(tag.reason) = "invalid_credentials_presentation"];
|
|||
|
|
// the identity does not have an active avatar
|
|||
|
|
errors.NotFound not_found = 3 [(tag.reason) = "no_active_avatar"];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message DeleteAvatarRequest {
|
|||
|
|
bytes avatar_credentials_presentation = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message DeleteAvatarResponse {
|
|||
|
|
oneof response {
|
|||
|
|
google.protobuf.Empty success = 1;
|
|||
|
|
errors.FailedZkAuthentication invalid_credentials_presentation = 2 [(tag.reason) = "invalid_credentials_presentation"];
|
|||
|
|
}
|
|||
|
|
}
|