derec_library/primitives/sharing/request.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4use crate::primitives::sharing::SharingError;
5use crate::transport::TransportProtocolExt as _;
6use crate::{
7 derec_message::{DeRecMessageBuilder, current_timestamp, extract_inner_message},
8 types::{ChannelId, SharedKey},
9 utils::{generate_seed, verify_timestamps},
10};
11use derec_cryptography::vss;
12use derec_proto::{
13 CommittedDeRecShare, DeRecMessage, DeRecShare, MessageBody, StoreShareRequestMessage,
14 committed_de_rec_share::SiblingHash,
15};
16use prost::Message;
17use std::collections::HashMap;
18
19/// Current share algorithm identifier embedded into [`derec_proto::StoreShareRequestMessage`].
20///
21/// At present the sharing flow uses the library's VSS-based share generation and
22/// encodes that choice using the protocol value `0`.
23const SHARE_ALGORITHM_VSS: i32 = 0;
24/// `share` bytes carry a **full `Secret` payload** (`DeRecSecret` proto)
25/// instead of a single VSS share fragment. Used on replica channels —
26/// every replica holds an identical copy of the secret rather than a
27/// reconstructable fragment of it.
28///
29/// Disambiguates the payload semantics on the wire; the receiver's
30/// `Channel.role` is the authoritative source of truth, but a distinct
31/// `share_algorithm` value lets wire dumps and middle-boxes tell the two
32/// payload shapes apart without channel-state context.
33pub const SHARE_ALGORITHM_REPLICA_SECRET: i32 = 1;
34
35pub struct SplitResult {
36 pub shares: HashMap<ChannelId, CommittedDeRecShare>,
37}
38
39pub struct ProduceResult {
40 /// Serialized outer [`derec_proto::DeRecMessage`] envelope carrying an encrypted
41 /// [`derec_proto::StoreShareRequestMessage`] inner payload.
42 pub envelope: Vec<u8>,
43}
44
45pub struct ExtractResult {
46 pub request: StoreShareRequestMessage,
47}
48
49/// Splits a secret into verifiable committed shares, one per Helper channel.
50///
51/// In DeRec, the *sharing* flow splits a secret into independently verifiable
52/// shares using a Verifiable Secret Sharing (VSS) scheme. Each generated share is:
53///
54/// - Bound to a specific `secret_id` and `version`
55/// - Committed using a Merkle commitment and proof
56/// - Returned as a [`CommittedDeRecShare`] ready to be delivered to its Helper
57///
58/// To send a share to a Helper, pass the returned [`CommittedDeRecShare`] to
59/// [`produce`] together with the Helper's shared key to produce the encrypted
60/// delivery envelope.
61///
62/// # Deterministic channel/share assignment
63///
64/// The input `channels` slice is sorted by [`ChannelId`] before shares are assigned.
65/// Generated VSS shares are assigned in that sorted order. Duplicate channel IDs are
66/// rejected with [`SharingError::DuplicateChannelId`].
67///
68/// # Arguments
69///
70/// * `channels` - Slice of Helper [`ChannelId`] values. Each entry receives exactly
71/// one committed share. Must not be empty. Duplicate entries are rejected.
72/// * `secret_id` - Identifier of the secret being protected. Embedded into each
73/// generated share. Must not be empty.
74/// * `version` - Logical version of this secret distribution. Embedded into each
75/// generated share.
76/// * `secret_data` - Raw secret bytes to split using VSS. Must not be empty.
77/// * `threshold` - Minimum number of shares required to reconstruct the secret.
78/// Must satisfy `2 <= threshold <= channels.len()`.
79///
80/// # Returns
81///
82/// On success returns [`SplitResult`] containing:
83///
84/// - `shares`: `HashMap<ChannelId, CommittedDeRecShare>` — one committed share per channel.
85///
86/// # Errors
87///
88/// Returns [`crate::Error`] (specifically `Error::Sharing(...)`) in the following cases:
89///
90/// - [`SharingError::EmptyChannels`] if `channels` is empty
91/// - [`SharingError::EmptySecretData`] if `secret_data` is empty
92/// - [`SharingError::DuplicateChannelId`] if `channels` contains any repeated ID
93/// - [`SharingError::InvalidThreshold`] if `threshold` does not satisfy
94/// `2 <= threshold <= channels.len()`
95/// - [`SharingError::VssShareFailed`] if the underlying VSS algorithm fails
96///
97/// # Security Notes
98///
99/// - The caller is responsible for securely managing the original `secret_data`.
100///
101/// # Example
102///
103/// ```
104/// use derec_library::primitives::sharing::request::{split, SplitResult};
105/// use derec_library::types::ChannelId;
106///
107/// let channels = [ChannelId(1), ChannelId(2), ChannelId(3)];
108///
109/// let SplitResult { shares } = split(
110/// &channels,
111/// 1,
112/// 1,
113/// b"super_secret_value",
114/// 2,
115/// ).expect("split failed");
116///
117/// assert_eq!(shares.len(), 3);
118/// ```
119#[cfg_attr(
120 feature = "logging",
121 tracing::instrument(
122 skip_all,
123 fields(
124 channels_count = channels.len(),
125 version = version,
126 threshold = threshold,
127 secret_data_len = secret_data.as_ref().len(),
128 )
129 )
130)]
131pub fn split(
132 channels: &[ChannelId],
133 secret_id: u64,
134 version: u32,
135 secret_data: impl AsRef<[u8]>,
136 threshold: usize,
137) -> Result<SplitResult, crate::Error> {
138 let secret_data = secret_data.as_ref();
139
140 let ordered_channels = validate_split_inputs(channels, secret_data, threshold)?;
141 let channel_count = ordered_channels.len();
142
143 let vss_shares = generate_vss_shares(secret_data, threshold, channel_count)?;
144
145 let shares = build_shares(ordered_channels, vss_shares, secret_id, version);
146
147 #[cfg(feature = "logging")]
148 tracing::info!("secret split into shares");
149
150 Ok(SplitResult { shares })
151}
152
153/// Produces an encrypted [`derec_proto::DeRecMessage`] envelope wrapping a
154/// [`CommittedDeRecShare`] as a [`derec_proto::StoreShareRequestMessage`] inner payload.
155///
156/// Call this once for each share returned by [`split`], providing the corresponding
157/// Helper's shared key (established during pairing). The resulting wire bytes should be
158/// sent to the Helper over the channel transport. On the Helper side, the inner
159/// [`derec_proto::StoreShareRequestMessage`] extracted by [`extract`] must be retained
160/// for future verification and recovery flows.
161///
162/// # Arguments
163///
164/// * `channel_id` - The Helper channel this share belongs to.
165/// * `version` - Share-distribution version, matching the value passed to [`split`].
166/// * `secret_id` - Identifier of the secret being distributed. Must match the value
167/// passed to [`split`].
168/// * `committed_share` - The share for this channel, as returned by [`split`].
169/// * `keep_list` - Ordered list of version numbers the Helper should retain. Pass an empty
170/// slice to let the Helper apply its default retention policy.
171/// * `description` - Human-readable description of this share distribution. May be empty.
172/// * `shared_key` - Previously established 32-byte symmetric channel key used to
173/// encrypt the inner request.
174///
175/// # Returns
176///
177/// On success returns [`ProduceResult`] containing:
178///
179/// - `envelope`: serialized [`derec_proto::DeRecMessage`] wire bytes carrying an encrypted
180/// [`derec_proto::StoreShareRequestMessage`].
181///
182/// # Errors
183///
184/// Returns [`crate::Error`] if envelope construction or symmetric encryption fails.
185///
186/// # Example
187///
188/// ```
189/// use derec_library::primitives::sharing::request::{split, produce, SplitResult, ProduceResult};
190/// use derec_library::types::ChannelId;
191///
192/// let channels = [ChannelId(1), ChannelId(2), ChannelId(3)];
193/// let SplitResult { shares } = split(&channels, 1, 1, b"super_secret_value", 2)
194/// .expect("split failed");
195///
196/// let shared_key = [42u8; 32];
197/// let channel_id = ChannelId(1);
198/// let committed_share = shares.get(&channel_id).expect("missing share");
199///
200/// let ProduceResult { envelope } =
201/// produce(channel_id, 1, 1, committed_share, &[], "", &shared_key, None, None)
202/// .expect("produce failed");
203///
204/// assert!(!envelope.is_empty());
205/// ```
206#[allow(clippy::too_many_arguments)]
207#[cfg_attr(
208 feature = "logging",
209 tracing::instrument(skip_all, fields(channel_id = channel_id.0, version = version))
210)]
211pub fn produce(
212 channel_id: ChannelId,
213 version: u32,
214 secret_id: u64,
215 committed_share: &CommittedDeRecShare,
216 keep_list: &[u32],
217 description: impl Into<String>,
218 shared_key: &SharedKey,
219 reply_to: Option<derec_proto::TransportProtocol>,
220 replica_id: Option<u64>,
221) -> Result<ProduceResult, crate::Error> {
222 let timestamp = current_timestamp();
223
224 let msg = StoreShareRequestMessage {
225 share: committed_share.encode_to_vec(),
226 share_algorithm: SHARE_ALGORITHM_VSS,
227 version,
228 keep_list: keep_list.to_vec(),
229 version_description: description.into(),
230 timestamp: Some(timestamp),
231 secret_id,
232 reply_to,
233 replica_id,
234 };
235
236 let envelope = DeRecMessageBuilder::channel()
237 .channel_id(channel_id)
238 .timestamp(timestamp)
239 .message_body(MessageBody::StoreShareRequest(msg))
240 .encrypt(shared_key)?
241 .build()?
242 .encode_to_vec();
243
244 #[cfg(feature = "logging")]
245 tracing::info!("share request envelope produced");
246
247 Ok(ProduceResult { envelope })
248}
249
250/// Decrypts and decodes an incoming [`StoreShareRequestMessage`] from an outer envelope.
251///
252/// Call this on the **Helper** side after receiving a sharing request from an Owner.
253/// The function:
254///
255/// 1. Decodes the outer [`derec_proto::DeRecMessage`] envelope from `envelope_bytes`
256/// 2. Decrypts and decodes the inner [`StoreShareRequestMessage`] using `shared_key`
257/// 3. Validates the invariant `envelope.timestamp == request.timestamp`
258/// 4. If the request carries a `reply_to`, validates it via
259/// [`crate::transport::TransportProtocol::validate`] — same gate the
260/// orchestrator runs and the FFI/WASM seams enforce. A mismatched scheme
261/// or otherwise malformed peer-supplied endpoint is rejected here, so
262/// callers handing the extracted request to
263/// [`crate::primitives::sharing::response::produce`] can trust the
264/// `reply_to` is structurally sound. Note the asymmetry: [`produce`]
265/// on this side does NOT validate the application-supplied `reply_to`
266/// on outbound requests; the FFI/WASM seams do, and Rust-direct callers
267/// using [`produce`] are responsible for handing in a well-formed
268/// `reply_to` themselves.
269///
270/// The extracted [`StoreShareRequestMessage`] should be passed to
271/// [`crate::primitives::sharing::response::produce`] to build the acknowledgement
272/// response. The Helper must also persist the [`StoreShareRequestMessage`] itself (e.g.
273/// as serialized bytes via `.encode_to_vec()`) for future verification and recovery flows.
274///
275/// # Arguments
276///
277/// * `envelope_bytes` - Serialized [`derec_proto::DeRecMessage`] wire bytes received from
278/// the Owner, as produced by [`produce`].
279/// * `shared_key` - Previously established 32-byte symmetric channel key used to
280/// decrypt the inner message.
281///
282/// # Returns
283///
284/// On success returns [`ExtractResult`] containing:
285///
286/// - `request`: the decrypted inner [`derec_proto::StoreShareRequestMessage`].
287///
288/// # Errors
289///
290/// Returns [`crate::Error`] if:
291///
292/// - `envelope_bytes` cannot be decoded as a valid [`derec_proto::DeRecMessage`]
293/// - decryption or inner-message decoding fails
294/// - the inner message is not a [`derec_proto::StoreShareRequestMessage`]
295/// - `envelope.timestamp != request.timestamp`
296/// - [`crate::Error::Transport`] if `request.reply_to` is present and fails
297/// [`crate::transport::TransportProtocol::validate`] (unknown protocol
298/// discriminant, mismatched scheme, oversize URI, etc.)
299///
300/// # Security Notes
301///
302/// - The decrypted [`derec_proto::StoreShareRequestMessage`] carries the committed share the
303/// Helper must persist for future verification and recovery; treat it as sensitive material
304/// and store it securely.
305/// - **No freshness or replay protection.** The timestamp check
306/// enforced here only binds the envelope to the inner body
307/// (`envelope.timestamp == body.timestamp`). It does NOT enforce
308/// a freshness window against the receiver's clock and does NOT
309/// detect replays of a previously-captured ciphertext. Because
310/// the channel key is long-lived, a recorded envelope stays
311/// decryptable indefinitely. Callers MUST add a freshness window
312/// and per-channel anti-replay (monotonic counter or nonce log)
313/// on top before driving any side-effecting state off the parsed
314/// body.
315///
316/// # Example
317///
318/// ```
319/// use derec_library::primitives::sharing::request::{split, produce, extract, SplitResult, ProduceResult, ExtractResult};
320/// use derec_library::types::ChannelId;
321///
322/// let channels = [ChannelId(1), ChannelId(2), ChannelId(3)];
323/// let SplitResult { shares } = split(&channels, 1, 1, b"super_secret_value", 2)
324/// .expect("split failed");
325///
326/// let channel_id = ChannelId(1);
327/// let shared_key = [42u8; 32];
328/// let committed_share = shares.get(&channel_id).expect("missing share");
329///
330/// let ProduceResult { envelope } =
331/// produce(channel_id, 1, 1, committed_share, &[], "", &shared_key, None, None)
332/// .expect("produce failed");
333///
334/// let ExtractResult { request } = extract(&envelope, &shared_key).expect("extract failed");
335///
336/// assert_eq!(request.secret_id, 1);
337/// assert_eq!(request.version, 1);
338/// ```
339#[cfg_attr(
340 feature = "logging",
341 tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
342)]
343pub fn extract(
344 envelope_bytes: &[u8],
345 shared_key: &SharedKey,
346) -> Result<ExtractResult, crate::Error> {
347 let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;
348
349 let request = match extract_inner_message(&envelope.message, shared_key)? {
350 MessageBody::StoreShareRequest(message) => message,
351 _ => {
352 #[cfg(feature = "logging")]
353 tracing::warn!("unexpected message type; expected StoreShareRequestMessage");
354 return Err(crate::Error::Invariant(
355 "Invalid message. Expected: StoreShareRequestMessage",
356 ));
357 }
358 };
359
360 verify_timestamps(envelope.timestamp, request.timestamp)?;
361
362 if let Some(reply_to) = request.reply_to.as_ref() {
363 reply_to.validate()?;
364 }
365
366 #[cfg(feature = "logging")]
367 tracing::info!("share request extracted and validated");
368
369 Ok(ExtractResult { request })
370}
371
372fn generate_vss_shares(
373 secret_data: &[u8],
374 threshold: usize,
375 channels_len: usize,
376) -> Result<Vec<derec_cryptography::vss::VSSShare>, SharingError> {
377 let entropy = generate_seed::<32>();
378 let t = threshold as u64;
379 let n = channels_len as u64;
380
381 vss::share(t, n, secret_data, &entropy)
382 .map_err(|source| SharingError::VssShareFailed { source })
383}
384
385fn build_shares(
386 ordered_channels: Vec<ChannelId>,
387 vss_shares: Vec<derec_cryptography::vss::VSSShare>,
388 secret_id: u64,
389 version: u32,
390) -> HashMap<ChannelId, CommittedDeRecShare> {
391 let mut shares = HashMap::with_capacity(ordered_channels.len());
392
393 for (channel_id, share) in ordered_channels.into_iter().zip(vss_shares) {
394 let derec_share = DeRecShare {
395 encrypted_secret: share.encrypted_secret.clone(),
396 x: share.x.clone(),
397 y: share.y.clone(),
398 secret_id,
399 version,
400 };
401
402 let committed_derec_share = CommittedDeRecShare {
403 de_rec_share: derec_share.encode_to_vec(),
404 commitment: share.commitment.clone(),
405 merkle_path: share
406 .merkle_path
407 .iter()
408 .map(|(is_left, hash)| SiblingHash {
409 is_left: *is_left,
410 hash: hash.clone(),
411 })
412 .collect(),
413 };
414
415 shares.insert(channel_id, committed_derec_share);
416 }
417
418 shares
419}
420
421fn validate_split_inputs(
422 channels: &[ChannelId],
423 secret_data: &[u8],
424 threshold: usize,
425) -> Result<Vec<ChannelId>, crate::Error> {
426 if channels.is_empty() {
427 #[cfg(feature = "logging")]
428 tracing::warn!("channels list is empty");
429 return Err(SharingError::EmptyChannels.into());
430 }
431
432 if secret_data.is_empty() {
433 #[cfg(feature = "logging")]
434 tracing::warn!("secret_data is empty");
435 return Err(SharingError::EmptySecretData.into());
436 }
437
438 let mut ordered_channels: Vec<ChannelId> = channels.to_vec();
439 ordered_channels.sort();
440 for pair in ordered_channels.windows(2) {
441 if pair[0] == pair[1] {
442 #[cfg(feature = "logging")]
443 tracing::warn!("duplicate channel ID detected");
444 return Err(SharingError::DuplicateChannelId(pair[0].into()).into());
445 }
446 }
447
448 let channel_count = ordered_channels.len();
449
450 if threshold < 2 || threshold > channel_count {
451 #[cfg(feature = "logging")]
452 tracing::warn!(
453 threshold = threshold,
454 channels = channel_count,
455 "threshold out of valid range"
456 );
457 return Err(SharingError::InvalidThreshold {
458 threshold,
459 channels: channel_count,
460 }
461 .into());
462 }
463
464 Ok(ordered_channels)
465}