Skip to main content

derec_library/protocol/
traits.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4use super::error::{ChannelStoreError, SecretStoreError, ShareStoreError, StateStoreError};
5use crate::Result;
6use crate::protocol::types::{
7    Channel, MissingPolicy, SecretKind, SecretValue, Share, StateItem, StateKey, StateKind,
8    UserSecrets,
9};
10use crate::types::ChannelId;
11use derec_proto::TransportProtocol;
12use std::{future::Future, pin::Pin};
13
14/// Type-erased future returned by [`DeRecSecretStore`] methods.
15///
16/// `Send` on native targets so multi-threaded executors (e.g. `tokio::spawn`)
17/// can take it; under the `ffi` feature or `wasm32` the `Send` bound is
18/// dropped because callbacks cross an FFI boundary or run in a
19/// single-threaded host. Sync backends can return
20/// `Box::pin(std::future::ready(...))` at zero cost.
21#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
22pub type SecretStoreFuture<'a, T> =
23    Pin<Box<dyn Future<Output = std::result::Result<T, SecretStoreError>> + 'a>>;
24/// Type-erased future returned by [`DeRecSecretStore`] methods.
25///
26/// `Send` on native targets so multi-threaded executors (e.g. `tokio::spawn`)
27/// can take it; under the `ffi` feature or `wasm32` the `Send` bound is
28/// dropped because callbacks cross an FFI boundary or run in a
29/// single-threaded host. Sync backends can return
30/// `Box::pin(std::future::ready(...))` at zero cost.
31#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
32pub type SecretStoreFuture<'a, T> =
33    Pin<Box<dyn Future<Output = std::result::Result<T, SecretStoreError>> + Send + 'a>>;
34
35/// Type-erased future returned by [`DeRecChannelStore`] methods. See
36/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
37#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
38pub type ChannelStoreFuture<'a, T> =
39    Pin<Box<dyn Future<Output = std::result::Result<T, ChannelStoreError>> + 'a>>;
40/// Type-erased future returned by [`DeRecChannelStore`] methods. See
41/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
42#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
43pub type ChannelStoreFuture<'a, T> =
44    Pin<Box<dyn Future<Output = std::result::Result<T, ChannelStoreError>> + Send + 'a>>;
45
46/// Type-erased future returned by [`DeRecShareStore`] methods. See
47/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
48#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
49pub type ShareStoreFuture<'a, T> =
50    Pin<Box<dyn Future<Output = std::result::Result<T, ShareStoreError>> + 'a>>;
51/// Type-erased future returned by [`DeRecShareStore`] methods. See
52/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
53#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
54pub type ShareStoreFuture<'a, T> =
55    Pin<Box<dyn Future<Output = std::result::Result<T, ShareStoreError>> + Send + 'a>>;
56
57/// Type-erased future returned by [`DeRecTransport::send`]. See
58/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
59#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
60pub type TransportFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
61/// Type-erased future returned by [`DeRecTransport::send`]. See
62/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
63#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
64pub type TransportFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
65
66/// Type-erased future returned by [`DeRecStateStore`] methods. See
67/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
68#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
69pub type StateStoreFuture<'a, T> =
70    Pin<Box<dyn Future<Output = std::result::Result<T, StateStoreError>> + 'a>>;
71/// Type-erased future returned by [`DeRecStateStore`] methods. See
72/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
73#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
74pub type StateStoreFuture<'a, T> =
75    Pin<Box<dyn Future<Output = std::result::Result<T, StateStoreError>> + Send + 'a>>;
76
77/// Keychain-grade storage for the protocol's per-channel cryptographic state.
78///
79/// Holds three kinds of material (see [`SecretKind`]):
80/// [`SecretKind::SharedKey`] and [`SecretKind::PairingSecret`] are
81/// sensitive — implementations should persist them with keychain-grade
82/// protection. [`SecretKind::PairingContact`] is a transient public-key blob
83/// that only needs durable storage.
84///
85/// # VSS guarantee
86///
87/// Individual Verifiable Secret Sharing shares reveal **zero** information
88/// about the original secret (information-theoretic security), so share
89/// storage does **not** require this trait.
90///
91/// # Executor independence
92///
93/// Methods return [`SecretStoreFuture`] — a type-erased [`std::future::Future`]
94/// that any executor can poll. Sync implementations return
95/// `Box::pin(std::future::ready(...))` at zero cost; async implementations
96/// return `Box::pin(async move { ... })`. No runtime is prescribed; see
97/// [`SecretStoreFuture`] for the per-target `Send` rules.
98///
99/// # Concurrency
100///
101/// The protocol holds each store by `&mut Self`, so implementations never
102/// see overlapping calls and need no internal synchronization.
103pub trait DeRecSecretStore {
104    /// Load a secret for the given `(secret_id, channel_id)` pair.
105    ///
106    /// `secret_id` partitions storage so a single backend can serve many
107    /// secrets on the same device (Owner of N secrets, or Helper for N
108    /// Owners). Returns `Ok(None)` when no entry of the requested
109    /// [`SecretKind`] exists for this partition key.
110    fn load(
111        &self,
112        secret_id: u64,
113        channel_id: ChannelId,
114        kind: SecretKind,
115    ) -> SecretStoreFuture<'_, Option<SecretValue>>;
116
117    /// Load secrets of the same [`SecretKind`] for several channels in
118    /// one call, scoped to `secret_id`.
119    ///
120    /// Used by the [`crate::protocol`] orchestrator when broadcasting a
121    /// request (discovery, recovery, verification, sharing, unpairing) to
122    /// keep the per-broadcast roundtrip count constant instead of linear
123    /// in the number of paired channels.
124    fn load_many(
125        &self,
126        secret_id: u64,
127        channel_ids: &[ChannelId],
128        kind: SecretKind,
129        missing_policy: MissingPolicy,
130    ) -> SecretStoreFuture<'_, Vec<(ChannelId, SecretValue)>>;
131
132    /// Persist a secret for the given `(secret_id, channel_id)` pair.
133    ///
134    /// The [`SecretKind`] is derived from the [`SecretValue`] variant.
135    /// An existing entry of the same kind under the same partition is
136    /// silently overwritten.
137    fn save(
138        &mut self,
139        secret_id: u64,
140        channel_id: ChannelId,
141        value: SecretValue,
142    ) -> SecretStoreFuture<'_, ()>;
143
144    /// Remove a secret for the given `(secret_id, channel_id)` pair.
145    /// Idempotent: removing a non-existent entry is `Ok(())`.
146    fn remove(
147        &mut self,
148        secret_id: u64,
149        channel_id: ChannelId,
150        kind: SecretKind,
151    ) -> SecretStoreFuture<'_, ()>;
152}
153
154/// Storage backend for paired channels.
155///
156/// A [`Channel`] is the post-pairing representation of a peer relationship,
157/// retaining only the fields needed for ongoing protocol operations — see
158/// [`Channel`] for the per-field documentation. The full
159/// [`derec_proto::ContactMessage`] — which carries ephemeral cryptographic
160/// material — is discarded after pairing.
161///
162/// # Implementor notes
163///
164/// - [`load`](DeRecChannelStore::load) returns `Ok(None)` when no channel
165///   exists for the given ID.
166/// - [`save`](DeRecChannelStore::save) silently replaces any previously stored
167///   channel with the same ID.
168///
169/// # Channel linking
170///
171/// The channel store also owns the **channel-link graph**: a record that two
172/// channels belong to the same Owner identity (e.g. after a recovery
173/// re-pairing). [`link_channel`](DeRecChannelStore::link_channel) records one
174/// undirected, idempotent, transitive edge;
175/// [`linked_channels`](DeRecChannelStore::linked_channels) returns a channel's
176/// whole connected component. Linking moves no share data — it is pure
177/// relationship metadata. Recovery/discovery resolves the linked set here, then
178/// loads the corresponding shares via [`DeRecShareStore::load_many`].
179///
180/// # Executor independence
181///
182/// Same as [`DeRecSecretStore`]; methods return [`ChannelStoreFuture`].
183pub trait DeRecChannelStore {
184    /// Load the [`Channel`] for `(secret_id, channel_id)`.
185    ///
186    /// `secret_id` partitions storage so one backend can serve many
187    /// secrets on the same device. Returns `Ok(None)` when no channel
188    /// exists for this partition key.
189    fn load(
190        &self,
191        secret_id: u64,
192        channel_id: ChannelId,
193    ) -> ChannelStoreFuture<'_, Option<Channel>>;
194
195    /// Persist a [`Channel`] under `secret_id`. The channel ID is taken
196    /// from [`Channel::id`]. Replaces any previously stored channel for
197    /// the same `(secret_id, channel_id)`.
198    fn save(&mut self, secret_id: u64, channel: Channel) -> ChannelStoreFuture<'_, ()>;
199
200    /// Remove the channel for `(secret_id, channel_id)`. Returns `true`
201    /// when an entry was removed, `false` when none existed.
202    fn remove(
203        &mut self,
204        secret_id: u64,
205        channel_id: ChannelId,
206    ) -> ChannelStoreFuture<'_, bool>;
207
208    /// Return every channel stored under `secret_id`. Used by the
209    /// protocol to enumerate paired peers when building the secret bag
210    /// and when fanning out broadcast flows.
211    fn channels(&self, secret_id: u64) -> ChannelStoreFuture<'_, Vec<Channel>>;
212
213    /// Link two channels under `secret_id` as belonging to the same
214    /// Owner identity. The relation is **undirected, idempotent, and
215    /// transitive** within the partition.
216    fn link_channel(
217        &mut self,
218        secret_id: u64,
219        a: ChannelId,
220        b: ChannelId,
221    ) -> ChannelStoreFuture<'_, ()>;
222
223    /// Return the full set of channels linked to `channel_id` under
224    /// `secret_id`, **including `channel_id` itself**. An unlinked
225    /// channel returns `[channel_id]`.
226    fn linked_channels(
227        &self,
228        secret_id: u64,
229        channel_id: ChannelId,
230    ) -> ChannelStoreFuture<'_, Vec<ChannelId>>;
231}
232
233/// Storage backend for secret shares.
234///
235/// Each entry is opaque protobuf bytes keyed by `(channel_id, secret_id, version)`.
236/// The byte format depends on which side stored it; the store itself never
237/// decodes:
238///
239/// - **Helper** stores the encoded [`derec_proto::StoreShareRequestMessage`]
240///   received from the Owner. Recovery returns this whole message to the
241///   library, and verification derives the share content from
242///   `StoreShareRequestMessage.share`.
243/// - **Owner** stores the encoded [`derec_proto::CommittedDeRecShare`] it
244///   sent to each helper, so that the verification handler can replay the
245///   commitment when validating each helper's response.
246///
247/// # Relation to channel linking
248///
249/// This store is a **pure keyed store** — it never sees the channel-link
250/// graph. Linking lives in [`DeRecChannelStore`]. Callers that need shares
251/// across linked channels resolve the channel set via
252/// [`DeRecChannelStore::linked_channels`] first, then pass it to
253/// [`load_many`](DeRecShareStore::load_many).
254///
255/// # Why `secret_id` is required on filtered loads
256///
257/// Versions are namespaced by `secret_id`: the same `version` number can
258/// legitimately exist for two different secrets (e.g. a helper holds v1 from
259/// owner A and v1 from owner B). A version-only query would conflate them, so
260/// [`load`](DeRecShareStore::load) and
261/// [`load_many`](DeRecShareStore::load_many) both require `secret_id`.
262/// [`load_all`](DeRecShareStore::load_all) — the lone exception — exists
263/// for **discovery**, which by definition enumerates what's stored before any
264/// `secret_id` is known.
265///
266/// # Executor independence
267///
268/// Same as [`DeRecSecretStore`]; methods return [`ShareStoreFuture`].
269pub trait DeRecShareStore {
270    /// Load shares stored for `(secret_id, channel_id)`.
271    ///
272    /// - **Specific versions**: pass the versions you need in `versions`.
273    ///   Missing versions are silently skipped.
274    /// - **All versions**: pass an empty slice.
275    fn load(
276        &self,
277        secret_id: u64,
278        channel_id: ChannelId,
279        versions: &[u32],
280    ) -> ShareStoreFuture<'_, Vec<Share>>;
281
282    /// Load shares for several channels in one call, scoped to
283    /// `secret_id`. Recovery uses this with the set returned by
284    /// [`DeRecChannelStore::linked_channels`], so it is a single
285    /// round-trip regardless of how many channels are linked.
286    fn load_many(
287        &self,
288        secret_id: u64,
289        channel_ids: &[ChannelId],
290        versions: &[u32],
291    ) -> ShareStoreFuture<'_, Vec<Share>>;
292
293    /// Load every share stored under `secret_id` across the given
294    /// channels and every version. Used by Discovery to enumerate the
295    /// helper's holdings for the active secret.
296    fn load_all(
297        &self,
298        secret_id: u64,
299        channel_ids: &[ChannelId],
300    ) -> ShareStoreFuture<'_, Vec<Share>>;
301
302    /// Return the highest version number stored for `secret_id` across
303    /// all channels, or `None` if no shares exist yet for this secret.
304    fn latest_version(&self, secret_id: u64) -> ShareStoreFuture<'_, Option<u32>>;
305
306    /// Persist a share for `(secret_id, channel_id)`.
307    ///
308    /// # Conceptual storage key
309    ///
310    /// The protocol considers the full storage key to be
311    /// `(secret_id, channel_id, share.version, share.replica_id)`.
312    /// Replica destinations reuse the source's channel shared key with
313    /// helpers (the key travels in the `ReplicaSecretPayload`), so
314    /// two replicas writing the same `(secret_id, channel_id, version)`
315    /// look cryptographically identical at the wire layer — only
316    /// `share.replica_id` separates them. A naive helper that ignored
317    /// `replica_id` and overwrote on the three-tuple key would silently
318    /// lose one of the two writes.
319    ///
320    /// # Implementation freedom
321    ///
322    /// The trait does not dictate how implementations represent the
323    /// `replica_id` discriminator (separate column, composite primary
324    /// key, write-time conflict log, etc.). The contract is:
325    ///
326    /// - Writes from distinct `replica_id`s for the same
327    ///   `(secret_id, channel_id, version)` MUST both survive — neither
328    ///   may silently overwrite the other.
329    /// - A write that matches an existing entry on all four fields
330    ///   replaces it (idempotent re-send).
331    /// - `load`, `load_many`, and `load_all` return every distinct
332    ///   `(version, replica_id)` entry matching the requested filter;
333    ///   the application performs any per-application coalescing.
334    ///
335    /// `share.secret_id` is denormalized metadata and must match the
336    /// partition key `secret_id` — implementations may assert this.
337    fn save(
338        &mut self,
339        secret_id: u64,
340        channel_id: ChannelId,
341        share: Share,
342    ) -> ShareStoreFuture<'_, ()>;
343
344    /// Drop every share stored under `(secret_id, channel_id)`. Used
345    /// when an unpair flow tears down a channel. Idempotent.
346    fn remove_channel(
347        &mut self,
348        secret_id: u64,
349        channel_id: ChannelId,
350    ) -> ShareStoreFuture<'_, ()>;
351}
352
353/// Storage for the user-facing secret contents, keyed by `secret_id`.
354///
355/// One `secret_id` maps to at most one [`UserSecrets`] entry — the most
356/// recent snapshot the application handed off via
357/// `start(FlowKind::ProtectSecret)`. The pair-completion auto-publish
358/// hook reads from here so a freshly-paired Helper or Replica receives
359/// the current secret without an explicit re-publish from the app.
360///
361/// # Executor independence
362///
363/// Methods return [`ShareStoreFuture`] — same `Send` rules as the other
364/// store traits. The error type is reused from [`ShareStoreError`]
365/// because the persistence concerns overlap (latest-version bookkeeping,
366/// IO failures); no separate error category was warranted.
367///
368/// # Concurrency
369///
370/// The protocol holds the store by `&mut Self`, so implementations never
371/// see overlapping calls and need no internal synchronization.
372pub trait DeRecUserSecretStore {
373    /// Return the latest [`UserSecrets`] entry for `secret_id`, or
374    /// `Ok(None)` if the application has never published for this
375    /// `secret_id` on this instance.
376    fn load_latest(&self, secret_id: u64) -> ShareStoreFuture<'_, Option<UserSecrets>>;
377
378    /// Persist `value` as the latest entry for `secret_id`, overwriting
379    /// any prior entry. The store keeps only the latest snapshot — older
380    /// versions are recoverable via the helper share quorum if needed.
381    fn save_latest(
382        &mut self,
383        secret_id: u64,
384        value: UserSecrets,
385    ) -> ShareStoreFuture<'_, ()>;
386
387    /// Drop the entry for `secret_id`. Idempotent: removing a
388    /// non-existent entry is `Ok(())`.
389    fn remove(&mut self, secret_id: u64) -> ShareStoreFuture<'_, ()>;
390}
391
392/// Outbound transport abstraction.
393///
394/// The library calls `send` whenever it needs to deliver bytes to a peer.
395/// The `endpoint` value comes from the `TransportProtocol` stored during pairing.
396///
397/// # Executor independence
398///
399/// Same as [`DeRecSecretStore`]; `send` returns [`TransportFuture`].
400pub trait DeRecTransport {
401    /// Deliver `message` to `endpoint`.
402    ///
403    /// `endpoint` is the [`TransportProtocol`] the peer advertised during
404    /// pairing. The library calls this from protocol handlers whenever an
405    /// outbound envelope needs to reach a peer.
406    fn send(&self, endpoint: &TransportProtocol, message: Vec<u8>) -> TransportFuture<'_>;
407}
408
409/// Durable storage for the orchestrator's in-flight protocol state.
410///
411/// The `DeRecProtocol` orchestrator produces short-lived state during
412/// every flow — outstanding verification challenges, in-progress recovery
413/// accumulators, and pending unpair acknowledgements. In long-running
414/// processes this state can live in memory; in stateless deployments
415/// (serverless functions, load-balanced services with instance churn)
416/// the state must survive across process boundaries or replies will
417/// arrive to a live channel with nothing to bind them to.
418///
419/// Every backend chooses its own persistence layer — in-memory `HashMap`
420/// for local development and tests, SQLite for edge or single-process
421/// deployments, Redis / Postgres / DynamoDB for load-balanced or
422/// serverless deployments.
423///
424/// # Contract
425///
426/// - [`save`](DeRecStateStore::save) is a **full-replacement upsert**.
427///   No per-item merge or append semantic. Accumulator-style state
428///   ([`StateItem::PendingRecovery`] and [`StateItem::SharingRound`])
429///   grows via load-modify-save from the library.
430/// - [`load`](DeRecStateStore::load) is a **pure read**. No side effects.
431///   Returns `Ok(None)` when the row does not exist.
432/// - [`remove`](DeRecStateStore::remove) is idempotent: removing a
433///   missing entry is `Ok(false)`, and returning `Ok(true)` iff a row
434///   was actually removed.
435/// - [`load_all`](DeRecStateStore::load_all) returns every item of the
436///   given kind under this `secret_id`, in no guaranteed order.
437///
438/// # Concurrency
439///
440/// The library guarantees at-most-once processing of any given inbound
441/// response only in **single-instance deployments**. In multi-instance /
442/// load-balanced deployments where two instances may hold a
443/// [`DeRecProtocol`](super::DeRecProtocol) against the same
444/// `secret_id` at once:
445///
446/// - `load` + `remove` is not atomic across calls (two round-trips).
447/// - Two instances processing the same inbound response can each `load`
448///   the entry, each `remove` it, and each proceed with response
449///   handling — producing **duplicate events** to the application.
450/// - All library-emitted events (`ShareVerified`, `Unpaired`, etc.) are
451///   idempotent from the application's perspective: on-wire state has
452///   already settled, and a duplicate event does not corrupt anything.
453/// - Concurrent inbound shares racing to modify a
454///   [`StateItem::PendingRecovery`] accumulator, or concurrent inbound
455///   store-share responses racing to update a
456///   [`StateItem::SharingRound`] tally, can clobber each other via naive
457///   load-modify-save. **The application layer is responsible for
458///   serializing concurrent `process()` calls that touch the same
459///   `(secret_id, version)`** if this matters.
460///
461/// # Executor independence
462///
463/// Same as [`DeRecSecretStore`]; methods return [`StateStoreFuture`].
464///
465/// # Concurrency (single-instance)
466///
467/// The protocol holds the store by `&mut Self`, so a single-instance
468/// implementation never sees overlapping calls and needs no internal
469/// synchronization. Multi-instance backends must provide their own
470/// consistency guarantees.
471pub trait DeRecStateStore {
472    /// Insert or full-replace by `(secret_id, item.key())`. Idempotent —
473    /// if the row already exists, the existing entry is replaced in place
474    /// with the caller-supplied `item`.
475    fn save(&mut self, secret_id: u64, item: StateItem) -> StateStoreFuture<'_, ()>;
476
477    /// Read the item at `(secret_id, key)`. Returns `Ok(None)` when no
478    /// row exists. No side effects.
479    fn load(&self, secret_id: u64, key: StateKey) -> StateStoreFuture<'_, Option<StateItem>>;
480
481    /// Remove the item at `(secret_id, key)`. Returns `Ok(true)` iff a
482    /// row was removed. Idempotent — removing a missing entry is
483    /// `Ok(false)`, not an error.
484    fn remove(&mut self, secret_id: u64, key: StateKey) -> StateStoreFuture<'_, bool>;
485
486    /// Return every item of the given `kind` under this `secret_id`, in
487    /// no guaranteed order.
488    ///
489    /// Used by the library to sweep timeouts (walk
490    /// [`StateKind::PendingUnpair`], filter by
491    /// [`StateItem::PendingUnpair::started_at`]) and for
492    /// recovery-accumulator introspection. Data volume per kind is
493    /// bounded by the number of channels or active reconstruction
494    /// targets and is expected to be small.
495    fn load_all(&self, secret_id: u64, kind: StateKind) -> StateStoreFuture<'_, Vec<StateItem>>;
496}