derec_library/protocol/types.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4//! Protocol-layer types.
5//!
6//! Everything in this module is "post-pairing" — the channel and store
7//! shapes the orchestrator manages once a pair handshake completes. The
8//! primitives layer never touches these (it operates on raw wire bytes
9//! and the cross-layer [`crate::types::ChannelId`] / [`crate::types::SharedKey`]
10//! aliases).
11//!
12//! Re-exported at [`crate::protocol`] for ergonomic access, so callers
13//! can write `use derec_library::protocol::Channel;` rather than
14//! `use derec_library::protocol::types::Channel;`.
15
16use crate::types::ChannelId;
17use derec_cryptography::pairing::PairingSecretKeyMaterial;
18use derec_proto::ContactMessage;
19#[cfg(any(feature = "serde", target_arch = "wasm32"))]
20use serde::{Deserialize, Serialize};
21use zeroize::Zeroizing;
22
23/// Selects which channels to target for a discovery request.
24#[derive(Debug, Clone)]
25pub enum Target {
26 /// Send to all paired channels (most common case).
27 All,
28 /// Send to a single channel.
29 Single(ChannelId),
30 /// Send to a specific set of channels.
31 Many(Vec<ChannelId>),
32}
33
34/// Status of a channel in the protocol lifecycle.
35///
36/// Replica channels start as `Pending` after pairing completes and transition
37/// to `Paired` once fingerprint verification succeeds. Helper/Owner channels
38/// are `Paired` immediately after pairing.
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
40#[cfg_attr(
41 any(feature = "serde", target_arch = "wasm32"),
42 derive(Serialize, Deserialize)
43)]
44pub enum ChannelStatus {
45 /// Channel is awaiting fingerprint verification (replica only).
46 Pending,
47 /// Channel is fully paired and ready for protocol messages.
48 #[default]
49 Paired,
50}
51
52/// A channel — the post-pairing representation of a peer.
53///
54/// Stored by [`crate::protocol::DeRecChannelStore`] and returned by its
55/// `channels()` method.
56///
57/// `Serialize` / `Deserialize` are derived for the FFI and WASM bridges,
58/// which ship channels to host languages as JSON over the language
59/// boundary. Library consumers writing a Rust `DeRecChannelStore` see
60/// only the typed value and never observe the serde representation;
61/// the wire format is not part of the public API and may change
62/// independently. `#[serde(default)]` annotations let bridges decode
63/// legacy bytes that predate later-added fields without erroring.
64#[derive(Clone, Debug)]
65#[cfg_attr(
66 any(feature = "serde", target_arch = "wasm32"),
67 derive(Serialize, Deserialize)
68)]
69pub struct Channel {
70 /// Unique identifier for this channel.
71 pub id: ChannelId,
72 /// The peer's transport endpoint.
73 pub transport: derec_proto::TransportProtocol,
74 /// Application-level identity metadata for the peer on this channel.
75 ///
76 /// Free-form key/value pairs — the protocol treats this as opaque and
77 /// never inspects keys or values. Anything an app wants to remember
78 /// about *who* is on the other end (display name, account id, avatar
79 /// URI, ...) lives here. App-level identity logic (e.g. auto-linking
80 /// by display name) reads from this map; the protocol does not.
81 ///
82 /// On the initiator side, this is whatever the caller supplied when
83 /// starting [`crate::protocol::DeRecFlow::Pairing`]. On the responder
84 /// side, it is the peer's own `communication_info` extracted from the
85 /// wire pair-request — the same map that surfaces in
86 /// [`crate::protocol::DeRecEvent::PairingCompleted::peer_communication_info`].
87 #[cfg_attr(any(feature = "serde", target_arch = "wasm32"), serde(default))]
88 pub communication_info: std::collections::HashMap<String, String>,
89 /// Lifecycle status. Messages on `Pending` channels are ignored.
90 #[cfg_attr(any(feature = "serde", target_arch = "wasm32"), serde(default))]
91 pub status: ChannelStatus,
92 /// Unix timestamp (seconds) when the channel was created.
93 #[cfg_attr(any(feature = "serde", target_arch = "wasm32"), serde(default))]
94 pub created_at: u64,
95 /// This node's role on this channel, fixed at pairing time.
96 ///
97 /// The orchestrator enforces flow directionality against this value: an
98 /// `Owner` may initiate `ProtectSecret` / `VerifyShares` / `Discovery` /
99 /// `RecoverSecret`; a `Helper` may not. Inbound messages are gated the
100 /// other way around — a `StoreShareRequest` is only honored on a channel
101 /// where this node is the `Helper`, and so on.
102 pub role: derec_proto::SenderKind,
103 /// The peer's replica identity, populated only when `role` is
104 /// `ReplicaSource` or `ReplicaDestination`.
105 ///
106 /// Extracted from the peer's `derec.replica_id` entry in
107 /// `CommunicationInfo` during the pair handshake. `None` on
108 /// Helper/Owner channels and as a defensive default on
109 /// freshly-paired Replica channels where the peer did not advertise
110 /// one.
111 #[cfg_attr(any(feature = "serde", target_arch = "wasm32"), serde(default))]
112 pub replica_id: Option<u64>,
113}
114
115/// Per-helper metadata stored inside the secret bag for recovery.
116///
117/// Each entry records the pairing state of a Helper so that recovery can
118/// re-establish communication channels without external configuration.
119#[derive(Clone, PartialEq, ::prost::Message)]
120pub struct HelperInfo {
121 /// Unique channel identifier assigned during pairing.
122 #[prost(uint64, tag = "1")]
123 pub channel_id: u64,
124 /// The Helper's message endpoint URI.
125 #[prost(string, tag = "2")]
126 pub transport_uri: ::prost::alloc::string::String,
127 /// Symmetric key negotiated during pairing (32 bytes).
128 #[prost(bytes = "vec", tag = "4")]
129 pub shared_key: ::prost::alloc::vec::Vec<u8>,
130 /// App-level identity metadata for this helper. Free-form key/value
131 /// pairs — the protocol treats it as opaque, never inspects keys or
132 /// values, and copies it verbatim from [`Channel::communication_info`]
133 /// at protect-time. A recovering owner who decodes the bag can use
134 /// this to recognise each helper (e.g. by a `"name"` key the app set
135 /// on pairing).
136 ///
137 /// **Wire stability**: the now-removed `name: String` was previously at
138 /// tag 3. Using tag 5 lets prost silently drop the old `name` field
139 /// when decoding legacy bags (empty `communication_info`), and lets
140 /// older codebases silently drop this new field when decoding new
141 /// bags. Degraded but not broken in either direction.
142 #[prost(map = "string, string", tag = "5")]
143 pub communication_info:
144 ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>,
145}
146
147/// A single user-facing secret within the bag.
148///
149/// The Owner can store multiple logical secrets (credentials, keys, notes)
150/// inside a single secret bag. Each `UserSecret` is independently
151/// identifiable so the application can present, add, or remove individual
152/// entries while the protocol treats the entire bag as one opaque blob.
153#[derive(Clone, PartialEq, ::prost::Message)]
154pub struct UserSecret {
155 /// Application-defined identifier.
156 #[prost(bytes = "vec", tag = "1")]
157 pub id: ::prost::alloc::vec::Vec<u8>,
158 /// Human-readable label.
159 #[prost(string, tag = "2")]
160 pub name: ::prost::alloc::string::String,
161 /// Raw secret bytes.
162 #[prost(bytes = "vec", tag = "3")]
163 pub data: ::prost::alloc::vec::Vec<u8>,
164}
165
166/// Snapshot of the user-facing secret contents persisted by
167/// [`crate::protocol::DeRecUserSecretStore`] for one `secret_id`.
168///
169/// Written every time the application calls
170/// `start(FlowKind::ProtectSecret)`; read by the pair-completion
171/// auto-publish hook so a freshly-paired Helper or Replica receives the
172/// current state without an explicit re-publish from the app.
173#[derive(Clone, Debug, PartialEq)]
174pub struct UserSecrets {
175 /// Secret version this snapshot represents. Monotonically increasing
176 /// per `secret_id` — the protocol bumps it on every publish.
177 pub version: u32,
178 /// User-facing secret entries. Same wire shape as
179 /// [`Secret::secrets`].
180 pub secrets: Vec<UserSecret>,
181 /// Optional human-readable label for this version, forwarded to
182 /// helpers in `StoreShareRequest.description`.
183 pub description: Option<String>,
184 /// Owner-side cached replica composite for this version, populated
185 /// after the VSS split completes. Lets the Owner resume future
186 /// `ProtectSecret` rounds without re-deriving share material, and
187 /// surfaces under [`Secret::replicas`] on the next snapshot rebuild.
188 /// `None` when this `secret_id` has no replica setup (or before
189 /// the first sharing round commits).
190 pub replicas: Option<Replicas>,
191}
192
193/// Per-replica metadata stored inside the [`Secret`] — mirrors
194/// [`HelperInfo`] but for the replica role and carries the extra
195/// `replica_id` + `sender_kind` fields needed by the replica model.
196///
197/// **No per-pair key**: all replica channels for a given `secret_id`
198/// converge on a single group key (see [`ReplicaSecretPayload::shared_key`]
199/// for how that key is handed off to a new joiner). Each replica's
200/// `(secret_id, channel_id)` entry in
201/// [`crate::protocol::DeRecSecretStore`] holds that same group key, so
202/// any replica can address any other replica's peers by loading the
203/// channel key from its own secret store — this struct does not need to
204/// carry it.
205#[derive(Clone, PartialEq, ::prost::Message)]
206pub struct ReplicaInfo {
207 /// Channel identifier the originator uses to address this peer.
208 #[prost(uint64, tag = "1")]
209 pub channel_id: u64,
210 /// The peer's message endpoint URI.
211 #[prost(string, tag = "2")]
212 pub transport_uri: ::prost::alloc::string::String,
213 /// App-level identity metadata for this peer. Same opacity contract
214 /// as [`HelperInfo::communication_info`].
215 #[prost(map = "string, string", tag = "4")]
216 pub communication_info:
217 ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>,
218 /// The peer's `replica_id` — global stable identity of the replica
219 /// device, separate from the per-channel `channel_id`.
220 #[prost(uint64, tag = "5")]
221 pub replica_id: u64,
222 /// Raw `SenderKind` value the peer played in this pair (typically
223 /// `REPLICA_SOURCE` or `REPLICA_DESTINATION`). Carried for future
224 /// conflict-resolution flows; not used by the protocol layer today.
225 #[prost(int32, tag = "6")]
226 pub sender_kind: i32,
227}
228
229/// The protocol's `secret` — serialized into `DeRecSecret.secret_data`.
230///
231/// This is the actual payload that gets protobuf-encoded, then placed into
232/// the `secret_data` bytes field of the canonical `DeRecSecret` protobuf
233/// message before encryption and distribution. Matches the DeRec
234/// specification's `secret` term (distinct from a `UserSecret` entry,
235/// which is one application-defined item *inside* this struct).
236#[derive(Clone, PartialEq, ::prost::Message)]
237pub struct Secret {
238 /// Snapshot of all paired Helpers at the time of distribution.
239 #[prost(message, repeated, tag = "1")]
240 pub helpers: ::prost::alloc::vec::Vec<HelperInfo>,
241 /// The user-facing secrets the Owner wishes to protect.
242 #[prost(message, repeated, tag = "2")]
243 pub secrets: ::prost::alloc::vec::Vec<UserSecret>,
244 /// Replica composite: the destination peers, the per-helper share
245 /// map, and the group key. `None` when this `secret_id` has no
246 /// replica setup. See [`Replicas`] for field semantics.
247 #[prost(message, optional, tag = "3")]
248 pub replicas: ::core::option::Option<Replicas>,
249 /// The `replica_id` of the device that created or last updated this
250 /// version of the secret. Used by Destinations to attribute origin
251 /// and will drive future conflict-resolution logic.
252 #[prost(uint64, tag = "4")]
253 pub owner_replica_id: u64,
254}
255
256/// Replica composite carried inside [`Secret`] — the destination
257/// roster + the 32-byte group key shared by every replica channel.
258///
259/// The per-helper share map is *not* part of this composite: VSS
260/// shares are derived from the encoded `Secret` bytes and so cannot
261/// be embedded inside the `Secret` itself. The wire-level share map
262/// rides on [`ReplicaSecretPayload`] alongside the encoded `Secret`
263/// instead.
264///
265/// `shared_key` must be 32 bytes when [`Self::replicas`] is
266/// non-empty. The library enforces this invariant on the producer
267/// side during sharing round construction and on the consumer side in
268/// [`crate::protocol::DeRecProtocol::restore`].
269#[derive(Clone, PartialEq, ::prost::Message)]
270pub struct Replicas {
271 /// Snapshot of all paired Replica Destinations at protect time.
272 #[prost(message, repeated, tag = "1")]
273 pub replicas: ::prost::alloc::vec::Vec<ReplicaInfo>,
274 /// 32-byte replica group key.
275 #[prost(bytes = "vec", tag = "2")]
276 pub shared_key: ::prost::alloc::vec::Vec<u8>,
277}
278
279/// A single helper's share of the current secret bag — wire-pairs a
280/// `channel_id` with the serialized `CommittedDeRecShare` bytes that
281/// were sent to that helper. Part of [`ReplicaSecretPayload`].
282#[derive(Clone, PartialEq, ::prost::Message)]
283pub struct ChannelShare {
284 /// Channel id of the helper that holds this share.
285 #[prost(uint64, tag = "1")]
286 pub channel_id: u64,
287 /// Serialized `CommittedDeRecShare` bytes — the same payload the
288 /// helper received in their `StoreShareRequest`.
289 #[prost(bytes = "vec", tag = "2")]
290 pub committed_share: ::prost::alloc::vec::Vec<u8>,
291}
292
293/// The composite payload sent to each Replica Destination on a
294/// `ProtectSecret` round. Carries the full [`Secret`] plus the map of
295/// `(channel_id → committed_share)` for the same round, so the
296/// Destination can recover via either path — read the secret directly,
297/// or contact each helper using `secret.helpers[i].shared_key` and
298/// request their stored share.
299///
300/// # Group-key handover
301///
302/// All replica channels for a given `secret_id` converge on a single
303/// symmetric "group" key. The `shared_key` field carries that group key
304/// inside the encrypted payload **only** when the sender knows the
305/// receiver doesn't have it yet — i.e. on the first round to a newly
306/// paired Destination. Both sides swap their stored channel key
307/// (`(secret_id, channel_id)` in [`crate::protocol::DeRecSecretStore`])
308/// from the per-pair ephemeral handshake key to the group key:
309///
310/// - **Sender**: swap immediately after the request envelope is sent.
311/// The ack response from the new joiner will already be encrypted
312/// with the group key.
313/// - **Receiver**: swap before encrypting the ack response, so the
314/// ack uses the group key and matches what the sender expects.
315///
316/// On the first-ever replica pair, the group key is implicitly the
317/// pair-handshake key — `shared_key` is left empty, no swap happens,
318/// and the channel-key entry both sides already saved is the group key.
319#[derive(Clone, PartialEq, ::prost::Message)]
320pub struct ReplicaSecretPayload {
321 /// The full secret the sender is committing to this version.
322 #[prost(message, optional, tag = "1")]
323 pub secret: ::core::option::Option<Secret>,
324 /// One entry per helper that received a VSS share on this round.
325 #[prost(message, repeated, tag = "2")]
326 pub shares: ::prost::alloc::vec::Vec<ChannelShare>,
327 /// 32-byte replica-group key. Present only on the first-sync round
328 /// to a newly-paired Destination; empty on every subsequent round
329 /// (since the receiving channel already holds the group key) and
330 /// empty when the receiving Destination is the very first pair for
331 /// this `secret_id` (the pair-handshake key is implicitly the group
332 /// key). See type-level docs for the swap protocol.
333 #[prost(bytes = "vec", tag = "3")]
334 pub shared_key: ::prost::alloc::vec::Vec<u8>,
335}
336
337/// Kind of secret material stored by [`crate::protocol::DeRecSecretStore`].
338///
339/// Each variant has its own lifecycle (see per-variant docs). Used as the
340/// `kind` argument to [`crate::protocol::DeRecSecretStore::load`] and
341/// [`crate::protocol::DeRecSecretStore::remove`]; on
342/// [`crate::protocol::DeRecSecretStore::save`] the kind is inferred from
343/// the [`SecretValue`] variant and need not be passed.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum SecretKind {
346 /// The post-pairing symmetric channel key (see [`SecretValue::SharedKey`]).
347 SharedKey = 0,
348 /// The ephemeral ECIES / ML-KEM key material used during pairing.
349 PairingSecret = 1,
350 /// The initiator's [`ContactMessage`] stored transiently between
351 /// `start` and pairing completion. Removed once the shared key
352 /// is derived.
353 PairingContact = 2,
354}
355
356/// How [`crate::protocol::DeRecSecretStore::load_many`] handles channels
357/// with no stored secret of the requested [`SecretKind`].
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum MissingPolicy {
360 /// Silently drop missing channels from the returned vector.
361 ///
362 /// Use when missing entries are an expected outcome — e.g. a `Target::Many`
363 /// list that mixes paired and unpaired channels.
364 Skip,
365 /// Return [`crate::protocol::SecretStoreError::MissingEntries`] carrying
366 /// the channel ids that had no entry.
367 ///
368 /// Use when every input id is expected to have an entry — e.g. after
369 /// filtering to channels already known to
370 /// [`crate::protocol::DeRecChannelStore`]. A miss signals a cross-store
371 /// invariant violation.
372 Fail,
373}
374
375/// Opaque, serialized pairing key material as held by
376/// [`crate::protocol::DeRecSecretStore`] under [`SecretValue::PairingSecret`].
377///
378/// This is the store-boundary form of the ephemeral key pair the pairing
379/// handshake produces. The protocol deliberately exposes it as an opaque
380/// byte blob rather than a cryptographic type, so a store implementation can
381/// persist and reload it without depending on `derec-cryptography` or any
382/// serialization framework: call [`as_bytes`](Self::as_bytes) to obtain the
383/// bytes to persist on `save`, and hand the same bytes back to
384/// [`from_bytes`](Self::from_bytes) on `load`.
385///
386/// The byte layout is a library-internal detail, is not part of the public
387/// API, and may change between versions; treat the blob as opaque and never
388/// interpret it.
389///
390/// The bytes are held in [`zeroize::Zeroizing`] so the plaintext key material
391/// is wiped from memory on drop.
392#[derive(Clone)]
393pub struct PairingKeyMaterial(Zeroizing<Vec<u8>>);
394
395impl PairingKeyMaterial {
396 /// Wrap raw bytes previously obtained from [`as_bytes`](Self::as_bytes)
397 /// and persisted by a store.
398 pub fn from_bytes(bytes: Vec<u8>) -> Self {
399 Self(Zeroizing::new(bytes))
400 }
401
402 /// The opaque bytes to persist. Round-trips through
403 /// [`from_bytes`](Self::from_bytes).
404 pub fn as_bytes(&self) -> &[u8] {
405 self.0.as_slice()
406 }
407
408 /// Serialize live pairing secret key material into the store-boundary
409 /// form.
410 ///
411 /// Library-internal: the pairing handlers call this before handing the
412 /// value to the secret store, keeping the `ark-serialize` encoding an
413 /// implementation detail that never crosses the public API.
414 pub(crate) fn from_secret(material: &PairingSecretKeyMaterial) -> Self {
415 use ark_serialize::CanonicalSerialize as _;
416 let mut buf = Vec::with_capacity(material.compressed_size());
417 material
418 .serialize_compressed(&mut buf)
419 .expect("ark serialization of PairingSecretKeyMaterial is infallible");
420 Self(Zeroizing::new(buf))
421 }
422
423 /// Reconstruct live pairing secret key material from the store-boundary
424 /// form.
425 ///
426 /// Library-internal: the pairing handlers call this after loading the
427 /// value from the secret store. A decode failure means the persisted
428 /// bytes were corrupted or truncated, which is an internal invariant
429 /// violation rather than valid caller input.
430 pub(crate) fn to_secret(&self) -> crate::Result<PairingSecretKeyMaterial> {
431 use ark_serialize::CanonicalDeserialize as _;
432 PairingSecretKeyMaterial::deserialize_compressed(self.0.as_slice())
433 .map_err(|_| crate::Error::Invariant("stored PairingSecret bytes failed to decode"))
434 }
435}
436
437#[cfg(feature = "serde")]
438impl Serialize for PairingKeyMaterial {
439 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
440 where
441 S: serde::Serializer,
442 {
443 self.0.as_slice().serialize(serializer)
444 }
445}
446
447#[cfg(feature = "serde")]
448impl<'de> Deserialize<'de> for PairingKeyMaterial {
449 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
450 where
451 D: serde::Deserializer<'de>,
452 {
453 let bytes = Vec::<u8>::deserialize(deserializer)?;
454 Ok(Self(Zeroizing::new(bytes)))
455 }
456}
457
458/// Serde adapter for the prost [`ContactMessage`] carried by
459/// [`SecretValue::PairingContact`]. prost messages have no native serde
460/// support, so the value is (de)serialized through its canonical protobuf
461/// byte encoding.
462#[cfg(feature = "serde")]
463mod contact_serde {
464 use super::ContactMessage;
465 use prost::Message as _;
466 use serde::{Deserialize as _, Deserializer, Serialize as _, Serializer};
467
468 pub(super) fn serialize<S>(contact: &ContactMessage, serializer: S) -> Result<S::Ok, S::Error>
469 where
470 S: Serializer,
471 {
472 contact.encode_to_vec().serialize(serializer)
473 }
474
475 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<ContactMessage, D::Error>
476 where
477 D: Deserializer<'de>,
478 {
479 let bytes = Vec::<u8>::deserialize(deserializer)?;
480 ContactMessage::decode(bytes.as_slice()).map_err(serde::de::Error::custom)
481 }
482}
483
484/// The payload returned by [`crate::protocol::DeRecSecretStore::load`] and
485/// passed to [`crate::protocol::DeRecSecretStore::save`].
486///
487/// Variants are 1:1 with [`SecretKind`].
488///
489/// With the `serde` feature enabled, `Serialize` / `Deserialize` are
490/// derived so a store implementation can persist an entry with any serde
491/// format instead of hand-rolling a codec. This is an alternative to the
492/// byte-level accessors on the individual payloads (e.g.
493/// [`PairingKeyMaterial::as_bytes`] / [`PairingKeyMaterial::from_bytes`]);
494/// implementors pick whichever fits their backend, and consumers that do
495/// not use serde pay no dependency for it. The serde wire format is not
496/// part of the public API and may change independently.
497#[derive(Clone)]
498#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
499pub enum SecretValue {
500 /// The post-pairing symmetric channel key. Established by pairing and used
501 /// to authenticate and encrypt every subsequent message on the channel.
502 SharedKey(crate::types::SharedKey),
503 /// The ephemeral ECIES / ML-KEM key material created by `start` and
504 /// consumed when the pairing response arrives. Removed once the shared
505 /// key is derived. Held as an opaque [`PairingKeyMaterial`] blob so
506 /// store implementors never touch a cryptography primitive.
507 PairingSecret(PairingKeyMaterial),
508 /// The initiator's [`ContactMessage`], needed by
509 /// [`crate::primitives::pairing::response::process`] to derive the shared
510 /// key. Ephemeral — removed after pairing completes.
511 PairingContact(#[cfg_attr(feature = "serde", serde(with = "contact_serde"))] ContactMessage),
512}
513
514/// Tag identifying which kind of in-flight orchestrator state an entry in
515/// [`crate::protocol::DeRecStateStore`] holds. Used by
516/// [`crate::protocol::DeRecStateStore::load_all`] to filter by category.
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
518pub enum StateKind {
519 /// Outstanding [`derec_proto::VerifyShareRequestMessage`], one per
520 /// channel. Load-bearing for the replay-defence binding gate.
521 PendingVerification,
522 /// Recovery accumulator, one per `(secret_id, version)`. Holds every
523 /// [`derec_proto::GetShareResponseMessage`] received so far for that
524 /// reconstruction target.
525 PendingRecovery,
526 /// Outstanding unpair acknowledgement, one per channel. Carries the
527 /// `started_at` unix-seconds timestamp so the orchestrator can time
528 /// out unresponsive peers.
529 PendingUnpair,
530 /// Active sharing round. At most one entry exists per `secret_id`
531 /// (a new `start(ProtectSecret)` overwrites any prior round). Holds
532 /// the per-channel tallies (`pending` / `confirmed` / `failed`) and
533 /// the `started_at` timestamp used to time out unresponsive helpers.
534 SharingRound,
535}
536
537/// Secondary-key selector identifying a single row within a given
538/// [`StateKind`] under a `secret_id`. Passed to
539/// [`crate::protocol::DeRecStateStore::load`] and
540/// [`crate::protocol::DeRecStateStore::remove`].
541#[derive(Debug, Clone, PartialEq, Eq, Hash)]
542pub enum StateKey {
543 /// Row is scoped to one channel.
544 PendingVerification { channel_id: ChannelId },
545 /// Row is scoped to one reconstruction target.
546 PendingRecovery { version: u32 },
547 /// Row is scoped to one channel.
548 PendingUnpair { channel_id: ChannelId },
549 /// At most one row per `secret_id`. No secondary key.
550 SharingRound,
551}
552
553impl StateKey {
554 /// The [`StateKind`] this key selects. Used by store implementations
555 /// that persist rows in a `(secret_id, kind, secondary_key)` schema.
556 pub fn kind(&self) -> StateKind {
557 match self {
558 StateKey::PendingVerification { .. } => StateKind::PendingVerification,
559 StateKey::PendingRecovery { .. } => StateKind::PendingRecovery,
560 StateKey::PendingUnpair { .. } => StateKind::PendingUnpair,
561 StateKey::SharingRound => StateKind::SharingRound,
562 }
563 }
564}
565
566/// The payload of one row in the [`crate::protocol::DeRecStateStore`].
567///
568/// # Write pattern
569///
570/// The library treats [`crate::protocol::DeRecStateStore::save`] as
571/// **full-replacement upsert** — there is no per-item merge or append
572/// semantic at the store level. Accumulator-style state
573/// ([`StateItem::PendingRecovery`] and [`StateItem::SharingRound`]) grows
574/// via load-modify-save cycles from the library. Backends do not need to
575/// implement any append primitive; a naive replace-on-save is correct.
576#[derive(Debug, Clone)]
577pub enum StateItem {
578 /// The full outstanding [`derec_proto::VerifyShareRequestMessage`] the
579 /// orchestrator sent for this channel. Retained so the corresponding
580 /// inbound [`derec_proto::VerifyShareResponseMessage`] can be validated
581 /// against the exact `(nonce, secret_id, version)` triple that was
582 /// minted at request time.
583 ///
584 /// Overwritten in place by a subsequent `save` for the same
585 /// `(secret_id, channel_id)`; the most recent challenge wins.
586 PendingVerification {
587 channel_id: ChannelId,
588 request: derec_proto::VerifyShareRequestMessage,
589 },
590
591 /// Accumulator for one in-progress recovery target.
592 ///
593 /// The library writes this variant one share at a time as each inbound
594 /// [`derec_proto::GetShareResponseMessage`] arrives. The write sequence
595 /// under a single `(secret_id, version)` is:
596 ///
597 /// 1. First response arrives. Library calls `save` with a `shares`
598 /// vector containing exactly one element.
599 /// 2. Second response arrives. Library `load`s the accumulator,
600 /// appends the new share to the returned Vec, and `save`s the
601 /// grown Vec back.
602 /// 3. …repeat until threshold. On threshold met, library `remove`s
603 /// the accumulator.
604 ///
605 /// Implementations MUST accept `shares` vectors of any length,
606 /// including one. Every `save` replaces the stored value in place
607 /// with the caller-supplied Vec; no append primitive is required.
608 ///
609 /// # Concurrency
610 ///
611 /// See [`crate::protocol::DeRecStateStore`] for the multi-instance
612 /// concurrency contract. Concurrent inbound shares racing on the same
613 /// accumulator will clobber each other via a naive load-modify-save;
614 /// the application layer is responsible for serializing concurrent
615 /// `process()` calls that touch the same `(secret_id, version)` if
616 /// this matters.
617 PendingRecovery {
618 version: u32,
619 shares: Vec<derec_proto::GetShareResponseMessage>,
620 },
621
622 /// Outstanding unpair acknowledgement window. `started_at` is the
623 /// unix-seconds timestamp stamped when the request was sent; the
624 /// orchestrator sweeps expired entries via
625 /// [`crate::protocol::DeRecStateStore::load_all`].
626 PendingUnpair {
627 channel_id: ChannelId,
628 started_at: u64,
629 },
630
631 /// Active sharing round for `secret_id`. Created by
632 /// `start(ProtectSecret)` and cleared by the orchestrator once every
633 /// targeted helper has responded (confirmed, rejected, or timed
634 /// out). At most one entry exists per `secret_id`; a fresh
635 /// `start(ProtectSecret)` overwrites any prior in-flight round.
636 ///
637 /// `pending` / `confirmed` / `failed` partition the round's target
638 /// channels; the union is invariant across the round's lifetime.
639 /// `started_at` is the unix-seconds timestamp used to time out
640 /// unresponsive helpers.
641 SharingRound {
642 version: u32,
643 pending: std::collections::HashSet<ChannelId>,
644 confirmed: std::collections::HashSet<ChannelId>,
645 failed: std::collections::HashSet<ChannelId>,
646 started_at: u64,
647 },
648}
649
650impl StateItem {
651 /// The [`StateKind`] this item is an instance of.
652 pub fn kind(&self) -> StateKind {
653 match self {
654 StateItem::PendingVerification { .. } => StateKind::PendingVerification,
655 StateItem::PendingRecovery { .. } => StateKind::PendingRecovery,
656 StateItem::PendingUnpair { .. } => StateKind::PendingUnpair,
657 StateItem::SharingRound { .. } => StateKind::SharingRound,
658 }
659 }
660
661 /// The [`StateKey`] identifying this item within its `(secret_id, kind)`
662 /// partition. Convenience so callers don't have to hand-construct a
663 /// key that matches the payload.
664 pub fn key(&self) -> StateKey {
665 match self {
666 StateItem::PendingVerification { channel_id, .. } => StateKey::PendingVerification {
667 channel_id: *channel_id,
668 },
669 StateItem::PendingRecovery { version, .. } => {
670 StateKey::PendingRecovery { version: *version }
671 }
672 StateItem::PendingUnpair { channel_id, .. } => StateKey::PendingUnpair {
673 channel_id: *channel_id,
674 },
675 StateItem::SharingRound { .. } => StateKey::SharingRound,
676 }
677 }
678}
679
680/// A single stored share entry, fully self-describing.
681#[derive(Debug, Clone)]
682pub struct Share {
683 /// Numeric identifier of the secret this share belongs to.
684 pub secret_id: u64,
685 /// Version number of the secret.
686 pub version: u32,
687 /// Stable per-device identifier of the replica that produced this
688 /// share, copied from
689 /// [`derec_proto::StoreShareRequestMessage::replica_id`] when the
690 /// helper persisted the write.
691 ///
692 /// `None` when the writer was a non-replica `Owner`. `Some(id)` when
693 /// the writer was `ReplicaSource`. Two distinct replicas may produce
694 /// the same `(secret_id, channel_id, version)` independently
695 /// — see [`crate::protocol::DeRecShareStore::save`] for the
696 /// conceptual storage key and the disambiguation contract.
697 pub replica_id: Option<u64>,
698 /// Opaque protobuf bytes — see [`crate::protocol::DeRecShareStore`] for
699 /// the per-side format.
700 pub bytes: Vec<u8>,
701}
702
703#[cfg(all(test, feature = "serde"))]
704mod tests {
705 use super::*;
706
707 /// Every `SecretValue` variant round-trips through serde — the path a
708 /// store implementation takes when it opts for serde over the
709 /// byte-level accessors. Exercises the custom `PairingKeyMaterial`
710 /// impls and the prost `ContactMessage` adapter.
711 #[test]
712 fn secret_value_serde_round_trips_all_variants() {
713 let cases = [
714 SecretValue::SharedKey([7u8; 32]),
715 SecretValue::PairingSecret(PairingKeyMaterial::from_bytes(vec![1, 2, 3, 4, 5])),
716 SecretValue::PairingContact(ContactMessage {
717 nonce: 42,
718 ..Default::default()
719 }),
720 ];
721
722 for value in cases {
723 let json = serde_json::to_vec(&value).expect("serialize");
724 let decoded: SecretValue = serde_json::from_slice(&json).expect("deserialize");
725 match (&value, &decoded) {
726 (SecretValue::SharedKey(a), SecretValue::SharedKey(b)) => assert_eq!(a, b),
727 (SecretValue::PairingSecret(a), SecretValue::PairingSecret(b)) => {
728 assert_eq!(a.as_bytes(), b.as_bytes())
729 }
730 (SecretValue::PairingContact(a), SecretValue::PairingContact(b)) => {
731 assert_eq!(a, b)
732 }
733 _ => panic!("variant changed across serde round-trip"),
734 }
735 }
736 }
737
738 /// The serde form and the `from_bytes`/`as_bytes` form describe the
739 /// same opaque blob, so a value serialized one way decodes the other.
740 #[test]
741 fn pairing_key_material_serde_matches_byte_accessors() {
742 let material = PairingKeyMaterial::from_bytes(vec![9, 8, 7, 6]);
743 let json = serde_json::to_vec(&material).expect("serialize");
744 let decoded: PairingKeyMaterial = serde_json::from_slice(&json).expect("deserialize");
745 assert_eq!(material.as_bytes(), decoded.as_bytes());
746 }
747}