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}