Skip to main content

derec_library/protocol/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4//! Higher-level protocol orchestrator for the DeRec protocol.
5//!
6//! This module provides [`DeRecProtocol`], a stateful orchestrator that wraps the
7//! core protocol flows. The caller supplies concrete implementations of:
8//!
9//! - [`DeRecChannelStore`] — paired-channel record storage
10//! - [`DeRecShareStore`] — secret share storage
11//! - [`DeRecSecretStore`] — cryptographic key storage
12//! - [`DeRecUserSecretStore`] — secret-snapshot storage for replica auto-publish
13//! - [`DeRecTransport`] — outbound message delivery
14//!
15//! The application feeds incoming wire bytes to [`DeRecProtocol::process`] and
16//! reacts to the returned [`DeRecEvent`] values. All routing, state persistence,
17//! and reply sending are handled internally.
18//!
19//! # Flows
20//!
21//! Each `DeRecProtocol::start(DeRecFlow::…)` entry point drives one
22//! protocol flow:
23//!
24//! - **Pairing** — establish a channel + derive a shared key with a peer.
25//! - **ProtectSecret** (sharing) — VSS-split the current secret to every
26//!   paired Helper and ship the full secret to every paired Replica.
27//! - **VerifyShares** — challenge a Helper to prove it still holds a
28//!   specific stored share via a SHA-384 commitment (see
29//!   [`StateItem::PendingVerification`](crate::protocol::types::StateItem)
30//!   for the orchestrator-owned request/response binding row in the
31//!   state store).
32//! - **Discovery** — ask a Helper which `(secret_id, version)` tuples it
33//!   currently holds for us. Frequently the precursor to `RecoverSecret`
34//!   but useful for routine inventory too.
35//! - **RecoverSecret** — collect enough Helper shares to reconstruct an
36//!   earlier secret version.
37//! - **Restore** — [`DeRecProtocol::restore`] commits a recovered
38//!   [`crate::protocol::types::Secret`] into a fresh protocol instance:
39//!   reseats canonical helper / replica channels at the recovered
40//!   version and wipes the throwaway recovery-mode channels. Not a
41//!   [`DeRecFlow`] variant — called once, directly on the protocol,
42//!   after a `SecretRecovered` event surfaces.
43//! - **UpdateChannelInfo** — broadcast updated `communication_info`
44//!   and/or `transport_protocol` to one or more paired peers. Either
45//!   side may initiate. The accompanying setters
46//!   [`DeRecProtocol::set_communication_info`] and
47//!   [`DeRecProtocol::set_own_transport`] update local state first; the
48//!   flow then announces the change. The endpoint-changeover discipline
49//!   on `set_own_transport` is required reading before broadcasting a
50//!   transport update — both endpoints must remain reachable through
51//!   the changeover or in-flight traffic will be lost.
52//! - **Unpair** — Owner-initiated channel teardown. Ack semantics are
53//!   governed by [`DeRecProtocolBuilder::with_unpair_ack`].
54//!
55//! See [`DeRecFlow`] for the per-variant role requirements and field
56//! semantics.
57//!
58//! # Reserved `CommunicationInfo` keys
59//!
60//! `CommunicationInfo` is an opaque app-defined string map *except* for
61//! entries under the `derec.*` namespace, which the library reserves
62//! for its own use (e.g. carrying the sender's `replica_id` on
63//! replica-mode pairing envelopes). Apps SHOULD NOT use this namespace;
64//! the orchestrator silently auto-injects, extracts, and strips
65//! `derec.*` entries at the protocol boundary. See
66//! [`reserved_keys`] for the current set of keys and their wire
67//! encoding.
68
69pub mod error;
70pub mod events;
71#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
72pub(crate) mod pending_action_wire;
73pub mod reserved_keys;
74pub mod traits;
75pub mod types;
76
77mod builder;
78mod handlers;
79
80use crate::{
81    Error, Result,
82    primitives::pairing::request::create_contact as create_contact_message,
83    types::ChannelId,
84};
85pub use builder::DeRecProtocolBuilder;
86use derec_proto::{ContactMessage, ContactMode, DeRecMessage, StatusEnum, TransportProtocol};
87pub use error::{
88    ChannelStoreError, ProcessError, SecretStoreError, ShareStoreError, StateStoreError,
89};
90use prost::Message;
91use std::collections::{HashMap, HashSet};
92pub use traits::{
93    ChannelStoreFuture, DeRecChannelStore, DeRecSecretStore, DeRecShareStore, DeRecStateStore,
94    DeRecTransport, DeRecUserSecretStore, SecretStoreFuture, ShareStoreFuture, StateStoreFuture,
95    TransportFuture,
96};
97pub use types::{
98    Channel, ChannelShare, ChannelStatus, HelperInfo, MissingPolicy, PairingKeyMaterial,
99    ReplicaInfo, ReplicaSecretPayload, Secret, SecretKind, SecretValue, Share, StateItem, StateKey,
100    StateKind, Target, UserSecret, UserSecrets,
101};
102
103
104pub use events::{
105    AutoAcceptPolicy, DeRecEvent, DeRecFlow, PendingAction, PendingActionKind, UnpairAck,
106};
107pub use handlers::restore::RestoreError;
108
109#[cfg(not(target_arch = "wasm32"))]
110use crate::utils::now_secs;
111#[cfg(target_arch = "wasm32")]
112use crate::wasm::now_secs;
113
114/// Internal state of the single secret container managed by the protocol.
115///
116/// Created on the first `ProtectSecret` flow and reused (with incrementing
117/// version) on every subsequent call.
118/// Higher-level DeRec protocol orchestrator.
119///
120/// Generic over:
121/// - `ChannelStore` — paired channel storage ([`DeRecChannelStore`])
122/// - `ShareStore`   — share storage ([`DeRecShareStore`])
123/// - `SecretStore`  — secret storage ([`DeRecSecretStore`])
124/// - `Transport`    — outbound transport ([`DeRecTransport`])
125///
126/// The caller provides concrete implementations; the library imposes no
127/// runtime or I/O requirements.
128///
129/// # Lifecycle
130///
131/// ```text
132/// DeRecProtocolBuilder::new(secret_id).<setters>.build()?
133///   │
134///   ├── create_contact / start(Pairing)          → pairing
135///   ├── start(ProtectSecret)                     → sharing
136///   ├── start(VerifyShares)                      → verification
137///   ├── start(Pairing { Owner })                 (recovery re-pair)
138///   │     └── start(Discovery)                   → discovery       (emits SecretsDiscovered)
139///   ├── start(RecoverSecret)                     → recovery        (emits SecretRecovered)
140///   │     └── restore(&secret, version)          → commit recovered secret into canonical state
141///   ├── start(UpdateChannelInfo)                 → endpoint/info update (either side)
142///   └── start(Unpair)                            → unpair          (Owner-initiated; ack
143///                                                                   semantics governed by
144///                                                                   [`DeRecProtocolBuilder::with_unpair_ack`])
145///
146/// loop { process(incoming_bytes) → Vec<DeRecEvent> }
147/// ```
148///
149/// See [`DeRecFlow`] for the full set of orchestrator entry points
150/// and the role each requires on the targeted channel(s).
151pub struct DeRecProtocol<
152    ChannelStore: DeRecChannelStore,
153    ShareStore: DeRecShareStore,
154    SecretStore: DeRecSecretStore,
155    UserSecretStore: DeRecUserSecretStore,
156    StateStore: DeRecStateStore,
157    Transport: DeRecTransport,
158> {
159    /// Set via [`DeRecProtocolBuilder::with_channel_store`].
160    pub channel_store: ChannelStore,
161    /// Set via [`DeRecProtocolBuilder::with_share_store`].
162    pub share_store: ShareStore,
163    /// Set via [`DeRecProtocolBuilder::with_secret_store`].
164    pub secret_store: SecretStore,
165    /// Set via [`DeRecProtocolBuilder::with_user_secret_store`].
166    pub user_secret_store: UserSecretStore,
167    /// Set via [`DeRecProtocolBuilder::with_state_store`]. Holds
168    /// in-flight orchestrator state (verification challenges, recovery
169    /// accumulators, pending unpair acks) so stateless / load-balanced
170    /// deployments preserve it across process restarts.
171    pub state_store: StateStore,
172    /// Set via [`DeRecProtocolBuilder::with_transport`].
173    pub transport: Transport,
174    /// Set via [`DeRecProtocolBuilder::with_own_transport`].
175    pub own_transport: TransportProtocol,
176    /// Configured via [`DeRecProtocolBuilder::with_unpair_ack`].
177    pub(crate) unpair_ack: UnpairAck,
178    /// Configured via [`DeRecProtocolBuilder::with_threshold`].
179    threshold: usize,
180    /// Configured via [`DeRecProtocolBuilder::with_keep_versions_count`].
181    keep_versions_count: usize,
182    /// Configured via [`DeRecProtocolBuilder::with_timeout`].
183    timeout_in_secs: u64,
184    /// Configured via [`DeRecProtocolBuilder::with_communication_info`].
185    pub(crate) communication_info: HashMap<String, String>,
186    /// Configured via [`DeRecProtocolBuilder::with_auto_respond_on_failure`].
187    pub(crate) auto_respond_on_failure: bool,
188    /// Configured via [`DeRecProtocolBuilder::with_auto_reply_to`].
189    ///
190    /// When `true`, every outbound request envelope is stamped with
191    /// `request.reply_to = self.own_transport` so the responder knows which
192    /// endpoint to route the response to. When `false` (the default),
193    /// outbound requests leave `reply_to` unset and the responder falls back
194    /// to the channel's stored peer endpoint. See `replyTo` on each request
195    /// proto for the wire-level semantics.
196    pub(crate) auto_reply_to: bool,
197    /// Configured via [`DeRecProtocolBuilder::with_auto_accept`].
198    ///
199    /// When a flow's field on the policy is `true`, [`Self::process`]
200    /// invokes the equivalent of [`Self::accept`] internally for that
201    /// flow and emits [`DeRecEvent::AutoAccepted`] in place of
202    /// [`DeRecEvent::ActionRequired`].
203    pub(crate) auto_accept: AutoAcceptPolicy,
204    /// Configured via [`DeRecProtocolBuilder::with_replica_id`].
205    ///
206    /// `Some(id)` enables this node to participate in replica-mode pairings
207    /// (the id is auto-injected under `derec.replica_id` in outbound
208    /// `PairRequest`/`PairResponse`, and required to honour inbound replica
209    /// pairings). `None` disables replica flows entirely — any attempt to
210    /// initiate or accept a replica-mode pairing returns
211    /// [`Error::ReplicaIdNotConfigured`](crate::Error::ReplicaIdNotConfigured).
212    pub(crate) replica_id: Option<u64>,
213    /// Configured via [`DeRecProtocolBuilder::with_parameter_range`].
214    ///
215    /// `Some(range)` advertises the local node's acceptable bounds in
216    /// outbound `PairRequest` / `PairResponse` envelopes and validates
217    /// the peer's advertised range on inbound ones. `None` declares no
218    /// constraints — every peer range is accepted and outbound
219    /// envelopes omit the field.
220    pub(crate) parameter_range: Option<derec_proto::ParameterRange>,
221    /// Identifier of the single secret this protocol instance manages.
222    ///
223    /// Set at construction (`DeRecProtocolBuilder::new(secret_id)`) and
224    /// never changes — apps that juggle multiple secrets instantiate one
225    /// protocol per `secret_id`.
226    secret_id: u64,
227}
228
229impl<
230    Ch: DeRecChannelStore,
231    Sh: DeRecShareStore,
232    Ss: DeRecSecretStore,
233    Us: DeRecUserSecretStore,
234    T: DeRecTransport,
235    St: DeRecStateStore,
236> DeRecProtocol<Ch, Sh, Ss, Us, St, T>
237{
238
239    /// Construct a [`DeRecProtocol`] directly from its components.
240    ///
241    /// Prefer [`DeRecProtocolBuilder`] for the type-checked
242    /// construction path; both entry points run the same runtime
243    /// validation and surface the same errors.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`crate::Error::InvalidInput`] if `threshold < 2`. A
248    /// threshold of `0` or `1` collapses threshold secret sharing and
249    /// lets a single helper reconstruct the secret unilaterally — two
250    /// is the minimum value that preserves secret confidentiality
251    /// against one compromised helper.
252    #[allow(clippy::too_many_arguments)]
253    pub fn new(
254        secret_id: u64,
255        channel_store: Ch,
256        share_store: Sh,
257        secret_store: Ss,
258        user_secret_store: Us,
259        state_store: St,
260        transport: T,
261        own_transport: TransportProtocol,
262        threshold: usize,
263        keep_versions_count: usize,
264        timeout_in_secs: u64,
265    ) -> Result<Self> {
266        if threshold < 2 {
267            return Err(crate::Error::InvalidInput(
268                "threshold must be >= 2; 0 or 1 lets a single helper reconstruct the secret \
269                 and defeats threshold sharing",
270            ));
271        }
272        Ok(Self {
273            channel_store,
274            share_store,
275            secret_store,
276            user_secret_store,
277            state_store,
278            transport,
279            own_transport,
280            unpair_ack: UnpairAck::Required,
281            threshold,
282            keep_versions_count,
283            timeout_in_secs,
284            communication_info: HashMap::new(),
285            auto_respond_on_failure: false,
286            auto_reply_to: false,
287            auto_accept: AutoAcceptPolicy::default(),
288            replica_id: None,
289            parameter_range: None,
290            secret_id,
291        })
292    }
293
294    /// Returns the secret identifier this protocol instance was configured with.
295    pub fn secret_id(&self) -> u64 {
296        self.secret_id
297    }
298
299    /// Returns the configured local replica id, or `None` if the protocol
300    /// was built without [`DeRecProtocolBuilder::with_replica_id`].
301    ///
302    /// Apps can use this to surface "replica flows are enabled" to the user,
303    /// or to inspect their own identity for logging/diagnostics. The id is
304    /// the same value that the orchestrator auto-injects under
305    /// `derec.replica_id` in outbound replica-mode `PairRequest` /
306    /// `PairResponse` envelopes.
307    pub fn replica_id(&self) -> Option<u64> {
308        self.replica_id
309    }
310
311    /// Generate an out-of-band contact message (QR code payload, deep link, …).
312    ///
313    /// Either party (Owner or Helper) may call this to begin a pairing session.
314    /// The returned [`ContactMessage`] should be delivered out-of-band to the peer.
315    /// Any material the library needs later — either the ephemeral pairing
316    /// secret (`InlineKeys` / `HashedKeys`) or the contact itself (`NoKeys`) —
317    /// is persisted automatically via the configured stores.
318    ///
319    /// # Channel ID
320    ///
321    /// Pass `Some(id)` to use a specific channel identifier, or `None` to have
322    /// the library generate a random one. Applications targeting `NoKeys` mode
323    /// typically pass a small human-typable value (4 digits) for manual entry.
324    ///
325    /// # Contact mode
326    ///
327    /// - [`ContactMode::InlineKeys`] embeds the initiator's ML-KEM + ECIES
328    ///   public keys directly in the contact. Simplest to use; the contact is
329    ///   ~1.2 KB.
330    /// - [`ContactMode::HashedKeys`] embeds only a SHA-384 binding hash over
331    ///   the keys. The contact stays small enough for a QR code; the scanner
332    ///   obtains the real keys via a `PrePair` round-trip and validates them
333    ///   against the hash. Requires the `own_transport` set on this protocol
334    ///   to be **ephemeral** — the plaintext PrePair traffic must not be
335    ///   linkable to a long-lived endpoint.
336    /// - [`ContactMode::NoKeys`] carries no key material and no commitment —
337    ///   only `channel_id`, `nonce`, and `transport_protocol`. Small enough
338    ///   to be hand-typed. Keys are generated on the fly by the creator when
339    ///   the corresponding `PrePairRequest` arrives; trust rests entirely on
340    ///   the OOB delivery channel being fully trusted (e.g. a verified email
341    ///   from an already-KYC-authenticated institution). Applications MUST
342    ///   rate-limit inbound `PrePairRequest`s per `channel_id` and expire
343    ///   outstanding NoKeys contacts on a short timer.
344    ///
345    /// # Nonce
346    ///
347    /// - `None`: the library generates a fresh cryptographically-random
348    ///   `u64`. Recommended default for `InlineKeys` and `HashedKeys` where
349    ///   the nonce is a security parameter.
350    /// - `Some(n)`: application-controlled value. Required for `NoKeys`
351    ///   where the recipient typically types it in; also valid for the
352    ///   other modes if the app wants deterministic control.
353    #[cfg_attr(feature = "logging", tracing::instrument(skip_all))]
354    pub async fn create_contact(
355        &mut self,
356        channel_id: Option<ChannelId>,
357        contact_mode: ContactMode,
358        nonce: Option<u64>,
359    ) -> Result<ContactMessage> {
360        let channel_id = channel_id.unwrap_or_else(|| ChannelId(rand::random::<u64>()));
361
362        #[cfg(feature = "logging")]
363        tracing::debug!(
364            channel_id = channel_id.0,
365            contact_mode = contact_mode as i32,
366            "creating contact message"
367        );
368
369        let result = create_contact_message(
370            channel_id,
371            contact_mode,
372            self.own_transport.clone(),
373            nonce,
374        )?;
375
376        // Persist the material the eventual `PrePairRequest` /
377        // `PairRequest` handler will need to look up:
378        // - `Some(secret_key)` on InlineKeys / HashedKeys → store as
379        //   `PairingSecret`. Handler decrypts the encrypted PairRequest
380        //   with the ECIES secret and re-publishes keys on the PrePair
381        //   leg (HashedKeys only).
382        // - `None` on NoKeys → store the contact itself as
383        //   `PairingContact` so the incoming PrePairRequest handler can
384        //   (a) authenticate the caller by matching `nonce`, and
385        //   (b) generate fresh key material on the fly for the response.
386        match result.secret_key {
387            Some(secret_key) => {
388                self.secret_store
389                    .save(
390                        self.secret_id,
391                        channel_id,
392                        SecretValue::PairingSecret(PairingKeyMaterial::from_secret(&secret_key)),
393                    )
394                    .await?;
395            }
396            None => {
397                self.secret_store
398                    .save(
399                        self.secret_id,
400                        channel_id,
401                        SecretValue::PairingContact(result.contact_message.clone()),
402                    )
403                    .await?;
404            }
405        }
406
407        #[cfg(feature = "logging")]
408        tracing::info!(channel_id = channel_id.0, "contact message created");
409
410        Ok(result.contact_message)
411    }
412
413    /// Replace this node's local communication info.
414    ///
415    /// Only mutates local state — to propagate the change to paired peers,
416    /// follow up with [`DeRecFlow::UpdateChannelInfo`].
417    ///
418    /// # Destructive replacement
419    ///
420    /// The supplied map fully replaces the current value. An empty map will
421    /// be transmitted as "clear all entries" when a subsequent
422    /// `UpdateChannelInfo` flow runs, which the peer will mirror into its
423    /// stored map. Pass the complete new map, not a delta.
424    pub fn set_communication_info(&mut self, info: HashMap<String, String>) {
425        self.communication_info = info;
426    }
427
428    /// Replace this node's local transport endpoint.
429    ///
430    /// Only mutates local state — to propagate the change to paired peers,
431    /// follow up with [`DeRecFlow::UpdateChannelInfo`].
432    ///
433    /// # Endpoint changeover discipline
434    ///
435    /// When `UpdateChannelInfo` is broadcast, each receiving peer routes its
436    /// response (and all subsequent messages) to the NEW endpoint. The
437    /// application MUST therefore:
438    ///
439    /// 1. Bring up the new endpoint and start listening on it **before**
440    ///    initiating the `UpdateChannelInfo` flow.
441    /// 2. Keep the old endpoint operational during the changeover. Peers
442    ///    that have not yet processed the update still route to the old
443    ///    address; in-flight messages may also be targeted there.
444    /// 3. Retire the old endpoint only once every targeted peer has
445    ///    surfaced [`DeRecEvent::ChannelInfoUpdated`] (or
446    ///    [`DeRecEvent::ChannelInfoUpdateRejected`]), plus a grace window
447    ///    for in-flight traffic.
448    ///
449    /// Failing to keep both endpoints reachable during this window will
450    /// cause messages to be lost.
451    /// Set the local node's transport endpoint.
452    ///
453    /// Accepts anything implementing
454    /// [`IntoOwnTransport`](crate::transport::IntoOwnTransport): a
455    /// typed [`crate::transport::TransportProtocol`], a `&str`, or a
456    /// `String`. Validation runs eagerly — a malformed URI surfaces
457    /// as [`crate::Error::Transport`] instead of being stored and
458    /// later propagated to peers.
459    ///
460    /// # Errors
461    ///
462    /// Returns [`crate::Error::Transport`] if the supplied value
463    /// fails URI validation (empty, oversize, control characters,
464    /// or scheme mismatch).
465    pub fn set_own_transport(
466        &mut self,
467        own_transport: impl crate::transport::IntoOwnTransport,
468    ) -> crate::Result<()> {
469        let tp = own_transport.into_own_transport()?;
470        self.own_transport = tp.into();
471        Ok(())
472    }
473
474    /// Unified entry point for initiating any protocol flow.
475    ///
476    /// Returns the flow's per-target `*Started` / `*Failed` events (one
477    /// `PairingStarted` for [`DeRecFlow::Pairing`]; one per targeted
478    /// channel for fan-out flows). See each `*Started` /
479    /// `*Failed` variant on [`DeRecEvent`] for the per-flow shape.
480    ///
481    /// # Errors
482    ///
483    /// - Programmer errors (invalid input, missing preconditions, role
484    ///   mismatch) surface as `Err` before any fan-out begins. Once
485    ///   fan-out starts for a multi-target flow, per-channel transport
486    ///   failures become `*Failed` events in the returned vec — they do
487    ///   not abort the round.
488    /// - Single-channel flows ([`DeRecFlow::Pairing`],
489    ///   [`DeRecFlow::Unpair`]) return `Err` on send failure — no
490    ///   `*Failed` event exists, so the single-`Err` signal is
491    ///   unambiguous.
492    #[cfg_attr(feature = "logging", tracing::instrument(skip_all))]
493    pub async fn start(&mut self, flow: DeRecFlow) -> Result<Vec<DeRecEvent>> {
494        // When `auto_reply_to` is enabled, stamp every outbound
495        // channel-mode request with our own transport so the responder
496        // routes its reply back to us — even if the channel's stored
497        // peer endpoint points elsewhere (e.g. a sibling replica).
498        // Pairing has its own dedicated `transport_protocol` field so
499        // it's intentionally excluded.
500        let reply_to = self.auto_reply_to.then(|| self.own_transport.clone());
501
502        match flow {
503            DeRecFlow::Pairing {
504                kind,
505                contact,
506                peer_communication_info,
507            } => {
508                self.start_pairing(kind, contact, peer_communication_info)
509                    .await
510            }
511            DeRecFlow::Discovery { target } => self.start_discovery(target, reply_to).await,
512            DeRecFlow::ProtectSecret {
513                secrets,
514                description,
515            } => {
516                self.start_protect_secret(secrets, description, reply_to)
517                    .await
518            }
519            DeRecFlow::VerifyShares {
520                secret_id,
521                version,
522                target,
523            } => {
524                self.start_verify_shares(secret_id, version, target, reply_to)
525                    .await
526            }
527            DeRecFlow::RecoverSecret { secret_id, version } => {
528                self.start_recover_secret(secret_id, version, reply_to)
529                    .await
530            }
531            DeRecFlow::Unpair { channel_id, memo } => {
532                self.start_unpair(channel_id, memo, reply_to).await
533            }
534            DeRecFlow::UpdateChannelInfo {
535                target,
536                communication_info,
537                transport_protocol,
538            } => {
539                self.start_update_channel_info(target, communication_info, transport_protocol)
540                    .await
541            }
542        }
543    }
544
545    /// Accept a pending action from an [`DeRecEvent::ActionRequired`] event.
546    ///
547    /// Executes the "do work + send response" path for the given action,
548    /// returning the same events that auto-respond would have produced.
549    #[cfg_attr(feature = "logging", tracing::instrument(skip_all))]
550    pub async fn accept(&mut self, action: PendingAction) -> Result<Vec<DeRecEvent>> {
551        let mut events = self.accept_inner(action).await?;
552        let auto_publish_events = self.maybe_auto_publish_after_pair(&events).await?;
553        events.extend(auto_publish_events);
554        Ok(events)
555    }
556
557    /// Reject a pending action from an [`DeRecEvent::ActionRequired`] event.
558    ///
559    /// Builds and sends a rejection response to the peer with the given status
560    /// and memo. The `status` parameter allows the caller to specify the exact
561    /// failure reason (e.g. [`StatusEnum::Rejected`], [`StatusEnum::Fail`],
562    /// [`StatusEnum::TooFrequent`], etc.).
563    #[cfg_attr(feature = "logging", tracing::instrument(skip_all))]
564    pub async fn reject(
565        &mut self,
566        action: PendingAction,
567        status: StatusEnum,
568        memo: &str,
569    ) -> Result<()> {
570        match action {
571            PendingAction::Pairing {
572                channel_id,
573                request,
574                trace_id,
575                ..
576            } => {
577                handlers::pairing::reject(
578                    &mut self.secret_store,
579                    &self.transport,
580                    &self.communication_info,
581                    self.secret_id,
582                    channel_id,
583                    &request,
584                    status,
585                    memo,
586                    trace_id,
587                )
588                .await
589            }
590            PendingAction::StoreShare {
591                channel_id,
592                request,
593                shared_key,
594                trace_id,
595            } => {
596                handlers::sharing::reject(
597                    &mut self.channel_store,
598                    &self.transport,
599                    self.secret_id,
600                    channel_id,
601                    &request,
602                    &shared_key,
603                    status,
604                    memo,
605                    trace_id,
606                )
607                .await
608            }
609            PendingAction::VerifyShare {
610                channel_id,
611                request,
612                shared_key,
613                trace_id,
614            } => {
615                handlers::verification::reject(
616                    &mut self.channel_store,
617                    &self.transport,
618                    self.secret_id,
619                    channel_id,
620                    &request,
621                    &shared_key,
622                    status,
623                    memo,
624                    trace_id,
625                )
626                .await
627            }
628            PendingAction::Discovery {
629                channel_id,
630                request,
631                shared_key,
632                trace_id,
633            } => {
634                handlers::discovery::reject(
635                    &mut self.channel_store,
636                    &self.transport,
637                    self.secret_id,
638                    channel_id,
639                    &request,
640                    &shared_key,
641                    status,
642                    memo,
643                    trace_id,
644                )
645                .await
646            }
647            PendingAction::GetShare {
648                channel_id,
649                request,
650                shared_key,
651                trace_id,
652            } => {
653                handlers::recovery::reject(
654                    &mut self.channel_store,
655                    &self.transport,
656                    self.secret_id,
657                    channel_id,
658                    &request,
659                    &shared_key,
660                    status,
661                    memo,
662                    trace_id,
663                )
664                .await
665            }
666            PendingAction::Unpair {
667                channel_id,
668                request,
669                shared_key,
670                trace_id,
671            } => {
672                handlers::unpairing::reject(
673                    &mut self.channel_store,
674                    &self.transport,
675                    self.secret_id,
676                    channel_id,
677                    &request,
678                    &shared_key,
679                    status,
680                    memo,
681                    trace_id,
682                )
683                .await
684            }
685            PendingAction::UpdateChannelInfo {
686                channel_id,
687                shared_key,
688                trace_id,
689                ..
690            } => {
691                handlers::update_channel_info::reject(
692                    &mut self.channel_store,
693                    &self.transport,
694                    self.secret_id,
695                    channel_id,
696                    &shared_key,
697                    status,
698                    memo,
699                    trace_id,
700                )
701                .await
702            }
703            PendingAction::PrePair {
704                channel_id,
705                request,
706                trace_id,
707            } => {
708                handlers::pairing::reject_pre_pair(
709                    &self.transport,
710                    channel_id,
711                    &request,
712                    status,
713                    memo,
714                    trace_id,
715                )
716                .await
717            }
718        }
719    }
720
721    /// Feed any incoming wire bytes here regardless of which flow they belong to.
722    ///
723    /// The library:
724    ///
725    /// 1. Decodes the outer [`DeRecMessage`] envelope to read `channel_id`
726    /// 2. Looks up the channel's key material to determine the message kind
727    /// 3. Dispatches to the appropriate message handler based on the channel state
728    /// 4. Returns the events the application should react to
729    ///
730    /// # Security: bounding inbound message size
731    ///
732    /// This function does **not** enforce an upper bound on `message.len()`,
733    /// and no library entry point that ingests peer wire bytes does either.
734    /// Legitimate envelopes span many orders of magnitude:
735    ///
736    /// - Tens of bytes for empty acks / ping-class messages.
737    /// - A few KB for pairing material and verification proofs.
738    /// - Hundreds of KB to several MB for `StoreShareRequest` carrying a
739    ///   share of a large secret.
740    /// - Many MB for `ReplicaSync` envelopes carrying an entire secret
741    ///   (`O(num_secrets × num_helpers × max_secret_bytes)`).
742    ///
743    /// Any cap tight enough to provide meaningful DoS resistance would risk
744    /// silently truncating a legitimate replica sync — at which point the
745    /// secret can become unrecoverable. The protocol therefore delegates
746    /// inbound-size bounding to the **application's transport layer**,
747    /// which knows the deployment's max secret size, helper count, and
748    /// replica fan-out and can pick a ceiling that fits.
749    ///
750    /// Callers MUST refuse oversized envelopes upstream (e.g. enforce a
751    /// max HTTP body / WebSocket frame size consistent with their
752    /// configuration) before handing bytes to this function.
753    ///
754    /// Malformed bytes — including truncation, varint overflow, and any
755    /// `prost`-level decode failure — surface as
756    /// [`ProcessError`] wrapping [`Error::ProtobufDecode`]. This function
757    /// never panics on adversarial input. Protobuf recursion depth is
758    /// bounded by `prost`'s decoder; DeRec's schema is shallow (~3 levels),
759    /// so no additional caller-side recursion limit is required.
760    #[cfg_attr(
761        feature = "logging",
762        tracing::instrument(skip_all, fields(message_len = message.len()))
763    )]
764    pub async fn process(
765        &mut self,
766        message: &[u8],
767    ) -> std::result::Result<Vec<DeRecEvent>, ProcessError> {
768        let _ = self.cleanup_expired_channels().await;
769
770        let mut timeout_events = self.check_sharing_round_timeouts().await;
771        let mut unpair_timeout_events = self.check_unpair_timeouts().await;
772
773        let envelope = DeRecMessage::decode(message).map_err(|e| ProcessError {
774            channel_id: None,
775            source: Error::ProtobufDecode(e),
776        })?;
777        let channel_id = ChannelId(envelope.channel_id);
778
779        let result = self.process_inner(&envelope, channel_id).await;
780        let mut events = result.map_err(|source| ProcessError {
781            channel_id: Some(channel_id),
782            source,
783        })?;
784
785        // Auto-accept intercept: any `ActionRequired` in `events` whose
786        // action kind the configured `AutoAcceptPolicy` opts into is
787        // replaced in-place with `AutoAccepted` + the same flow events
788        // a manual `accept(action)` would have produced. Errors from
789        // the internal `accept_inner` propagate via `ProcessError`
790        // exactly as a manual accept would surface them through the
791        // caller's own error-handling — keeps the contract uniform.
792        events = self
793            .apply_auto_accept(events)
794            .await
795            .map_err(|source| ProcessError {
796                channel_id: Some(channel_id),
797                source,
798            })?;
799
800        // Ordering: sharing-round timeouts first, then unpair
801        // timeouts, then events produced by this specific message.
802        timeout_events.append(&mut unpair_timeout_events);
803        timeout_events.append(&mut events);
804        let mut events = timeout_events;
805
806        self.update_sharing_round(&mut events).await;
807
808        let auto_publish_events = self
809            .maybe_auto_publish_after_pair(&events)
810            .await
811            .map_err(|source| ProcessError {
812                channel_id: Some(channel_id),
813                source,
814            })?;
815        events.extend(auto_publish_events);
816
817        Ok(events)
818    }
819
820    /// Compute the fingerprint for a paired channel.
821    ///
822    /// Returns a formatted string like `"1234-5678-9012-3456"` derived from
823    /// the channel's shared key via SHA-256. Both parties will derive the same
824    /// fingerprint for the same shared key, enabling visual out-of-band
825    /// verification.
826    ///
827    /// Returns an error if the channel has no shared key (not yet paired).
828    #[cfg_attr(feature = "logging", tracing::instrument(skip_all, fields(channel_id = channel_id.0)))]
829    pub async fn get_fingerprint(&self, channel_id: ChannelId) -> Result<String> {
830        let shared_key = match self
831            .secret_store
832            .load(self.secret_id, channel_id, SecretKind::SharedKey)
833            .await?
834        {
835            Some(SecretValue::SharedKey(key)) => key,
836            _ => {
837                return Err(Error::InvalidInput(
838                    "channel has no shared key — not yet paired",
839                ));
840            }
841        };
842
843        Ok(derec_cryptography::replica::fingerprint(&shared_key))
844    }
845
846    /// Verify that a fingerprint matches the one derived from a channel's shared key.
847    ///
848    /// If the fingerprint matches, the channel status is updated from `Pending`
849    /// to `Paired`, enabling it to process protocol messages. Returns `true` on
850    /// match, `false` otherwise. Returns an error if the channel has no shared key.
851    #[cfg_attr(feature = "logging", tracing::instrument(skip_all, fields(channel_id = channel_id.0)))]
852    pub async fn verify_fingerprint(
853        &mut self,
854        channel_id: ChannelId,
855        fingerprint: &str,
856    ) -> Result<bool> {
857        let local = self.get_fingerprint(channel_id).await?;
858        if local != fingerprint {
859            return Ok(false);
860        }
861
862        // Update channel status to Paired.
863        let mut transitioned_replica = false;
864        if let Some(mut channel) = self.channel_store.load(self.secret_id, channel_id).await? {
865            transitioned_replica = channel.role == derec_proto::SenderKind::ReplicaSource
866                && channel.status == crate::protocol::types::ChannelStatus::Pending;
867            channel.status = crate::protocol::types::ChannelStatus::Paired;
868            self.channel_store.save(self.secret_id, channel).await?;
869        }
870
871        // Replica destinations only become eligible publish targets once
872        // the fingerprint is verified. Mirror the helper-pair hook in
873        // `process()` so the newly-confirmed peer receives the current
874        // secret without an explicit follow-up `ProtectSecret` call. The
875        // Pending→Paired transition means at least one Replica
876        // Destination is now paired, so the empty-payload fallback
877        // always applies when no `UserSecrets` snapshot has been cached
878        // yet.
879        if transitioned_replica {
880            let snapshot = self.user_secret_store.load_latest(self.secret_id).await?;
881            let (secrets, description) = match snapshot {
882                Some(s) => (s.secrets, s.description),
883                None => (Vec::new(), None),
884            };
885            let reply_to = self.auto_reply_to.then(|| self.own_transport.clone());
886            self.publish_secret(secrets, description, reply_to).await?;
887        }
888
889        Ok(true)
890    }
891
892    /// Rebuild this protocol's `secret_id` namespace from a
893    /// [`crate::protocol::types::Secret`] handed up by a
894    /// [`DeRecEvent::SecretRecovered`] event.
895    ///
896    /// # Caller flow
897    ///
898    /// ```text
899    /// fresh DeRecProtocol → empty stores
900    ///   → re-pair with helpers on a fresh channel-id namespace
901    ///   → start(RecoverSecret { secret_id, version })
902    ///   → SecretRecovered { secret } event arrives
903    ///   → DeRecProtocol::restore(&secret, version)
904    /// ```
905    ///
906    /// On success: canonical helper channels are persisted with
907    /// `SharedKey` + owner-side tracking shares at
908    /// `recovered_version`; canonical replica channels are persisted
909    /// with the group key from `secret.replicas.shared_key`;
910    /// the user-secret snapshot is committed at `recovered_version`;
911    /// the protocol's `replica_id` is adopted from
912    /// `secret.owner_replica_id` if previously unset; every other
913    /// channel under `self.secret_id` (i.e. the recovery-mode
914    /// channels) is unpaired (request sent to the helper, local
915    /// state dropped). The protocol resumes normal operation
916    /// immediately — the next `start(ProtectSecret)` publishes
917    /// `recovered_version + 1` to the restored helpers.
918    ///
919    /// The snapshot write is the commit point — nothing is removed
920    /// before it succeeds. Any mid-flight failure leaves state the
921    /// next `restore` call will detect as one of the precondition
922    /// errors below.
923    ///
924    /// # Errors
925    ///
926    /// Precondition / invariant failures surface as
927    /// [`crate::Error::Restore`] wrapping one of:
928    ///
929    /// - [`RestoreError::AlreadyRestored`] when a user-secret
930    ///   snapshot exists for this `secret_id`.
931    /// - [`RestoreError::Conflict`] when one or more channels live
932    ///   at canonical helper / replica ids carried by `secret`.
933    /// - [`RestoreError::Invariant`] when the recovered `Secret`
934    ///   is internally inconsistent (e.g. non-empty `replicas` with
935    ///   empty `replicas.shared_key`).
936    ///
937    /// Store I/O failures mid-restore propagate as the underlying
938    /// [`crate::Error::ShareStore`], [`crate::Error::ChannelStore`],
939    /// or [`crate::Error::SecretStore`] variant.
940    #[cfg_attr(feature = "logging", tracing::instrument(skip_all))]
941    pub async fn restore(
942        &mut self,
943        secret: &crate::protocol::types::Secret,
944        recovered_version: u32,
945    ) -> Result<Vec<DeRecEvent>> {
946        handlers::restore::restore(
947            &mut self.channel_store,
948            &mut self.share_store,
949            &mut self.secret_store,
950            &mut self.user_secret_store,
951            &self.transport,
952            &mut self.state_store,
953            &mut self.replica_id,
954            self.secret_id,
955            secret,
956            recovered_version,
957        )
958        .await
959    }
960
961    async fn start_pairing(
962        &mut self,
963        kind: derec_proto::SenderKind,
964        contact: derec_proto::ContactMessage,
965        peer_communication_info: HashMap<String, String>,
966    ) -> Result<Vec<DeRecEvent>> {
967        let channel_id = handlers::pairing::start(
968            &mut self.channel_store,
969            &mut self.secret_store,
970            &self.transport,
971            &self.own_transport,
972            &self.communication_info,
973            self.secret_id,
974            kind,
975            contact,
976            peer_communication_info,
977            self.replica_id,
978            self.parameter_range,
979        )
980        .await?;
981        Ok(vec![DeRecEvent::PairingStarted {
982            channel_id: ChannelId(channel_id),
983            kind,
984        }])
985    }
986
987    async fn start_discovery(
988        &mut self,
989        target: crate::protocol::types::Target,
990        reply_to: Option<derec_proto::TransportProtocol>,
991    ) -> Result<Vec<DeRecEvent>> {
992        let resolved =
993            handlers::resolve_target(&mut self.channel_store, self.secret_id, target.clone())
994                .await?;
995        handlers::require_role(
996            &self.channel_store,
997            self.secret_id,
998            &resolved,
999            derec_proto::SenderKind::Owner,
1000        )
1001        .await?;
1002        handlers::discovery::start(
1003            &mut self.channel_store,
1004            &mut self.secret_store,
1005            &self.transport,
1006            self.secret_id,
1007            target,
1008            reply_to,
1009        )
1010        .await
1011    }
1012
1013    async fn start_protect_secret(
1014        &mut self,
1015        secrets: Vec<crate::protocol::types::UserSecret>,
1016        description: Option<String>,
1017        reply_to: Option<derec_proto::TransportProtocol>,
1018    ) -> Result<Vec<DeRecEvent>> {
1019        self.publish_secret(secrets, description, reply_to).await
1020    }
1021
1022    /// Run one publish round: VSS-split for Helpers when the threshold is
1023    /// met, build the Replica composite payload with the share material
1024    /// embedded, and fan both out. A no-op (silent return) when no paired
1025    /// Helpers or Replicas exist.
1026    async fn publish_secret(
1027        &mut self,
1028        secrets: Vec<crate::protocol::types::UserSecret>,
1029        description: Option<String>,
1030        reply_to: Option<derec_proto::TransportProtocol>,
1031    ) -> Result<Vec<DeRecEvent>> {
1032        let Some(round) = handlers::sharing::start(
1033            &mut self.channel_store,
1034            &mut self.share_store,
1035            &mut self.secret_store,
1036            &mut self.user_secret_store,
1037            &self.transport,
1038            secrets,
1039            description,
1040            self.threshold,
1041            self.keep_versions_count,
1042            self.secret_id,
1043            reply_to,
1044            self.replica_id,
1045        )
1046        .await?
1047        else {
1048            return Ok(Vec::new());
1049        };
1050
1051        // Only channels whose dispatch succeeded count as `pending` in
1052        // the sharing round accumulator — a peer we couldn't reach on
1053        // send won't be responding, so it doesn't gate SharingComplete.
1054        let version = round.version;
1055        let pending: HashSet<ChannelId> = round
1056            .outcomes
1057            .iter()
1058            .filter_map(|(cid, r)| r.as_ref().ok().map(|_| *cid))
1059            .collect();
1060
1061        if !pending.is_empty() {
1062            self.state_store
1063                .save(
1064                    self.secret_id,
1065                    StateItem::SharingRound {
1066                        version,
1067                        pending,
1068                        confirmed: HashSet::new(),
1069                        failed: HashSet::new(),
1070                        started_at: now_secs(),
1071                    },
1072                )
1073                .await?;
1074        }
1075
1076        Ok(round
1077            .outcomes
1078            .into_iter()
1079            .map(|(channel_id, res)| match res {
1080                Ok(()) => DeRecEvent::ProtectSecretStarted {
1081                    channel_id,
1082                    version,
1083                },
1084                Err(e) => DeRecEvent::ProtectSecretFailed {
1085                    channel_id,
1086                    version,
1087                    error: e.to_string(),
1088                },
1089            })
1090            .collect())
1091    }
1092
1093    async fn start_verify_shares(
1094        &mut self,
1095        secret_id: u64,
1096        version: u32,
1097        target: crate::protocol::types::Target,
1098        reply_to: Option<derec_proto::TransportProtocol>,
1099    ) -> Result<Vec<DeRecEvent>> {
1100        let resolved =
1101            handlers::resolve_target(&mut self.channel_store, self.secret_id, target.clone())
1102                .await?;
1103        handlers::require_role(
1104            &self.channel_store,
1105            self.secret_id,
1106            &resolved,
1107            derec_proto::SenderKind::Owner,
1108        )
1109        .await?;
1110        handlers::verification::start(
1111            &mut self.channel_store,
1112            &mut self.secret_store,
1113            &self.transport,
1114            &mut self.state_store,
1115            version,
1116            target,
1117            secret_id,
1118            reply_to,
1119        )
1120        .await
1121    }
1122
1123    async fn start_recover_secret(
1124        &mut self,
1125        secret_id: u64,
1126        version: u32,
1127        reply_to: Option<derec_proto::TransportProtocol>,
1128    ) -> Result<Vec<DeRecEvent>> {
1129        let all_paired: Vec<crate::types::ChannelId> = self
1130            .channel_store
1131            .channels(self.secret_id)
1132            .await?
1133            .iter()
1134            .map(|c| c.id)
1135            .collect();
1136        handlers::require_role(
1137            &self.channel_store,
1138            self.secret_id,
1139            &all_paired,
1140            derec_proto::SenderKind::Owner,
1141        )
1142        .await?;
1143        handlers::recovery::start(
1144            &mut self.channel_store,
1145            &mut self.secret_store,
1146            &mut self.state_store,
1147            &self.transport,
1148            secret_id,
1149            version,
1150            reply_to,
1151        )
1152        .await
1153    }
1154
1155    async fn start_unpair(
1156        &mut self,
1157        channel_id: ChannelId,
1158        memo: Option<String>,
1159        reply_to: Option<derec_proto::TransportProtocol>,
1160    ) -> Result<Vec<DeRecEvent>> {
1161        handlers::require_role(
1162            &self.channel_store,
1163            self.secret_id,
1164            &[channel_id],
1165            derec_proto::SenderKind::Owner,
1166        )
1167        .await?;
1168        // The handler returns an immediate `Unpaired` event for the
1169        // `UnpairAck::NotRequired` path (interleaved after
1170        // `UnpairStarted` here); the wait-for-ack path surfaces
1171        // `Unpaired` later from `process()` on the response, or from
1172        // the timeout sweep.
1173        let mut events = vec![DeRecEvent::UnpairStarted { channel_id }];
1174        events.extend(
1175            handlers::unpairing::start(
1176                &mut self.channel_store,
1177                &mut self.share_store,
1178                &mut self.secret_store,
1179                &self.transport,
1180                &mut self.state_store,
1181                self.secret_id,
1182                channel_id,
1183                memo,
1184                self.unpair_ack,
1185                now_secs(),
1186                reply_to,
1187            )
1188            .await?,
1189        );
1190        Ok(events)
1191    }
1192
1193    async fn start_update_channel_info(
1194        &mut self,
1195        target: crate::protocol::types::Target,
1196        communication_info: Option<HashMap<String, String>>,
1197        transport_protocol: Option<derec_proto::TransportProtocol>,
1198    ) -> Result<Vec<DeRecEvent>> {
1199        handlers::update_channel_info::start(
1200            &mut self.channel_store,
1201            &mut self.secret_store,
1202            &self.transport,
1203            self.secret_id,
1204            target,
1205            communication_info,
1206            transport_protocol,
1207        )
1208        .await
1209    }
1210
1211    async fn accept_inner(&mut self, action: PendingAction) -> Result<Vec<DeRecEvent>> {
1212        match action {
1213            PendingAction::Pairing {
1214                channel_id,
1215                request,
1216                kind,
1217                trace_id,
1218                ..
1219            } => {
1220                handlers::pairing::accept(
1221                    &mut self.channel_store,
1222                    &mut self.secret_store,
1223                    &self.transport,
1224                    &self.communication_info,
1225                    self.secret_id,
1226                    channel_id,
1227                    &request,
1228                    kind,
1229                    trace_id,
1230                    self.replica_id,
1231                    self.parameter_range,
1232                )
1233                .await
1234            }
1235            PendingAction::StoreShare {
1236                channel_id,
1237                request,
1238                shared_key,
1239                trace_id,
1240            } => {
1241                handlers::sharing::accept(
1242                    &mut self.channel_store,
1243                    &mut self.share_store,
1244                    &self.transport,
1245                    self.secret_id,
1246                    channel_id,
1247                    &request,
1248                    &shared_key,
1249                    trace_id,
1250                )
1251                .await
1252            }
1253            PendingAction::VerifyShare {
1254                channel_id,
1255                request,
1256                shared_key,
1257                trace_id,
1258            } => {
1259                handlers::verification::accept(
1260                    &mut self.channel_store,
1261                    &mut self.share_store,
1262                    &self.transport,
1263                    self.secret_id,
1264                    channel_id,
1265                    &request,
1266                    &shared_key,
1267                    trace_id,
1268                )
1269                .await
1270            }
1271            PendingAction::Discovery {
1272                channel_id,
1273                request,
1274                shared_key,
1275                trace_id,
1276            } => {
1277                handlers::discovery::accept(
1278                    &mut self.channel_store,
1279                    &mut self.share_store,
1280                    &self.transport,
1281                    self.secret_id,
1282                    channel_id,
1283                    &request,
1284                    &shared_key,
1285                    trace_id,
1286                )
1287                .await
1288            }
1289            PendingAction::GetShare {
1290                channel_id,
1291                request,
1292                shared_key,
1293                trace_id,
1294            } => {
1295                handlers::recovery::accept(
1296                    &mut self.channel_store,
1297                    &mut self.share_store,
1298                    &self.transport,
1299                    self.secret_id,
1300                    channel_id,
1301                    &request,
1302                    &shared_key,
1303                    trace_id,
1304                )
1305                .await
1306            }
1307            PendingAction::Unpair {
1308                channel_id,
1309                request,
1310                shared_key,
1311                trace_id,
1312            } => {
1313                handlers::unpairing::accept(
1314                    &mut self.channel_store,
1315                    &mut self.share_store,
1316                    &mut self.secret_store,
1317                    &self.transport,
1318                    self.secret_id,
1319                    channel_id,
1320                    &request,
1321                    &shared_key,
1322                    trace_id,
1323                )
1324                .await
1325            }
1326            PendingAction::UpdateChannelInfo {
1327                channel_id,
1328                request,
1329                shared_key,
1330                trace_id,
1331            } => {
1332                handlers::update_channel_info::accept(
1333                    &mut self.channel_store,
1334                    &self.transport,
1335                    self.secret_id,
1336                    channel_id,
1337                    &request,
1338                    &shared_key,
1339                    trace_id,
1340                )
1341                .await
1342            }
1343            PendingAction::PrePair {
1344                channel_id,
1345                request,
1346                trace_id,
1347            } => {
1348                handlers::pairing::accept_pre_pair(
1349                    &mut self.secret_store,
1350                    &self.transport,
1351                    self.secret_id,
1352                    channel_id,
1353                    &request,
1354                    trace_id,
1355                )
1356                .await
1357            }
1358        }
1359    }
1360
1361    /// Auto-publish the cached secret if `events` contain a
1362    /// helper-side `PairingCompleted` (the freshly-paired Helper needs
1363    /// shares). The replica-side equivalent fires from
1364    /// `verify_fingerprint` once the channel leaves `Pending`.
1365    ///
1366    /// The payload comes from `user_secret_store.load_latest()` when one
1367    /// has been cached by an earlier `start(ProtectSecret)`. When no
1368    /// snapshot exists yet **and** at least one Replica Destination is
1369    /// already paired, the hook publishes an empty payload so the roster
1370    /// snapshot still reaches every Destination — that's how a
1371    /// multi-device sync stays consistent before the application has
1372    /// added any user secrets.
1373    async fn maybe_auto_publish_after_pair(
1374        &mut self,
1375        events: &[DeRecEvent],
1376    ) -> Result<Vec<DeRecEvent>> {
1377        let helper_just_paired = events.iter().any(|e| {
1378            matches!(
1379                e,
1380                DeRecEvent::PairingCompleted {
1381                    kind: derec_proto::SenderKind::Owner,
1382                    ..
1383                }
1384            )
1385        });
1386        if !helper_just_paired {
1387            return Ok(Vec::new());
1388        }
1389        let snapshot = self.user_secret_store.load_latest(self.secret_id).await?;
1390        let (secrets, description) = match snapshot {
1391            Some(s) => (s.secrets, s.description),
1392            None => {
1393                if !self.has_paired_replica_destination().await? {
1394                    return Ok(Vec::new());
1395                }
1396                (Vec::new(), None)
1397            }
1398        };
1399        let reply_to = self.auto_reply_to.then(|| self.own_transport.clone());
1400        self.publish_secret(secrets, description, reply_to).await
1401    }
1402
1403    /// Returns `true` when at least one channel carries the local
1404    /// `ReplicaSource` role in `Paired` status — i.e. the peer is a
1405    /// Replica Destination that is fully verified and eligible for
1406    /// secret sync.
1407    async fn has_paired_replica_destination(&self) -> Result<bool> {
1408        let channels = self.channel_store.channels(self.secret_id).await?;
1409        Ok(channels.iter().any(|c| {
1410            c.role == derec_proto::SenderKind::ReplicaSource
1411                && c.status == crate::protocol::types::ChannelStatus::Paired
1412        }))
1413    }
1414
1415    /// Walk the post-`process_inner` event list and apply the
1416    /// [`AutoAcceptPolicy`]: each `ActionRequired` whose action kind
1417    /// the policy opts into is replaced with `AutoAccepted` plus the
1418    /// flow events `accept_inner(action)` produces. Other events pass
1419    /// through unchanged.
1420    async fn apply_auto_accept(&mut self, events: Vec<DeRecEvent>) -> Result<Vec<DeRecEvent>> {
1421        let mut out = Vec::with_capacity(events.len());
1422        for event in events {
1423            match event {
1424                DeRecEvent::ActionRequired { channel_id, action }
1425                    if self.auto_accept.allows(&action) =>
1426                {
1427                    let action_kind = action.kind();
1428                    out.push(DeRecEvent::AutoAccepted {
1429                        channel_id,
1430                        action_kind,
1431                    });
1432                    let accept_events = self.accept_inner(action).await?;
1433                    out.extend(accept_events);
1434                }
1435                other => out.push(other),
1436            }
1437        }
1438        Ok(out)
1439    }
1440
1441    async fn process_inner(
1442        &mut self,
1443        message: &DeRecMessage,
1444        channel_id: ChannelId,
1445    ) -> Result<Vec<DeRecEvent>> {
1446        if self.is_message_expired(message, channel_id) {
1447            return Ok(vec![DeRecEvent::NoOp]);
1448        }
1449
1450        if let Some(events) = self.process_channel_message(message, channel_id).await? {
1451            return Ok(events);
1452        }
1453
1454        if let Some(events) = self.process_pairing_message(message, channel_id).await? {
1455            return Ok(events);
1456        }
1457
1458        #[cfg(feature = "logging")]
1459        tracing::warn!(channel_id = channel_id.0, "no key material for channel");
1460
1461        Err(Error::InvalidInput(
1462            "unknown channel_id: no shared key or pairing secret found",
1463        ))
1464    }
1465
1466    fn is_message_expired(
1467        &self,
1468        envelope: &DeRecMessage,
1469        #[cfg_attr(not(feature = "logging"), allow(unused))] channel_id: ChannelId,
1470    ) -> bool {
1471        let Some(ts) = &envelope.timestamp else {
1472            return false;
1473        };
1474        let msg_secs = ts.seconds as u64;
1475        let now = now_secs();
1476        let age = now.saturating_sub(msg_secs);
1477        if age > self.timeout_in_secs {
1478            #[cfg(feature = "logging")]
1479            tracing::warn!(
1480                channel_id = channel_id.0,
1481                message_age_secs = age,
1482                timeout_secs = self.timeout_in_secs,
1483                "message discarded — older than configured timeout"
1484            );
1485            return true;
1486        }
1487        false
1488    }
1489
1490    async fn process_channel_message(
1491        &mut self,
1492        message: &DeRecMessage,
1493        channel_id: ChannelId,
1494    ) -> Result<Option<Vec<DeRecEvent>>> {
1495        let Some(SecretValue::SharedKey(shared_key)) = self
1496            .secret_store
1497            .load(self.secret_id, channel_id, SecretKind::SharedKey)
1498            .await?
1499        else {
1500            return Ok(None);
1501        };
1502
1503        if let Some(channel) = self.channel_store.load(self.secret_id, channel_id).await? {
1504            if channel.status == crate::protocol::types::ChannelStatus::Pending {
1505                #[cfg(feature = "logging")]
1506                tracing::warn!(
1507                    channel_id = channel_id.0,
1508                    "message ignored — channel is pending fingerprint verification"
1509                );
1510                return Ok(Some(vec![DeRecEvent::NoOp]));
1511            }
1512        }
1513
1514        let events = handlers::handle(
1515            &mut self.channel_store,
1516            &mut self.share_store,
1517            &mut self.secret_store,
1518            &self.transport,
1519            &mut self.state_store,
1520            message,
1521            self.secret_id,
1522            channel_id,
1523            &shared_key,
1524        )
1525        .await?;
1526
1527        Ok(Some(events))
1528    }
1529
1530    async fn process_pairing_message(
1531        &mut self,
1532        message: &DeRecMessage,
1533        channel_id: ChannelId,
1534    ) -> Result<Option<Vec<DeRecEvent>>> {
1535        use derec_proto::MessageBody;
1536
1537        // Try the plaintext PrePair layer first. PrePair envelopes carry
1538        // a serialized `MessageBody` directly (no encryption — no shared
1539        // or asymmetric key exists yet), so they decode without crypto
1540        // material. ECIES ciphertext for the regular Pair flow won't
1541        // realistically decode to a valid `PrePair*` variant; if it ever
1542        // did, we fall through to the encrypted path below.
1543        if let Ok(inner) = crate::derec_message::extract_inner_plaintext_message(&message.message) {
1544            match inner {
1545                inner @ MessageBody::PrePairRequest(_) => {
1546                    // Initiator side. Two flavors:
1547                    // - HashedKeys: `PairingSecret` was saved at
1548                    //   `create_contact` time; the accept path publishes
1549                    //   its embedded keys.
1550                    // - NoKeys: only `PairingContact` was saved at
1551                    //   `create_contact_no_keys` time — no keys exist
1552                    //   until the accept path generates them on the fly.
1553                    // Route the message iff **either** correlation record
1554                    // exists; otherwise silently drop (unknown channel).
1555                    let has_pairing_secret = matches!(
1556                        self.secret_store
1557                            .load(self.secret_id, channel_id, SecretKind::PairingSecret)
1558                            .await?,
1559                        Some(SecretValue::PairingSecret(_))
1560                    );
1561                    let has_pairing_contact = matches!(
1562                        self.secret_store
1563                            .load(self.secret_id, channel_id, SecretKind::PairingContact)
1564                            .await?,
1565                        Some(SecretValue::PairingContact(_))
1566                    );
1567                    if !has_pairing_secret && !has_pairing_contact {
1568                        return Ok(None);
1569                    }
1570                    let events = handlers::pairing::handle_pre_pair_request(&inner, channel_id, message.trace_id)?;
1571                    return Ok(Some(events));
1572                }
1573                MessageBody::PrePairResponse(resp) => {
1574                    // Scanner side: needs the original HashedKeys contact
1575                    // (saved at `start` time) to validate the binding hash.
1576                    let Some(SecretValue::PairingContact(contact)) = self
1577                        .secret_store
1578                        .load(self.secret_id, channel_id, SecretKind::PairingContact)
1579                        .await?
1580                    else {
1581                        return Ok(None);
1582                    };
1583                    let events = handlers::pairing::on_pre_pair_response(
1584                        &mut self.channel_store,
1585                        &mut self.secret_store,
1586                        &self.transport,
1587                        &self.own_transport,
1588                        &self.communication_info,
1589                        self.secret_id,
1590                        channel_id,
1591                        &contact,
1592                        &resp,
1593                        self.replica_id,
1594                        self.parameter_range,
1595                    )
1596                    .await?;
1597                    return Ok(Some(events));
1598                }
1599                _ => {} // Fall through to the encrypted Pair path.
1600            }
1601        }
1602
1603        // Regular (encrypted) Pair flow.
1604        let Some(SecretValue::PairingSecret(pairing_secret)) = self
1605            .secret_store
1606            .load(self.secret_id, channel_id, SecretKind::PairingSecret)
1607            .await?
1608        else {
1609            return Ok(None);
1610        };
1611        let pairing_secret = pairing_secret.to_secret()?;
1612
1613        let events = handlers::handle_pairing(
1614            &mut self.channel_store,
1615            &mut self.secret_store,
1616            &self.transport,
1617            &self.communication_info,
1618            message,
1619            self.secret_id,
1620            channel_id,
1621            &pairing_secret,
1622            self.replica_id,
1623            self.parameter_range.as_ref(),
1624        )
1625        .await?;
1626        Ok(Some(events))
1627    }
1628
1629    /// Check if any channels in the active sharing round have timed out.
1630    ///
1631    /// Returns `ShareRejected` events for timed-out channels and moves them
1632    /// from `pending` to `failed` in the round tracker.
1633    async fn check_sharing_round_timeouts(&mut self) -> Vec<DeRecEvent> {
1634        let Ok(Some(StateItem::SharingRound {
1635            version,
1636            mut pending,
1637            confirmed,
1638            mut failed,
1639            started_at,
1640        })) = self
1641            .state_store
1642            .load(self.secret_id, StateKey::SharingRound)
1643            .await
1644        else {
1645            return vec![];
1646        };
1647        let now = now_secs();
1648        if now.saturating_sub(started_at) <= self.timeout_in_secs {
1649            return vec![];
1650        }
1651        let timed_out: Vec<ChannelId> = pending.drain().collect();
1652        let mut events = Vec::with_capacity(timed_out.len());
1653        for channel_id in timed_out {
1654            failed.insert(channel_id);
1655            events.push(DeRecEvent::ShareRejected {
1656                channel_id,
1657                version,
1658                status: StatusEnum::Fail as i32,
1659                memo: "timeout".to_owned(),
1660            });
1661
1662            #[cfg(feature = "logging")]
1663            tracing::warn!(
1664                channel_id = channel_id.0,
1665                version,
1666                "sharing round: helper timed out"
1667            );
1668        }
1669        // Persist the timeout-drained round so a subsequent
1670        // `update_sharing_round` can see the mutations.
1671        let _ = self
1672            .state_store
1673            .save(
1674                self.secret_id,
1675                StateItem::SharingRound {
1676                    version,
1677                    pending,
1678                    confirmed,
1679                    failed,
1680                    started_at,
1681                },
1682            )
1683            .await;
1684        events
1685    }
1686
1687    /// Drop local state for any pending unpair whose acknowledgement window has
1688    /// elapsed, returning an `Unpaired` event per dropped channel.
1689    async fn check_unpair_timeouts(&mut self) -> Vec<DeRecEvent> {
1690        let now = now_secs();
1691        let all = match self
1692            .state_store
1693            .load_all(self.secret_id, StateKind::PendingUnpair)
1694            .await
1695        {
1696            Ok(items) => items,
1697            Err(_) => return Vec::new(),
1698        };
1699        let expired: Vec<ChannelId> = all
1700            .into_iter()
1701            .filter_map(|item| match item {
1702                StateItem::PendingUnpair {
1703                    channel_id,
1704                    started_at,
1705                } if now.saturating_sub(started_at) > self.timeout_in_secs => Some(channel_id),
1706                _ => None,
1707            })
1708            .collect();
1709
1710        let mut events = Vec::with_capacity(expired.len());
1711        for cid in expired {
1712            let _ = self
1713                .state_store
1714                .remove(self.secret_id, StateKey::PendingUnpair { channel_id: cid })
1715                .await;
1716            if handlers::unpairing::drop_channel_state(
1717                &mut self.channel_store,
1718                &mut self.share_store,
1719                &mut self.secret_store,
1720                self.secret_id,
1721                cid,
1722            )
1723            .await
1724            .is_ok()
1725            {
1726                events.push(DeRecEvent::Unpaired { channel_id: cid });
1727            }
1728        }
1729        events
1730    }
1731
1732    /// Update the active sharing round based on events produced by `process_inner`.
1733    ///
1734    /// Moves channels from `pending` to `confirmed` or `failed` as
1735    /// `ShareConfirmed` / `ShareRejected` events arrive. When no channels
1736    /// remain pending, appends a [`DeRecEvent::SharingComplete`] summary.
1737    async fn update_sharing_round(&mut self, events: &mut Vec<DeRecEvent>) {
1738        let Ok(Some(StateItem::SharingRound {
1739            version: round_version,
1740            mut pending,
1741            mut confirmed,
1742            mut failed,
1743            started_at,
1744        })) = self
1745            .state_store
1746            .load(self.secret_id, StateKey::SharingRound)
1747            .await
1748        else {
1749            return;
1750        };
1751
1752        for event in events.iter() {
1753            match event {
1754                DeRecEvent::ShareConfirmed {
1755                    channel_id,
1756                    version,
1757                } if *version == round_version => {
1758                    pending.remove(channel_id);
1759                    confirmed.insert(*channel_id);
1760                }
1761                DeRecEvent::ShareRejected {
1762                    channel_id,
1763                    version,
1764                    ..
1765                } if *version == round_version => {
1766                    pending.remove(channel_id);
1767                    failed.insert(*channel_id);
1768                }
1769                _ => {}
1770            }
1771        }
1772
1773        let is_complete = pending.is_empty();
1774        let confirmed_count = confirmed.len();
1775        let failed_count = failed.len();
1776
1777        if is_complete {
1778            let threshold_met = confirmed_count >= self.threshold;
1779            let _ = self
1780                .state_store
1781                .remove(self.secret_id, StateKey::SharingRound)
1782                .await;
1783            events.push(DeRecEvent::SharingComplete {
1784                version: round_version,
1785                confirmed_count,
1786                failed_count,
1787                threshold_met,
1788            });
1789
1790            #[cfg(feature = "logging")]
1791            tracing::info!(
1792                version = round_version,
1793                confirmed_count,
1794                failed_count,
1795                threshold_met,
1796                "sharing round complete"
1797            );
1798        } else {
1799            let _ = self
1800                .state_store
1801                .save(
1802                    self.secret_id,
1803                    StateItem::SharingRound {
1804                        version: round_version,
1805                        pending,
1806                        confirmed,
1807                        failed,
1808                        started_at,
1809                    },
1810                )
1811                .await;
1812        }
1813    }
1814
1815    /// Remove pending channels that have exceeded the configured timeout,
1816    /// along with their associated pairing keys.
1817    ///
1818    /// Called automatically during [`process`](Self::process), but can also be
1819    /// invoked manually by the application.
1820    async fn cleanup_expired_channels(&mut self) -> Result<Vec<ChannelId>> {
1821        let now = now_secs();
1822        let timeout = self.timeout_in_secs;
1823        let channels = self.channel_store.channels(self.secret_id).await?;
1824
1825        let mut removed = Vec::new();
1826        for channel in channels {
1827            if channel.status == crate::protocol::types::ChannelStatus::Pending
1828                && now.saturating_sub(channel.created_at) > timeout
1829            {
1830                self.channel_store
1831                    .remove(self.secret_id, channel.id)
1832                    .await?;
1833                // Clean up any leftover pairing secret for this channel.
1834                let _ = self
1835                    .secret_store
1836                    .remove(self.secret_id, channel.id, SecretKind::PairingSecret)
1837                    .await;
1838                let _ = self
1839                    .secret_store
1840                    .remove(self.secret_id, channel.id, SecretKind::PairingContact)
1841                    .await;
1842
1843                #[cfg(feature = "logging")]
1844                tracing::info!(
1845                    channel_id = channel.id.0,
1846                    elapsed_secs = now.saturating_sub(channel.created_at),
1847                    "expired pending channel removed"
1848                );
1849
1850                removed.push(channel.id);
1851            }
1852        }
1853        Ok(removed)
1854    }
1855}