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}