Skip to main content

derec_library/derec_message/
builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4//! Helpers for constructing the top-level [`DeRecMessage`] protocol envelope.
5//!
6//! In the current DeRec protocol model, all flow messages except `ContactMessage`
7//! are transported inside a [`DeRecMessage`] envelope.
8//!
9//! ## Design
10//!
11//! The outer [`DeRecMessage`] envelope carries protocol metadata such as:
12//!
13//! - protocol version
14//! - channel ID
15//! - sequence
16//! - timestamp
17//!
18//! The `message` field contains the serialized bytes of an inner flow message.
19//! In practice, those bytes are typically encrypted before being wrapped in the
20//! envelope.
21//!
22//! ## Responsibilities
23//!
24//! This module is responsible for:
25//!
26//! - building a valid [`DeRecMessage`] protobuf envelope
27//! - attaching protocol metadata
28//! - enforcing, at the type level, that encryption happens before [`build`](DeRecMessageBuilder::build)
29//! - restricting which encryption method can be used depending on the builder mode
30//!
31//! This module does **not**:
32//!
33//! - serialize flow-specific messages beyond encoding the provided protobuf message
34//! - decrypt payloads
35//! - verify signatures
36//! - interpret the meaning of the inner flow message
37//!
38//! ## Builder modes
39//!
40//! The builder supports two distinct modes:
41//!
42//! - [`PairingMode`] for pairing-envelope encryption using [`encrypt_pairing`](DeRecMessageBuilder::<NotEncrypted, PairingMode>::encrypt_pairing)
43//! - [`ChannelMode`] for channel-message encryption using [`encrypt`](DeRecMessageBuilder::<NotEncrypted, ChannelMode>::encrypt)
44//!
45//! The mode is part of the builder type, so the wrong encryption method cannot
46//! be called by mistake.
47//!
48//! ## Lifecycle
49//!
50//! ```text
51//! flow message (e.g. PairRequestMessage)
52//!     ↓ serialize (protobuf)
53//! encoded bytes
54//!     ↓ encrypt (pairing or channel, depending on flow)
55//! encrypted bytes
56//!     ↓ wrap using DeRecMessageBuilder
57//! DeRecMessage { message = encrypted_bytes }
58//!     ↓ serialize (protobuf)
59//! wire bytes
60//! ```
61//!
62//! ## Notes
63//!
64//! - `ContactMessage` is not wrapped in a [`DeRecMessage`]
65//! - the `message` field is treated as opaque payload bytes by this module
66//! - envelope fields are used only for routing, sequencing, and protocol metadata
67
68use std::marker::PhantomData;
69
70use crate::{
71    derec_message::DeRecMessageBuilderError,
72    protocol_version::ProtocolVersion,
73    types::ChannelId,
74};
75use derec_proto::{DeRecMessage, MessageBody};
76use prost_types::Timestamp;
77
78/// Typestate marker indicating that the payload has not yet been encrypted.
79#[derive(Debug)]
80pub struct NotEncrypted;
81
82/// Typestate marker indicating that the payload has already been encrypted and
83/// the envelope may be built.
84#[derive(Debug)]
85pub struct Encrypted;
86
87/// Builder mode for pairing messages.
88///
89/// In this mode, the builder only exposes `encrypt_pairing` (see the
90/// `impl DeRecMessageBuilder<NotEncrypted, PairingMode>` block).
91#[derive(Debug)]
92pub struct PairingMode;
93
94/// Builder mode for channel messages.
95///
96/// In this mode, the builder only exposes `encrypt` (see the
97/// `impl DeRecMessageBuilder<NotEncrypted, ChannelMode>` block).
98#[derive(Debug)]
99pub struct ChannelMode;
100
101/// Builds a [`DeRecMessage`] envelope containing protocol metadata and an
102/// encrypted payload.
103///
104/// The builder uses typestate and mode generics:
105///
106/// - `State` controls whether the payload is ready to build
107/// - `Mode` controls which encryption method is permitted
108///
109/// This ensures at compile time that:
110///
111/// - [`build`](DeRecMessageBuilder::build) cannot be called before encryption
112/// - a pairing builder cannot call channel encryption
113/// - a channel builder cannot call pairing encryption
114///
115/// # Type parameters
116///
117/// * `State` - encryption state marker, typically [`NotEncrypted`] or [`Encrypted`]
118/// * `Mode` - builder mode marker, either [`PairingMode`] or [`ChannelMode`]
119///
120/// # Required fields
121///
122/// Before calling [`build`](DeRecMessageBuilder::build), the builder must have:
123///
124/// - `channel_id`
125/// - `timestamp`
126/// - `message`
127/// - the appropriate encryption method applied
128///
129/// # Typical usage
130///
131/// Pairing mode:
132///
133/// ```rust,ignore
134/// let envelope = DeRecMessageBuilder::new()
135///     .channel_id(channel_id)
136///     .timestamp(current_timestamp())
137///     .message(&pair_request)
138///     .encrypt_pairing(helper_public_key)?
139///     .build()?;
140/// ```
141///
142/// Channel mode:
143///
144/// ```rust,ignore
145/// let envelope = DeRecMessageBuilder::channel()
146///     .channel_id(channel_id)
147///     .timestamp(current_timestamp())
148///     .message_body(MessageBody::VerifyShareRequest(&verify_request))
149///     .encrypt(shared_key)?
150///     .build()?;
151/// ```
152#[derive(Debug)]
153pub struct DeRecMessageBuilder<State, Mode> {
154    pub(crate) sequence: Option<u32>,
155    pub(crate) channel_id: Option<ChannelId>,
156    pub(crate) timestamp: Option<Timestamp>,
157    pub(crate) message: Option<MessageBody>,
158    pub(crate) trace_id: Option<u64>,
159    encrypted: Vec<u8>,
160    _state: PhantomData<State>,
161    _mode: PhantomData<Mode>,
162}
163
164impl<State, Mode> DeRecMessageBuilder<State, Mode> {
165    /// Sets the channel identifier for the envelope.
166    ///
167    /// In the DeRec protocol, `channel_id` identifies the logical communication
168    /// channel associated with the message.
169    ///
170    /// # Arguments
171    ///
172    /// * `channel_id` - channel identifier to embed in the envelope
173    ///
174    /// # Returns
175    ///
176    /// The updated builder.
177    pub fn channel_id(mut self, channel_id: ChannelId) -> Self {
178        self.channel_id = Some(channel_id);
179        self
180    }
181
182    /// Sets the sequence number for the envelope.
183    ///
184    /// The sequence number tracks message ordering and may also be used by
185    /// higher-level protocol logic such as key rotation or replay detection.
186    ///
187    /// # Arguments
188    ///
189    /// * `sequence` - monotonically increasing message counter
190    ///
191    /// # Returns
192    ///
193    /// The updated builder.
194    pub fn sequence(mut self, sequence: u32) -> Self {
195        self.sequence = Some(sequence);
196        self
197    }
198
199    /// Sets the timestamp for the envelope.
200    ///
201    /// # Arguments
202    ///
203    /// * `timestamp` - protobuf [`Timestamp`] representing message creation time
204    ///
205    /// # Returns
206    ///
207    /// The updated builder.
208    pub fn timestamp(mut self, timestamp: Timestamp) -> Self {
209        self.timestamp = Some(timestamp);
210        self
211    }
212
213    /// Sets the request/response correlation token for the envelope.
214    ///
215    /// On request envelopes the requester chooses any opaque `u64` to identify
216    /// the in-flight call. On response envelopes the responder echoes back the
217    /// value from the request. Both happens automatically when this builder is
218    /// driven by the standard request/response producers, but the value can
219    /// also be set explicitly by callers that need to correlate from outside.
220    ///
221    /// Defaults to zero when not set, which the protocol treats as "no
222    /// correlation requested."
223    ///
224    /// # Arguments
225    ///
226    /// * `trace_id` - opaque correlation token
227    ///
228    /// # Returns
229    ///
230    /// The updated builder.
231    pub fn trace_id(mut self, trace_id: u64) -> Self {
232        self.trace_id = Some(trace_id);
233        self
234    }
235
236    /// Sets a freshly-drawn random `trace_id` on the envelope.
237    ///
238    /// Convenience for request producers that want a new correlation token
239    /// without managing the RNG themselves. Uses the same random source as the
240    /// rest of the library (`rand::rng().next_u64()`).
241    ///
242    /// # Returns
243    ///
244    /// The updated builder with a random `trace_id`.
245    pub fn auto_trace_id(mut self) -> Self {
246        use rand::Rng as _;
247        self.trace_id = Some(rand::rng().next_u64());
248        self
249    }
250
251    /// Encodes the inner payload and sets the `message_type` discriminant from a [`MessageBody`].
252    ///
253    /// This is the canonical way to attach the inner message — `prost`-encodes the
254    /// payload **and** records the correct `i32` `message_type` value so that
255    /// [`DeRecMessageBuilder::build`] can embed it in the outer [`DeRecMessage`]
256    /// envelope.
257    ///
258    /// # Arguments
259    ///
260    /// * `body` - a [`MessageBody`] variant wrapping a reference to the inner protocol message
261    ///
262    /// # Returns
263    ///
264    /// The updated builder.
265    pub fn message_body(mut self, body: MessageBody) -> Self {
266        self.message = Some(body);
267        self
268    }
269}
270
271impl DeRecMessageBuilder<NotEncrypted, PairingMode> {
272    /// Creates a new pairing-mode [`DeRecMessageBuilder`].
273    ///
274    /// This constructor is intended for flows that use pairing-envelope
275    /// encryption. Builders created with this constructor can call
276    /// [`encrypt_pairing`](Self::encrypt_pairing) but cannot call channel
277    /// encryption.
278    ///
279    /// # Returns
280    ///
281    /// A new empty builder in pairing mode.
282    ///
283    /// # Example
284    ///
285    /// ```rust,ignore
286    /// let builder = DeRecMessageBuilder::new();
287    /// ```
288    pub fn pairing() -> Self {
289        Self {
290            sequence: None,
291            channel_id: None,
292            timestamp: None,
293            message: None,
294            trace_id: None,
295            encrypted: Vec::new(),
296            _state: PhantomData,
297            _mode: PhantomData,
298        }
299    }
300
301    /// Encrypts the encoded payload using pairing-envelope encryption.
302    ///
303    /// This method is only available on builders created in [`PairingMode`].
304    /// After successful encryption, the builder transitions to the [`Encrypted`]
305    /// state, enabling [`build`](DeRecMessageBuilder::build).
306    ///
307    /// # Arguments
308    ///
309    /// * `public_key` - recipient public key used for asymmetric pairing encryption
310    ///
311    /// # Returns
312    ///
313    /// On success, returns a new builder in the [`Encrypted`] state.
314    ///
315    /// # Errors
316    ///
317    /// Returns [`DeRecMessageBuilderError`] if:
318    ///
319    /// - [`DeRecMessageBuilderError::MissingMessage`] if no payload was set
320    /// - the underlying pairing encryption routine fails
321    pub fn encrypt_pairing(
322        self,
323        public_key: impl AsRef<[u8]>,
324    ) -> Result<DeRecMessageBuilder<Encrypted, PairingMode>, DeRecMessageBuilderError> {
325        if self.message.is_none() {
326            return Err(DeRecMessageBuilderError::MissingMessage);
327        }
328
329        let encoded = self.message.unwrap().encode_to_vec();
330        let encrypted =
331            derec_cryptography::pairing::envelope::encrypt(&encoded, public_key.as_ref())?;
332
333        Ok(DeRecMessageBuilder {
334            message: None,
335            encrypted,
336            timestamp: self.timestamp,
337            sequence: self.sequence,
338            channel_id: self.channel_id,
339            trace_id: self.trace_id,
340            _state: PhantomData,
341            _mode: PhantomData,
342        })
343    }
344}
345
346impl DeRecMessageBuilder<NotEncrypted, ChannelMode> {
347    /// Creates a new channel-mode [`DeRecMessageBuilder`].
348    ///
349    /// This constructor is intended for flows that use symmetric channel
350    /// encryption. Builders created with this constructor can call
351    /// [`encrypt`](Self::encrypt) but cannot call pairing encryption.
352    ///
353    /// # Returns
354    ///
355    /// A new empty builder in channel mode.
356    ///
357    /// # Example
358    ///
359    /// ```rust,ignore
360    /// let builder = DeRecMessageBuilder::channel();
361    /// ```
362    pub fn channel() -> Self {
363        Self {
364            sequence: None,
365            channel_id: None,
366            timestamp: None,
367            message: None,
368            trace_id: None,
369            encrypted: Vec::new(),
370            _state: PhantomData,
371            _mode: PhantomData,
372        }
373    }
374
375    /// Encrypts the encoded payload using channel encryption.
376    ///
377    /// This method is only available on builders created in [`ChannelMode`].
378    /// The nonce is derived from the channel ID by placing the big-endian
379    /// `u64` channel identifier into the last 8 bytes of a 32-byte nonce.
380    ///
381    /// After successful encryption, the builder transitions to the
382    /// [`Encrypted`] state, enabling [`build`](DeRecMessageBuilder::build).
383    ///
384    /// # Arguments
385    ///
386    /// * `shared_key` - 32-byte symmetric channel key
387    ///
388    /// # Returns
389    ///
390    /// On success, returns a new builder in the [`Encrypted`] state.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`DeRecMessageBuilderError`] if:
395    ///
396    /// - [`DeRecMessageBuilderError::MissingMessage`] if no payload was set
397    /// - [`DeRecMessageBuilderError::MissingChannelId`] if `channel_id` was not set
398    /// - the underlying channel encryption routine fails
399    pub fn encrypt(
400        self,
401        shared_key: &[u8; 32],
402    ) -> Result<DeRecMessageBuilder<Encrypted, ChannelMode>, DeRecMessageBuilderError> {
403        if self.message.is_none() {
404            return Err(DeRecMessageBuilderError::MissingMessage);
405        }
406
407        let channel_id = self
408            .channel_id
409            .ok_or(DeRecMessageBuilderError::MissingChannelId)?;
410
411        let mut nonce = [0u8; 32];
412        nonce[24..].copy_from_slice(&u64::from(channel_id).to_be_bytes());
413
414        let encoded = self.message.unwrap().encode_to_vec();
415        let encrypted = derec_cryptography::channel::encrypt_message(&encoded, shared_key, &nonce)?;
416
417        Ok(DeRecMessageBuilder {
418            message: None,
419            encrypted,
420            timestamp: self.timestamp,
421            sequence: self.sequence,
422            channel_id: Some(channel_id),
423            trace_id: self.trace_id,
424            _state: PhantomData,
425            _mode: PhantomData,
426        })
427    }
428}
429
430impl<Mode> DeRecMessageBuilder<Encrypted, Mode> {
431    /// Builds the final [`DeRecMessage`] envelope.
432    ///
433    /// This method is only available after one of the permitted encryption
434    /// methods has been called successfully.
435    ///
436    /// # Returns
437    ///
438    /// On success returns a [`DeRecMessage`] containing:
439    ///
440    /// - protocol version from [`ProtocolVersion::current`]
441    /// - `channel_id`
442    /// - `sequence` or `0` if no sequence was set
443    /// - `timestamp`
444    /// - `message` containing the encrypted payload bytes
445    ///
446    /// # Errors
447    ///
448    /// Returns [`DeRecMessageBuilderError`] if:
449    ///
450    /// - [`DeRecMessageBuilderError::MissingChannelId`] if `channel_id` was not set
451    /// - [`DeRecMessageBuilderError::MissingTimestamp`] if `timestamp` was not set
452    /// - [`DeRecMessageBuilderError::MissingMessage`] if the encrypted payload is empty
453    ///
454    /// # Notes
455    ///
456    /// This function does not perform any additional cryptographic validation.
457    /// It only validates that the required envelope fields are present.
458    pub fn build(self) -> Result<DeRecMessage, DeRecMessageBuilderError> {
459        let protocol_version = ProtocolVersion::current();
460
461        if self.timestamp.is_none() {
462            return Err(DeRecMessageBuilderError::MissingTimestamp);
463        }
464
465        if self.encrypted.is_empty() {
466            return Err(DeRecMessageBuilderError::MissingMessage);
467        }
468
469        Ok(DeRecMessage {
470            protocol_version_major: protocol_version.major,
471            protocol_version_minor: protocol_version.minor,
472            sequence: self.sequence.unwrap_or_default(),
473            channel_id: self
474                .channel_id
475                .ok_or(DeRecMessageBuilderError::MissingChannelId)?
476                .into(),
477            timestamp: Some(
478                self.timestamp
479                    .ok_or(DeRecMessageBuilderError::MissingTimestamp)?,
480            ),
481            message: self.encrypted,
482            trace_id: self.trace_id.unwrap_or_default(),
483        })
484    }
485}
486
487/// Returns the current system time as a protobuf [`Timestamp`].
488///
489/// This helper converts the current system time into a UTC timestamp suitable
490/// for embedding into a [`DeRecMessage`] envelope.
491///
492/// # Returns
493///
494/// A [`Timestamp`] containing:
495///
496/// - `seconds`: whole seconds since the Unix epoch
497/// - `nanos`: nanosecond offset within the current second
498///
499/// # Panics
500///
501/// Panics if the system clock is earlier than the Unix epoch.
502#[cfg(not(target_arch = "wasm32"))]
503pub fn current_timestamp() -> Timestamp {
504    use std::time::{SystemTime, UNIX_EPOCH};
505
506    let now = SystemTime::now()
507        .duration_since(UNIX_EPOCH)
508        .expect("time went backwards");
509
510    Timestamp {
511        seconds: now.as_secs() as i64,
512        nanos: now.subsec_nanos() as i32,
513    }
514}
515
516#[cfg(target_arch = "wasm32")]
517pub fn current_timestamp() -> Timestamp {
518    let millis = js_sys::Date::now() as i64;
519
520    Timestamp {
521        seconds: millis / 1000,
522        nanos: ((millis % 1000) * 1_000_000) as i32,
523    }
524}