derec_library/primitives/pairing/request.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4use crate::primitives::pairing::PairingError;
5use crate::transport::TransportProtocolExt as _;
6use crate::utils::{ContactMessageExt as _, verify_timestamps};
7use crate::{
8 derec_message::{DeRecMessageBuilder, current_timestamp},
9 protocol_version::ProtocolVersion,
10 types::ChannelId,
11 utils::generate_seed,
12};
13use derec_cryptography::pairing::{
14 self as cryptography_pairing, PairingContactMessageMaterial, PairingSecretKeyMaterial,
15};
16use derec_proto::{
17 CommunicationInfo, ContactMessage, ContactMode, DeRecMessage, MessageBody, PairRequestMessage,
18 PrePairRequestMessage, SenderKind, TransportProtocol,
19};
20use prost::Message;
21use rand::{Rng, rng};
22
23pub struct CreateContactResult {
24 pub contact_message: ContactMessage,
25 /// Fresh pairing secret material tied to the keys the contact
26 /// creator has committed to.
27 ///
28 /// - [`ContactMode::InlineKeys`] / [`ContactMode::HashedKeys`]:
29 /// `Some(...)`. Callers MUST persist it — it is required later
30 /// to finalize pairing (decrypt the incoming `PairRequest` and
31 /// derive the shared key).
32 /// - [`ContactMode::NoKeys`]: `None`. No key material exists at
33 /// contact-creation time; the contact creator generates it on
34 /// the fly when the corresponding `PrePairRequest` arrives.
35 pub secret_key: Option<PairingSecretKeyMaterial>,
36}
37
38pub struct ProduceResult {
39 /// Serialized outer [`derec_proto::DeRecMessage`] wire bytes carrying an encrypted inner
40 /// [`derec_proto::PairRequestMessage`]. Ready to send over transport.
41 pub envelope: Vec<u8>,
42 pub initiator_contact_message: ContactMessage,
43 pub secret_key: PairingSecretKeyMaterial,
44}
45
46pub struct ExtractResult {
47 pub request: PairRequestMessage,
48}
49
50pub struct ProducePrePairResult {
51 /// Serialized outer [`derec_proto::DeRecMessage`] wire bytes carrying a **plaintext**
52 /// inner [`derec_proto::PrePairRequestMessage`]. Ready to send over transport.
53 pub envelope: Vec<u8>,
54}
55
56pub struct PrePairExtractResult {
57 pub request: PrePairRequestMessage,
58}
59
60/// Creates a [`derec_proto::ContactMessage`] used to bootstrap the DeRec *pairing* flow.
61///
62/// In DeRec, pairing begins with an **out-of-band contact transfer** (typically QR or
63/// another side channel). Unlike normal DeRec protocol traffic, the contact message is
64/// **not wrapped in a `DeRecMessage` envelope** and is **not encrypted**. It is sent as
65/// plain protobuf bytes (serialize the returned `contact_message` with `.encode_to_vec()`).
66///
67/// Single entry point for all three `contact_mode` variants. Mode-specific
68/// assembly happens in private helpers (`create_contact_inlined_keys`,
69/// `create_contact_hashed_keys`, `create_contact_no_keys`) invoked here.
70///
71/// # Arguments
72///
73/// * `channel_id` — Identifier embedded in the contact and copied into subsequent
74/// pairing messages by the recipient.
75/// * `contact_mode` — Selects how the initiator's public pairing material is delivered:
76/// - [`ContactMode::InlineKeys`]: keys are embedded directly in the contact.
77/// - [`ContactMode::HashedKeys`]: only a SHA-384 binding hash is embedded; the peer
78/// obtains the keys via a `PrePair` round-trip and verifies against the hash. The
79/// transport endpoint advertised here MUST be ephemeral — the plaintext `PrePair*`
80/// traffic must not be linkable to a long-lived endpoint.
81/// - [`ContactMode::NoKeys`]: no key material and no commitment. Keys are generated
82/// on the fly by the creator when the corresponding `PrePairRequest` arrives.
83/// Trust rests entirely on the OOB delivery channel being fully trusted.
84/// * `transport_protocol` — Endpoint the recipient uses to reach this initiator with
85/// the next protocol message. The `uri` field must not be empty.
86/// * `nonce` — Correlation nonce embedded in the contact.
87/// - `None`: the library generates a fresh cryptographically-random `u64`. Suitable
88/// default for `InlineKeys` / `HashedKeys` where the nonce is a security parameter.
89/// - `Some(n)`: application-controlled value. Required for `NoKeys` where callers
90/// typically pick a small human-typable value (4–6 decimal digits) for manual entry.
91/// Also valid for `InlineKeys` / `HashedKeys` if the app wants deterministic control.
92///
93/// # Returns
94///
95/// [`CreateContactResult`] with:
96///
97/// - `contact_message`: decoded [`ContactMessage`] — serialize with `.encode_to_vec()`
98/// before sending out-of-band.
99/// - `secret_key`: `Some(...)` for `InlineKeys` and `HashedKeys` (must be persisted);
100/// `None` for `NoKeys` (no key material at contact-creation time).
101///
102/// # Errors
103///
104/// - [`PairingError::EmptyTransportUri`] if `transport_protocol.uri` is empty.
105/// - [`PairingError::ContactMessageKeygen`] if pairing key generation fails
106/// (`InlineKeys` / `HashedKeys` only — `NoKeys` skips keygen).
107///
108/// # Example
109///
110/// ```
111/// use derec_library::primitives::pairing::request;
112/// use derec_library::types::ChannelId;
113/// use derec_proto::{ContactMode, Protocol, TransportProtocol};
114///
115/// let request::CreateContactResult {
116/// contact_message,
117/// secret_key,
118/// } = request::create_contact(
119/// ChannelId(42),
120/// ContactMode::InlineKeys,
121/// TransportProtocol {
122/// uri: "https://relay.example/derec".to_owned(),
123/// protocol: Protocol::Https.into(),
124/// },
125/// None,
126/// ).expect("Failed to create contact message");
127///
128/// assert!(secret_key.is_some());
129/// let _ = contact_message;
130/// ```
131#[cfg_attr(
132 feature = "logging",
133 tracing::instrument(skip_all, fields(channel_id = channel_id.0, contact_mode = contact_mode as i32))
134)]
135pub fn create_contact(
136 channel_id: ChannelId,
137 contact_mode: ContactMode,
138 transport_protocol: TransportProtocol,
139 nonce: Option<u64>,
140) -> Result<CreateContactResult, crate::Error> {
141 if transport_protocol.uri.trim().is_empty() {
142 #[cfg(feature = "logging")]
143 tracing::warn!("transport URI is empty");
144
145 return Err(PairingError::EmptyTransportUri.into());
146 }
147
148 let nonce = nonce.unwrap_or_else(|| rng().next_u64());
149
150 let (contact_message, secret_key) = match contact_mode {
151 ContactMode::InlineKeys => {
152 let (pk, sk) = generate_pairing_keys()?;
153 let msg = ContactMessage::inline_keys(channel_id, nonce, transport_protocol, pk);
154 (msg, Some(PairingSecretKeyMaterial::Initiator(sk)))
155 }
156 ContactMode::HashedKeys => {
157 let (pk, sk) = generate_pairing_keys()?;
158 let msg = ContactMessage::hashed_keys(channel_id, nonce, transport_protocol, &pk);
159 (msg, Some(PairingSecretKeyMaterial::Initiator(sk)))
160 }
161 ContactMode::NoKeys => {
162 let msg = ContactMessage::no_keys(channel_id, nonce, transport_protocol);
163 (msg, None)
164 }
165 };
166
167 #[cfg(feature = "logging")]
168 tracing::info!("contact message created");
169
170 Ok(CreateContactResult {
171 contact_message,
172 secret_key,
173 })
174}
175
176/// Produces a pairing request [`derec_proto::DeRecMessage`] envelope, continuing the DeRec
177/// pairing flow.
178///
179/// This function is executed by the **Responder** (the party that scanned or otherwise
180/// received the initiator's contact out-of-band).
181///
182/// Under the current protocol model:
183///
184/// 1. The initiator sends a [`derec_proto::ContactMessage`] out-of-band
185/// 2. The responder decodes that contact, performs the responder-side pairing-request
186/// cryptographic step, and constructs a [`derec_proto::PairRequestMessage`]
187/// 3. The inner [`derec_proto::PairRequestMessage`] is protobuf-serialized and then encrypted
188/// using the initiator's public ECIES key
189/// 4. The encrypted bytes are placed into a plain [`derec_proto::DeRecMessage`] envelope
190/// 5. The final result is serialized envelope bytes ready to be sent over the transport
191///
192/// Because pairing happens *before* a shared symmetric key exists, this function uses the
193/// pairing-specific **asymmetric** encryption mechanism for the inner message.
194///
195/// The returned [`derec_cryptography::pairing::PairingSecretKeyMaterial`] must be retained
196/// locally and later used to finalize pairing when the response arrives.
197///
198/// # Arguments
199///
200/// * `kind` - Role of the sender within the DeRec protocol (for example
201/// `Owner`, `Helper`, or `Replica`)
202/// * `transport_protocol` - Transport endpoint the initiator can use to reach this responder
203/// for subsequent protocol traffic. The `uri` field must not be empty or whitespace-only.
204/// * `contact_message` - The decoded [`derec_proto::ContactMessage`] received from the
205/// initiator, as returned by [`create_contact`] and decoded by the caller.
206/// * `communication_info` - Optional application-level identity metadata to advertise to the
207/// peer (free-form key/value pairs). Pass `None` to send no metadata; the protocol treats
208/// this as opaque.
209///
210/// # Returns
211///
212/// On success returns [`ProduceResult`] containing:
213///
214/// - `envelope`: serialized outer [`derec_proto::DeRecMessage`] envelope bytes
215/// - `initiator_contact_message`: the decoded initiator [`derec_proto::ContactMessage`],
216/// providing transport endpoint, public keys, channel identifier, and nonce
217/// - `secret_key`: responder-side pairing secret state required later to derive the final
218/// shared pairing key
219///
220/// # Errors
221///
222/// Returns [`crate::Error`] (specifically `Error::Pairing(...)`) in the following cases:
223///
224/// - [`PairingError::EmptyTransportUri`] if `transport_protocol.uri` is empty or whitespace
225/// - [`PairingError::InvalidContactMessage`] if required contact fields are missing or the
226/// contact transport protocol is absent or has an empty URI
227/// - [`PairingError::PairRequestKeygen`] if ML-KEM encapsulation or key generation fails
228/// - [`PairingError::PairingEncryption`] if inner-message encryption fails
229///
230/// # Security Notes
231///
232/// - The `contact_message` is peer-provided data; validate all required fields before use.
233/// - The returned secret key material must be securely retained by the responder.
234///
235/// # Example
236///
237/// ```
238/// use derec_library::primitives::pairing::request;
239/// use derec_library::types::ChannelId;
240/// use derec_proto::{ContactMode, Protocol, SenderKind, TransportProtocol};
241///
242/// // Initiator side: create a contact message out-of-band.
243/// let request::CreateContactResult { contact_message, .. } = request::create_contact(
244/// ChannelId(42),
245/// ContactMode::InlineKeys,
246/// TransportProtocol {
247/// uri: "https://relay.example/initiator".to_owned(),
248/// protocol: Protocol::Https.into(),
249/// },
250/// None,
251/// ).expect("create_contact failed");
252///
253/// // Responder side: build the pairing request envelope from the received contact.
254/// let request::ProduceResult { envelope, .. } = request::produce(
255/// SenderKind::Helper,
256/// TransportProtocol {
257/// uri: "https://relay.example/responder".to_owned(),
258/// protocol: Protocol::Https.into(),
259/// },
260/// &contact_message,
261/// None,
262/// None,
263/// ).expect("produce failed");
264///
265/// assert!(!envelope.is_empty());
266/// ```
267#[cfg_attr(
268 feature = "logging",
269 tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id, kind = kind as i32))
270)]
271pub fn produce(
272 kind: SenderKind,
273 transport_protocol: TransportProtocol,
274 contact_message: &ContactMessage,
275 communication_info: Option<CommunicationInfo>,
276 parameter_range: Option<derec_proto::ParameterRange>,
277) -> Result<ProduceResult, crate::Error> {
278 validate_inputs(
279 &transport_protocol,
280 contact_message,
281 ContactMode::InlineKeys,
282 )?;
283
284 let (pairing_request_key_material, secret_key) =
285 create_pairing_request_material(contact_message)?;
286
287 let timestamp = current_timestamp();
288
289 let request = PairRequestMessage {
290 sender_kind: kind.into(),
291 mlkem_ciphertext: pairing_request_key_material.mlkem_ciphertext,
292 ecies_public_key: pairing_request_key_material.ecies_public_key.clone(),
293 nonce: contact_message.nonce,
294 communication_info,
295 parameter_range,
296 transport_protocol: Some(transport_protocol),
297 timestamp: Some(timestamp),
298 };
299
300 // Encrypt with the INITIATOR's ECIES public key (from the contact) —
301 // only the initiator's matching secret key can decrypt. The
302 // responder's own freshly-generated pubkey travels in
303 // `request.ecies_public_key` (above) for the initiator to ECDH against
304 // when finishing the pairing; it is NOT the encryption key here.
305 let envelope = DeRecMessageBuilder::pairing()
306 .channel_id(contact_message.channel_id.into())
307 .timestamp(timestamp)
308 .message_body(MessageBody::PairRequest(request))
309 .encrypt_pairing(
310 contact_message
311 .ecies_public_key
312 .as_ref()
313 .expect("validate_inputs guarantees ecies_public_key is Some"),
314 )?
315 .build()?
316 .encode_to_vec();
317
318 #[cfg(feature = "logging")]
319 tracing::info!("pairing request envelope produced");
320
321 Ok(ProduceResult {
322 envelope,
323 initiator_contact_message: contact_message.clone(),
324 secret_key: PairingSecretKeyMaterial::Responder(secret_key),
325 })
326}
327
328/// Produces a `PrePairRequestMessage` envelope, the first step of the
329/// [`ContactMode::HashedKeys`] pairing flow.
330///
331/// When a [`ContactMessage`] arrives with `contactMode == HASHED_KEYS`, it carries
332/// only a SHA-384 commitment to the initiator's public keys (so the contact stays
333/// small enough for a QR code) — the actual ML-KEM and ECIES keys must be fetched
334/// over the wire via `PrePair` before a [`PairRequestMessage`] can be built. This
335/// function builds that fetch envelope on the responder (scanner) side.
336///
337/// The inner [`PrePairRequestMessage`] is **plaintext** — no shared key exists yet
338/// and the keys it asks for cannot themselves be used for encryption — so the outer
339/// [`DeRecMessage`] envelope is constructed directly rather than via the
340/// encryption-enforcing [`DeRecMessageBuilder`]. The envelope's `channelId` is
341/// taken from the [`ContactMessage`] so the contact creator can correlate the
342/// request with the right local pairing state.
343///
344/// # Arguments
345///
346/// * `transport_protocol` - Transport endpoint the contact creator should use to
347/// send the [`derec_proto::PrePairResponseMessage`] back. The `uri` field must
348/// not be empty or whitespace-only. Because [`PrePair`-messages][PrePairRequestMessage]
349/// travel as plaintext, this endpoint MUST be ephemeral (see the security note
350/// on `PrePairRequestMessage`).
351/// * `contact_message` - The decoded [`ContactMessage`] received out-of-band. Its
352/// `contact_mode` must be [`ContactMode::HashedKeys`]; for
353/// [`ContactMode::InlineKeys`] the responder already has the keys and should
354/// call [`produce`] directly instead.
355///
356/// # Returns
357///
358/// On success returns [`ProducePrePairResult`] containing the serialized outer
359/// [`DeRecMessage`] envelope bytes.
360///
361/// # Errors
362///
363/// Returns [`crate::Error`] (specifically `Error::Pairing(...)`) in the following cases:
364///
365/// - [`PairingError::EmptyTransportUri`] if `transport_protocol.uri` is empty or whitespace
366/// - [`PairingError::InvalidContactMessage`] if `contact_message.contact_mode` is neither
367/// [`ContactMode::HashedKeys`] nor [`ContactMode::NoKeys`] — including
368/// [`ContactMode::InlineKeys`] (which carries the keys inline and has no
369/// PrePair step) and any unknown enum value
370///
371/// # Security Notes
372///
373/// - The envelope is plaintext; do not include any sensitive material beyond
374/// what `PrePairRequestMessage` already exposes.
375/// - The transport endpoint advertised here is visible to passive observers.
376#[cfg_attr(
377 feature = "logging",
378 tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id))
379)]
380pub fn produce_pre_pair_request(
381 transport_protocol: TransportProtocol,
382 contact_message: &ContactMessage,
383) -> Result<ProducePrePairResult, crate::Error> {
384 validate_pre_pair_inputs(&transport_protocol, contact_message)?;
385
386 let timestamp = current_timestamp();
387 let request = PrePairRequestMessage {
388 nonce: contact_message.nonce,
389 transport_protocol: Some(transport_protocol),
390 timestamp: Some(timestamp),
391 };
392
393 let protocol_version = ProtocolVersion::current();
394 let envelope = DeRecMessage {
395 protocol_version_major: protocol_version.major,
396 protocol_version_minor: protocol_version.minor,
397 sequence: 0,
398 channel_id: contact_message.channel_id,
399 timestamp: Some(timestamp),
400 message: MessageBody::PrePairRequest(request).encode_to_vec(),
401 trace_id: 0,
402 }
403 .encode_to_vec();
404
405 #[cfg(feature = "logging")]
406 tracing::info!("PrePair request envelope produced");
407
408 Ok(ProducePrePairResult { envelope })
409}
410
411/// Decrypts and decodes an incoming [`derec_proto::PairRequestMessage`] from an outer
412/// [`derec_proto::DeRecMessage`] envelope.
413///
414/// Because pairing happens *before* a shared symmetric key exists, the inner message is
415/// decrypted using the pairing-specific **asymmetric** ECIES decryption mechanism.
416///
417/// This function:
418///
419/// 1. Decodes the outer [`derec_proto::DeRecMessage`] envelope from `envelope_bytes`
420/// 2. Decrypts the inner message bytes using `ecies_secret_key`
421/// 3. Decodes the decrypted bytes as a [`derec_proto::PairRequestMessage`]
422/// 4. Validates the invariant `envelope.timestamp == request.timestamp`
423///
424/// # Arguments
425///
426/// * `envelope_bytes` - Serialized outer [`derec_proto::DeRecMessage`] bytes carrying an
427/// asymmetrically-encrypted inner [`derec_proto::PairRequestMessage`], as produced by
428/// [`produce`].
429/// * `ecies_secret_key` - The initiator's ECIES secret key. Must correspond to the
430/// `ecies_public_key` the initiator published in their [`derec_proto::ContactMessage`],
431/// which is the key used by [`produce`] to encrypt the inner request.
432///
433/// # Returns
434///
435/// On success returns [`ExtractResult`] containing:
436///
437/// - `request`: the decrypted inner [`derec_proto::PairRequestMessage`]
438///
439/// # Errors
440///
441/// Returns [`crate::Error`] if:
442///
443/// - `envelope_bytes` cannot be decoded as a valid [`derec_proto::DeRecMessage`]
444/// - ECIES decryption fails
445/// - the decrypted bytes cannot be decoded as a [`derec_proto::PairRequestMessage`]
446/// - `envelope.timestamp != request.timestamp`
447/// - the inner message is not a [`derec_proto::PairRequestMessage`]
448///
449/// # Security: no freshness or replay protection
450///
451/// The timestamp check enforced here only binds the envelope to the
452/// inner body (`envelope.timestamp == body.timestamp`). It does NOT
453/// enforce a freshness window against the receiver's clock and does
454/// NOT detect replays of a previously-captured ciphertext. Pairing
455/// has a small extra mitigation (the per-channel `ContactMessage`
456/// nonce is one-shot — once consumed by the initiator the same
457/// `PairRequest` can no longer drive a fresh pairing forward), but
458/// a recorded envelope can still be re-decoded and inspected at
459/// any later time. Callers MUST add a freshness window and per-
460/// channel anti-replay (monotonic counter or nonce log) on top
461/// before driving any side-effecting state off the parsed body.
462///
463/// # Example
464///
465/// ```
466/// use derec_library::primitives::pairing::request;
467/// use derec_library::types::ChannelId;
468/// use derec_proto::{ContactMode, Protocol, SenderKind, TransportProtocol};
469///
470/// // Initiator: create the out-of-band contact message.
471/// let request::CreateContactResult {
472/// contact_message,
473/// secret_key: initiator_key,
474/// } = request::create_contact(
475/// ChannelId(42),
476/// ContactMode::InlineKeys,
477/// TransportProtocol {
478/// uri: "https://relay.example/initiator".to_owned(),
479/// protocol: Protocol::Https.into(),
480/// },
481/// None,
482/// ).expect("create_contact failed");
483///
484/// // Responder: build the pairing request envelope.
485/// let request::ProduceResult { envelope, .. } = request::produce(
486/// SenderKind::Helper,
487/// TransportProtocol {
488/// uri: "https://relay.example/responder".to_owned(),
489/// protocol: Protocol::Https.into(),
490/// },
491/// &contact_message,
492/// None,
493/// None,
494/// ).expect("produce failed");
495///
496/// // Initiator: decrypt the pairing request with the ECIES secret key.
497/// let request::ExtractResult { request: pair_request } =
498/// request::extract(&envelope, initiator_key.as_ref().unwrap().ecies_secret_key())
499/// .expect("extract failed");
500///
501/// assert_eq!(pair_request.nonce, contact_message.nonce);
502/// ```
503#[cfg_attr(
504 feature = "logging",
505 tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
506)]
507pub fn extract(
508 envelope_bytes: &[u8],
509 ecies_secret_key: &[u8],
510) -> Result<ExtractResult, crate::Error> {
511 let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;
512
513 let plaintext =
514 derec_cryptography::pairing::envelope::decrypt(&envelope.message, ecies_secret_key)
515 .map_err(PairingError::PairingEncryption)?;
516
517 let request = match MessageBody::decode_from_vec(plaintext.as_slice())
518 .map_err(crate::Error::ProtobufDecode)?
519 {
520 MessageBody::PairRequest(r) => r,
521 _ => {
522 #[cfg(feature = "logging")]
523 tracing::warn!("unexpected message type; expected PairRequestMessage");
524
525 return Err(crate::Error::Invariant(
526 "Invalid message. Expected: PairRequestMessage",
527 ));
528 }
529 };
530
531 verify_timestamps(envelope.timestamp, request.timestamp)?;
532
533 if let Some(tp) = request.transport_protocol.as_ref() {
534 tp.validate()?;
535 }
536
537 #[cfg(feature = "logging")]
538 tracing::info!("pairing request extracted and validated");
539
540 Ok(ExtractResult { request })
541}
542
543/// Decodes a plaintext [`PrePairRequestMessage`] from an outer
544/// [`DeRecMessage`] envelope produced by [`produce_pre_pair_request`].
545///
546/// The `PrePair` flow exchanges its messages **in plaintext** inside the
547/// envelope (no shared key exists yet, and the keys the message is asking
548/// for cannot themselves be used for encryption). This function performs
549/// no decryption — it decodes the envelope, decodes the inner
550/// [`MessageBody`], and validates the envelope-vs-body timestamp invariant.
551///
552/// # Arguments
553///
554/// * `envelope_bytes` - Serialized outer [`DeRecMessage`] wire bytes, as
555/// produced by [`produce_pre_pair_request`].
556///
557/// # Returns
558///
559/// On success returns [`PrePairExtractResult`] containing the decoded inner
560/// [`PrePairRequestMessage`]. The caller can recover the routing
561/// `channel_id` by decoding the envelope separately if it is not already
562/// known from context.
563///
564/// # Errors
565///
566/// Returns [`crate::Error`] if:
567///
568/// - `envelope_bytes` cannot be decoded as a valid [`DeRecMessage`]
569/// - the inner [`MessageBody`] cannot be decoded
570/// - the inner [`MessageBody`] is not a [`PrePairRequestMessage`]
571/// - `envelope.timestamp != request.timestamp`
572///
573/// # Security: no freshness or replay protection
574///
575/// The timestamp check enforced here only binds the envelope to the
576/// inner body (`envelope.timestamp == body.timestamp`). It does NOT
577/// enforce a freshness window against the receiver's clock and does
578/// NOT detect replays of a previously-captured envelope. PrePair
579/// envelopes are plaintext (no shared key yet), so a recorded
580/// envelope can be replayed verbatim by anyone on path. Callers
581/// MUST add a freshness window and per-channel anti-replay
582/// (monotonic counter or nonce log) on top before driving any
583/// side-effecting state off the parsed body.
584#[cfg_attr(
585 feature = "logging",
586 tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
587)]
588pub fn extract_pre_pair(envelope_bytes: &[u8]) -> Result<PrePairExtractResult, crate::Error> {
589 let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;
590
591 let request = match crate::derec_message::extract_inner_plaintext_message(&envelope.message)? {
592 MessageBody::PrePairRequest(r) => r,
593 _ => {
594 #[cfg(feature = "logging")]
595 tracing::warn!("unexpected message type; expected PrePairRequestMessage");
596
597 return Err(crate::Error::Invariant(
598 "Invalid message. Expected: PrePairRequestMessage",
599 ));
600 }
601 };
602
603 verify_timestamps(envelope.timestamp, request.timestamp)?;
604
605 if let Some(tp) = request.transport_protocol.as_ref() {
606 tp.validate()?;
607 }
608
609 #[cfg(feature = "logging")]
610 tracing::info!("PrePair request envelope decoded and validated");
611
612 Ok(PrePairExtractResult { request })
613}
614
615fn validate_inputs(
616 transport_protocol: &TransportProtocol,
617 contact_message: &ContactMessage,
618 expected_mode: ContactMode,
619) -> Result<(), crate::Error> {
620 if transport_protocol.uri.trim().is_empty() {
621 #[cfg(feature = "logging")]
622 tracing::warn!("transport URI is empty");
623
624 return Err(PairingError::EmptyTransportUri.into());
625 }
626 transport_protocol.validate()?;
627
628 super::validate_contact_for_mode(contact_message, expected_mode)?;
629
630 let initiator_tp =
631 contact_message
632 .transport_protocol
633 .as_ref()
634 .ok_or(PairingError::InvalidContactMessage(
635 "transport_protocol is missing",
636 ))?;
637
638 if initiator_tp.uri.trim().is_empty() {
639 #[cfg(feature = "logging")]
640 tracing::warn!("contact message transport_protocol.uri is empty");
641
642 return Err(PairingError::InvalidContactMessage("transport_protocol.uri is empty").into());
643 }
644 initiator_tp.validate()?;
645
646 Ok(())
647}
648
649fn validate_pre_pair_inputs(
650 transport_protocol: &TransportProtocol,
651 contact_message: &ContactMessage,
652) -> Result<(), crate::Error> {
653 let expected_mode = if contact_message.contact_mode == ContactMode::HashedKeys as i32 {
654 ContactMode::HashedKeys
655 } else if contact_message.contact_mode == ContactMode::NoKeys as i32 {
656 ContactMode::NoKeys
657 } else {
658 return Err(PairingError::InvalidContactMessage(
659 "contact_mode must be HashedKeys or NoKeys for PrePairRequest",
660 )
661 .into());
662 };
663 validate_inputs(transport_protocol, contact_message, expected_mode)
664}
665
666fn create_pairing_request_material(
667 contact_message: &ContactMessage,
668) -> Result<
669 (
670 cryptography_pairing::PairingRequestMessageMaterial,
671 cryptography_pairing::ResponderSecretKeyMaterial,
672 ),
673 crate::Error,
674> {
675 let mlkem_encapsulation_key = contact_message
676 .mlkem_encapsulation_key
677 .as_ref()
678 .expect("validate_inputs guarantees mlkem_encapsulation_key is Some")
679 .clone();
680 let ecies_public_key = contact_message
681 .ecies_public_key
682 .as_ref()
683 .expect("validate_inputs guarantees ecies_public_key is Some")
684 .clone();
685
686 let contact_pk = PairingContactMessageMaterial {
687 mlkem_encapsulation_key,
688 ecies_public_key,
689 };
690 let seed = generate_seed::<32>();
691 cryptography_pairing::pairing_request_message(*seed, &contact_pk)
692 .map_err(|e| PairingError::PairRequestKeygen { source: e }.into())
693}
694
695fn generate_pairing_keys() -> Result<
696 (
697 PairingContactMessageMaterial,
698 derec_cryptography::pairing::InitiatorSecretKeyMaterial,
699 ),
700 crate::Error,
701> {
702 let seed = generate_seed::<32>();
703 cryptography_pairing::contact_message(*seed)
704 .map_err(|e| PairingError::ContactMessageKeygen { source: e }.into())
705}