Skip to main content

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}