syntax = "proto3"; package v1sync; option go_package = "github.com/garethgeorge/backrest/gen/go/v1sync"; import "v1/config.proto"; import "v1/crypto.proto"; import "v1/restic.proto"; import "v1/service.proto"; import "v1/operations.proto"; import "types/value.proto"; import "google/protobuf/empty.proto"; import "google/api/annotations.proto"; import "google/protobuf/any.proto"; // BackrestSyncService provides methods to sync data between backrest instances. // This service provides its own authentication and authorization. service BackrestSyncService { rpc Sync(stream SyncStreamItem) returns (stream SyncStreamItem) {} } // BackrestSyncStateService provides methods to query the sync state of known hosts and clients. // This service should be served behind authentication and authorization. service BackrestSyncStateService { rpc GetPeerSyncStatesStream(SyncStateStreamRequest) returns (stream PeerState) {} // SetRemoteClientConfig pushes a config change to a connected authorized client peer. rpc SetRemoteClientConfig(SetRemoteClientConfigRequest) returns (SetRemoteClientConfigResponse) {} } message SyncStateStreamRequest { bool subscribe = 1; // If true, the stream will continue to send updates until cancelled. } message PeerState { string peer_instance_id = 1; string peer_keyid = 2; ConnectionState state = 3; string status_message = 4; repeated PlanMetadata known_plans = 5; // List of plan IDs that the peer has. repeated RepoMetadata known_repos = 6; // List of repo IDs that the peer has. RemoteConfig remote_config = 7; // The remote config of the peer, if available. int64 last_heartbeat_millis = 8; // The last time the peer sent a heartbeat, in milliseconds since epoch. } message AuthenticateRequest { v1.SignedMessage instance_id = 1; // The ID of the peer instance. } message GetOperationMetadataResponse { repeated int64 op_ids = 1; // The IDs of the operations. repeated int64 modnos = 2; // The modnos of the operations. } message LogDataEntry { string log_id = 1; // The ID of the log, only used for the first message in a log data stream. int64 owner_opid = 2; // The operation ID of the operation that owns this log data. int64 expiration_ts_unix = 3; // Unix timestamp in seconds when the log data expires. bytes chunk = 4; // The log data chunk, can be sent repeatedly, must be terminated by a packet with size = 0. } message SetAvailableResourcesRequest { repeated PlanMetadata repos = 1; // The repos that are available. repeated RepoMetadata plans = 2; // The plans that are available. } message RepoMetadata { string id = 1; string guid = 2; } message PlanMetadata { string id = 1; } enum ConnectionState { CONNECTION_STATE_UNKNOWN = 0; CONNECTION_STATE_PENDING = 1; CONNECTION_STATE_CONNECTED = 2; CONNECTION_STATE_DISCONNECTED = 3; CONNECTION_STATE_RETRY_WAIT = 4; CONNECTION_STATE_ERROR_AUTH = 10; CONNECTION_STATE_ERROR_PROTOCOL = 11; CONNECTION_STATE_ERROR_INTERNAL = 12; } message SetConfigRequest { repeated v1.Plan plans = 1; // The plans to set. repeated v1.Repo repos = 2; // The repos to set. repeated string repos_to_delete = 3; // The repo IDs to delete. repeated string plans_to_delete = 4; // The plan IDs to delete. } message SetRemoteClientConfigRequest { string peer_keyid = 1; // The key ID of the connected peer to push config to. repeated v1.Repo repos = 2; // Repos to create or update on the peer. repeated v1.Plan plans = 3; // Plans to create or update on the peer. repeated string repos_to_delete = 4; // Repo IDs to delete on the peer. repeated string plans_to_delete = 5; // Plan IDs to delete on the peer. } message SetRemoteClientConfigResponse {} message RemoteConfig { int32 modno = 1; // The modno of the config. int32 version = 2; // The storage version of the config. repeated v1.Repo repos = 3; repeated v1.Plan plans = 4; } message AuthorizationToken { v1.PublicKey public_key = 1; v1.SignedMessage instance_id = 2; // The ID of the peer instance. } message SyncStreamItem { oneof action { v1.SignedMessage signed_message = 1; SyncActionHandshake handshake = 3; // note: mostly deprecated, sent through headers rather than stream. SyncActionHeartbeat heartbeat = 4; SyncActionOperationManifest operation_manifest = 20; SyncActionReceiveOperations receive_operations = 21; SyncActionRequestOperationData request_operation_data = 22; SyncActionReceiveConfig receive_config = 23; SyncActionSetConfig set_config = 24; SyncActionRequestResources request_resources = 25; // request a list of available resources. Only used by the server. SyncActionReceiveResources receive_resources = 26; // receiving a list of available resources. SyncActionRequestLog request_log = 30; SyncActionReceiveLogData receive_log_data = 31; SyncActionThrottle throttle = 1000; SyncEstablishSharedSecret establish_shared_secret = 2; SyncActionEncrypted encrypted = 5; } // SyncActionHandshake is the first message sent by each peer over the // post-quantum encrypted channel. It carries the sender's long-term // ed25519 identity, its instance ID, and a single signature that binds // the identity to *this* transport session. // // The signature covers a domain-separated hash of: // "backrest-sync-handshake/v1\x00" // || protocol_version (8 bytes BE) // || LP(instance_id) // || LP(pairing_secret) // || LP(transport transcript) // // where LP(x) = 4-byte BE length prefix || x, and the transport transcript // is cryptoutil.TransportSession.Transcript() — a hash that commits to // the ephemeral KEM messages of this connection. // // The transcript binding is what defeats a MITM that completes a separate // KEM with each side: each leg has a different transcript, and the // legitimate peer's signature only commits to its own transcript, so the // attacker cannot forward a usable signature to either side. // // Receivers MUST recompute the transcript locally from their TransportSession // and reject the handshake if the signature does not verify against // public_key. There is no timestamp because freshness is provided by the // ephemeral KEM, not by clock comparison. message SyncActionHandshake { int64 protocol_version = 1; v1.PublicKey public_key = 2; // sender's long-term ed25519 identity string instance_id = 3; // covered by signature below string pairing_secret = 4; // optional pairing token; covered by signature below bytes signature = 5; // ed25519(public_key, H(handshake bind input)) } // SyncActionEncrypted wraps an encrypted SyncStreamItem. // After the post-quantum KEM handshake, all subsequent messages are sent // inside this envelope. message SyncActionEncrypted { bytes nonce = 1; // 12-byte GCM nonce bytes ciphertext = 2; // AES-256-GCM(serialized SyncStreamItem) } // SyncActionHeartbeat is sent periodically to keep the connection alive. message SyncActionHeartbeat {} message SyncActionReceiveConfig { RemoteConfig config = 1; } message SyncActionSetConfig { repeated v1.Repo repos = 1; repeated v1.Plan plans = 2; repeated string repos_to_delete = 3; repeated string plans_to_delete = 4; } message SyncActionRequestResources {} message SyncActionReceiveResources { repeated RepoMetadata repos = 1; repeated PlanMetadata plans = 2; } message SyncActionConnectRepo { string repo_id = 1; } enum RepoConnectionState { CONNECTION_STATE_UNKNOWN = 0; CONNECTION_STATE_PENDING = 1; // queried, response not yet received. CONNECTION_STATE_CONNECTED = 2; CONNECTION_STATE_UNAUTHORIZED = 3; CONNECTION_STATE_NOT_FOUND = 4; } message SyncActionOperationManifest { repeated int64 op_ids = 1; repeated int64 modnos = 2; } message SyncActionRequestOperationData { repeated int64 op_ids = 1; } message SyncActionReceiveOperations { v1.OperationEvent event = 1; } message SyncActionRequestLog { string log_id = 1; } message SyncActionReceiveLogData { string log_id = 1; // Required only for first message in a log data stream. int64 owner_opid = 2; // The operation ID of the operation that owns this log data. int64 expiration_ts_unix = 3; // Unix timestamp in seconds when the log data expires. // Can be sent repeatedly, must be terminated by a packet with size = 0. bytes chunk = 4; // If set, indicates an error occurred while fetching the log data. string error_message = 5; } message SyncActionThrottle { int64 delay_ms = 1; } // SyncEstablishSharedSecret is exchanged immediately after the connection // is opened. The initiator (client) sends kem_public_key. The responder // (server) replies with kem_encapsulation. Both sides then derive a shared // AES-256-GCM session key via the HPKE Export interface. All subsequent // messages must be wrapped in SyncActionEncrypted. // // The KEM is the post-quantum hybrid ML-KEM-1024 + ECDH-P384 (HPKE // ciphersuite ML-KEM-1024-P384 / KEM ID 0x0050, RFC 9180 + the IETF hybrid // KEM drafts). KDF is HKDF-SHA256, AEAD is AES-256-GCM. Peers must use // protocol_version=1; mismatched versions abort the connection. message SyncEstablishSharedSecret { uint32 protocol_version = 1; // current: 1 bytes kem_public_key = 2; // set by initiator bytes kem_encapsulation = 3; // set by responder } }