/* * Copyright 2024 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ syntax = "proto3"; option java_multiple_files = true; package org.signal.chat.backup; 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"; // Service for backup operations that require account authentication. // // Most actual backup operations operate on the backup-id and cannot be linked // to the caller's account, but setting up anonymous credentials and changing // backup tier requires account authentication. service Backups { option (require.auth) = AUTH_ONLY_AUTHENTICATED; // Set (blinded) backup-id(s) for the account. // // Each account may have a single active backup-id for each credential type // that can be used to store and retrieve backups. Once the backup-id is set, // BackupAuthCredentials can be generated using GetBackupAuthCredentials. // // The blinded backup-id and the key-pair used to blind it must be derived // from a recoverable secret. // // At least one of the credential types must be set on the request. // Only the primary device can set a blinded backup-id. rpc SetBackupId(SetBackupIdRequest) returns (SetBackupIdResponse) {} // Redeem a receipt acquired from /v1/subscription/{subscriberId}/receipt_credentials // to mark the account as eligible for the paid backup tier. // // After successful redemption, subsequent requests to // GetBackupAuthCredentials will return credentials with the level on the // provided receipt until the expiration time on the receipt. rpc RedeemReceipt(RedeemReceiptRequest) returns (RedeemReceiptResponse) {} // After setting a blinded backup-id with PUT /v1/archives/, this fetches // credentials that can be used to perform operations against that backup-id. // Clients may (and should) request up to 7 days of credentials at a time. // // The redemption_start and redemption_end seconds must be UTC day aligned, and // must not span more than 7 days. // // Each credential contains a receipt level which indicates the backup level // the credential is good for. If the account has paid backup access that // expires at some point in the provided redemption window, credentials with // redemption times after the expiration may be on a lower backup level. // // Clients must validate the receipt level on the credential matches a known // receipt level before using it. rpc GetBackupAuthCredentials(GetBackupAuthCredentialsRequest) returns (GetBackupAuthCredentialsResponse) {} } message SetBackupIdRequest { // A BackupAuthCredentialRequest containing a blinded encrypted backup-id, // encoded in standard padded base64. This backup-id should be used for // message backups only, and must have the message backup type set on the // credential. If absent, the message credential request will not be updated. bytes messages_backup_auth_credential_request = 1; // A BackupAuthCredentialRequest containing a blinded encrypted backup-id, // encoded in standard padded base64. This backup-id should be used for // media only, and must have the media type set on the credential. If absent, // the media credential request will not be updated. bytes media_backup_auth_credential_request = 2; } message SetBackupIdResponse {} message RedeemReceiptRequest { // Presentation for a previously acquired receipt, serialized with libsignal bytes presentation = 1; } message RedeemReceiptResponse { oneof response { // The receipt was successfully redeemed google.protobuf.Empty success = 1; // The target account does not have a backup-id commitment errors.FailedPrecondition account_missing_commitment = 2 [(tag.reason) = "account_missing_commitment"]; // The provided receipt presentation was malformed or expired errors.FailedPrecondition invalid_receipt = 3 [(tag.reason) = "invalid_receipt"]; } } message GetBackupAuthCredentialsRequest { // The redemption time for the first credential. This must be a day-aligned // seconds since epoch in UTC. int64 redemption_start = 1 [(require.range).min = 1]; // The redemption time for the last credential. This must be a day-aligned // seconds since epoch in UTC. The span between redemptionStart and // redemptionEnd must not exceed 7 days. int64 redemption_stop = 2 [(require.range).min = 1]; } message GetBackupAuthCredentialsResponse { message Credentials { // The requested message backup ZkCredentials indexed by the start of their // validity period. The smallest key should be for the requested // redemption_start, the largest for the requested redemption_end. map message_credentials = 1; // The requested media backup ZkCredentials indexed by the start of their // validity period. The smallest key should be for the requested // redemption_start, the largest for the requested redemption_end. map media_credentials = 2; } // The requested credentials. If absent, there was no existing blinded // backup id associated with the provided account. Credentials credentials = 1; } // Service for backup operations with anonymous credentials // // This service never requires account authentication. It instead requires a // backup-id authenticated with an anonymous credential that cannot be linked // to the account. // // To register an anonymous credential: // // 1. Set a backup-id on the authenticated channel via Backups::SetBackupId // 2. Retrieve BackupAuthCredentials via Backups::GetBackupAuthCredentials // 3. Generate a key pair and set the public key via // BackupsAnonymous::SetPublicKey // // Unless otherwise noted, requests for this service require a // SignedPresentation, which includes: // // - a presentation generated from a BackupAuthCredential issued by // GetBackupAuthCredentials // - a signature of that presentation using the private key of a key pair // previously set with SetPublicKey. service BackupsAnonymous { option (require.auth) = AUTH_ONLY_ANONYMOUS; // Retrieve credentials used to read objects stored on the backup cdn rpc GetCdnCredentials(GetCdnCredentialsRequest) returns (GetCdnCredentialsResponse) {} // Retrieve credentials used to interact with the SecureValueRecoveryB service rpc GetSvrBCredentials(GetSvrBCredentialsRequest) returns (GetSvrBCredentialsResponse) {} // Retrieve information about the currently stored message backup rpc GetMessageBackupInfo(GetBackupInfoRequest) returns (GetMessageBackupInfoResponse) {} // Retrieve information about the currently stored media backup rpc GetMediaBackupInfo(GetBackupInfoRequest) returns (GetMediaBackupInfoResponse) {} // Permanently set the public key of an ED25519 key-pair for the backup-id. // All requests (including this one!) must sign their BackupAuthCredential // presentations with the private key corresponding to the provided public key. rpc SetPublicKey(SetPublicKeyRequest) returns (SetPublicKeyResponse) {} // Refresh the backup, indicating that the backup is still active. Clients // must periodically upload new backups or perform a refresh. If a backup has // not been active for 30 days, it may be deleted. rpc Refresh(RefreshRequest) returns (RefreshResponse) {} // Retrieve an upload form that can be used to perform a resumable upload rpc GetUploadForm(GetUploadFormRequest) returns (GetUploadFormResponse) {} // Copy and re-encrypt media from the attachments cdn into the backup cdn. // The original, already encrypted, attachments will be encrypted with the // provided key material before being copied. // // The copy operation is not atomic and responses will be returned as copy // operations complete with detailed information about the outcome. If an // error is encountered, not all requests may be reflected in the responses. // // On retries, a particular destination media id must not be reused with a // different source media id or different encryption parameters. // // The response stream may be closed with STREAM_CLOSED error reason. In this // case, a BackupStreamClosed message will be present in the error details. rpc CopyMedia(CopyMediaRequest) returns (stream CopyMediaResponse) {} // Retrieve a page of media objects stored for this backup-id. A client may // have previously stored media objects that are no longer referenced in their // current backup. To reclaim storage space used by these orphaned objects, // perform a list operation and remove any unreferenced media objects // via DeleteMedia. rpc ListMedia(ListMediaRequest) returns (ListMediaResponse) {} // Delete media objects stored with this backup-id. Streams the locations of // media items back when the item has successfully been removed. // // The response stream may be closed with STREAM_CLOSED error reason. In this // case, a BackupStreamClosed message will be present in the error details. rpc DeleteMedia(DeleteMediaRequest) returns (stream DeleteMediaResponse) {} // Delete all backup metadata, objects, and stored public key. To use // backups again, a public key must be resupplied. rpc DeleteAll(DeleteAllRequest) returns (DeleteAllResponse) {} } message SignedPresentation { // Presentation of a BackupAuthCredential previously retrieved from // GetBackupAuthCredentials on the authenticated channel 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 [(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 [(require.nonEmpty) = true]; } message SetPublicKeyResponse { oneof response { // The public key was successfully set google.protobuf.Empty success = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. // // This may also be returned if there was an existing public key and the // provided public key did not match. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message GetCdnCredentialsRequest { SignedPresentation signed_presentation = 1; uint32 cdn = 2; } message GetCdnCredentialsResponse { message CdnCredentials { map headers = 1; } oneof response { // Headers to include with requests to the read from the backup CDN. Includes // time limited read-only credentials. CdnCredentials cdn_credentials = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message GetSvrBCredentialsRequest { SignedPresentation signed_presentation = 1; } message GetSvrBCredentialsResponse { message SvrBCredentials { // A username that can be presented to authenticate with SVRB string username = 1; // A password that can be presented to authenticate with SVRB string password = 2; } oneof response { SvrBCredentials svrb_credentials = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message GetBackupInfoRequest { SignedPresentation signed_presentation = 1; } message GetMessageBackupInfoResponse { message MessageBackupInfo { // The base directory of your backup data on the cdn. Always non-empty, even // if a backup has not actually been stored to the cdn. If a backup was // previously uploaded and has not expired, it can be found in the returned // cdn at /backup_dir/backup_name. string backup_dir = 1; // The CDN type where the message backup is stored. Media may be stored // elsewhere. uint32 cdn = 2; // The location of the message backup on the cdn. Always non-empty, even // if a backup has not actually been stored to the cdn. If a backup was // previously uploaded and has not expired, it can be found in the returned // cdn at /backup_dir/backup_name. string backup_name = 3; } oneof response { MessageBackupInfo backup_info = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message GetMediaBackupInfoResponse { message MediaBackupInfo { // The base directory of your backup data on the cdn. Always non-empty, even // if no media has been stored to the cdn or the credential is for a tier // that does not support media. string backup_dir = 1; // The prefix path component for media objects on a cdn. Stored media for a // media_id can be found at /backup_dir/media_dir/media_id, where the // media_id is encoded in unpadded url-safe base64. Always non-empty, even // if no media has been stored to the cdn or the credential is for a tier // that does not support media. string media_dir = 2; // The amount of space used to store media uint64 used_space = 3; } oneof response { MediaBackupInfo backup_info = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message RefreshRequest { SignedPresentation signed_presentation = 1; } message RefreshResponse { oneof response { // The backup was successfully refreshed google.protobuf.Empty success = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message GetUploadFormRequest { SignedPresentation signed_presentation = 1; message MessagesUploadType {} message MediaUploadType {} oneof upload_type { // Retrieve an upload form that can be used to perform a resumable upload of // a message backup. The finished upload will be available on the backup cdn. MessagesUploadType messages = 2; // Retrieve an upload form for a temporary location that can be used to // perform a resumable upload of an attachment. After uploading, the // attachment can be copied into the backup via CopyMedia. // // Behaves identically to the account authenticated version at /attachments. MediaUploadType media = 3; } // The length of the attachment for the requested upload form. Uploads // performed with this form will be limited to the provided length. uint64 uploadLength = 4 [(require.range) = {min: 1}]; } message GetUploadFormResponse { oneof response { common.UploadForm upload_form = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; // The request size was larger than the maximum supported upload size. The // maximum upload size is subject to change and is governed by // `global.attachments.maxBytes` errors.FailedPrecondition exceeds_max_upload_length = 3 [(tag.reason) = "oversize_upload"]; } } message CopyMediaItem { // The attachment cdn of the object to copy into the backup uint32 source_attachment_cdn = 1 [(require.range).min = 1, (require.range).max = 3]; // The attachment key of the object to copy into the backup string source_key = 2 [(require.nonEmpty) = true, (require.base64url) = true]; // The length of the source attachment before the encryption applied by the // copy operation uint64 object_length = 3; // media_id to copy on to the backup CDN bytes media_id = 4 [(require.exactlySize) = 15]; // A 32-byte key for the MAC bytes hmac_key = 5 [(require.exactlySize) = 32]; // A 32-byte encryption key for AES bytes encryption_key = 6 [(require.exactlySize) = 32]; } message CopyMediaRequest { SignedPresentation signed_presentation = 1; // Items to copy repeated CopyMediaItem items = 2 [(require.size) = {min: 1, max: 1000}]; } message CopyMediaResponse { message SourceNotFound {} message WrongSourceLength {} message OutOfSpace {} message CopySuccess { // The backup cdn where this media object is stored uint32 cdn = 1; } // The 15-byte media_id from the corresponding CopyMediaItem in the request bytes media_id = 1; oneof response { // The media item was successfully copied into the backup CopySuccess success = 2; // The source object was not found SourceNotFound source_not_found = 3 [(tag.reason) = "source_not_found"]; // The provided object length was incorrect WrongSourceLength wrong_source_length = 4 [(tag.reason) = "wrong_source_length"]; // All media capacity has been consumed. Free some space to continue. OutOfSpace out_of_space = 5 [(tag.reason) = "out_of_space"]; } } // The reason why a media stream RPC is being prematurely closed by the server. message BackupStreamClosed { oneof reason { // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 1 [(tag.reason) = "failed_authentication"]; } } message ListMediaRequest { SignedPresentation signed_presentation = 1; // A cursor returned by a previous call to ListMedia, absent on the first call optional string cursor = 2; // If provided, the maximum number of entries to return in a page. If absent, // a server-chosen default is used. optional uint32 limit = 3 [(require.range) = {min: 1, max: 10000}]; } message ListMediaResponse { message ListEntry { // The backup cdn where this media object is stored uint32 cdn = 1; // The media_id of the object bytes media_id = 2; // The length of the object in bytes uint64 length = 3; } message ListResult { // A page of media objects stored for this backup ID repeated ListEntry page = 1; // The base directory of the backup data on the cdn. The stored media can be // found at /backup_dir/media_dir/media_id, where the media_id is encoded with // unpadded url-safe base64. string backup_dir = 2; // The prefix path component for the media objects. The stored media for // media_id can be found at /backup_dir/media_dir/media_id, where the media_id // is encoded with unpadded url-safe base64. string media_dir = 3; // If set, the cursor value to pass to the next list request to continue // listing. If absent, all objects have been listed optional string cursor = 4; } oneof response { ListResult list_result = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message DeleteAllRequest { SignedPresentation signed_presentation = 1; } message DeleteAllResponse { oneof response { // The backup was successfully scheduled for deletion google.protobuf.Empty success = 1; // The provided backup auth credential presentation could not be // authenticated. Either, the presentation could not be verified, or // the public key signature was invalid, or there is no backup associated // with the backup-id in the presentation. errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"]; } } message DeleteMediaItem { // The backup cdn where this media object is stored uint32 cdn = 1; // The media_id of the object to delete bytes media_id = 2 [(require.exactlySize) = 15]; } message DeleteMediaRequest { SignedPresentation signed_presentation = 1; repeated DeleteMediaItem items = 2 [(require.size) = {min: 1, max: 1000}]; } message DeleteMediaResponse { DeleteMediaItem deleted_item = 1; }