derec-library 0.0.1-alpha.8

Rust SDK for the DeRec protocol, including native and WebAssembly bindings.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 DeRec Alliance. All rights reserved.

use crate::primitives::pairing::PairingError;
use crate::transport::TransportProtocolExt as _;
use crate::utils::{ContactMessageExt as _, verify_timestamps};
use crate::{
    derec_message::{DeRecMessageBuilder, current_timestamp},
    protocol_version::ProtocolVersion,
    types::ChannelId,
    utils::generate_seed,
};
use derec_cryptography::pairing::{
    self as cryptography_pairing, PairingContactMessageMaterial, PairingSecretKeyMaterial,
};
use derec_proto::{
    CommunicationInfo, ContactMessage, ContactMode, DeRecMessage, MessageBody, PairRequestMessage,
    PrePairRequestMessage, SenderKind, TransportProtocol,
};
use prost::Message;
use rand::{Rng, rng};

pub struct CreateContactResult {
    pub contact_message: ContactMessage,
    /// Fresh pairing secret material tied to the keys the contact
    /// creator has committed to.
    ///
    /// - [`ContactMode::InlineKeys`] / [`ContactMode::HashedKeys`]:
    ///   `Some(...)`. Callers MUST persist it — it is required later
    ///   to finalize pairing (decrypt the incoming `PairRequest` and
    ///   derive the shared key).
    /// - [`ContactMode::NoKeys`]: `None`. No key material exists at
    ///   contact-creation time; the contact creator generates it on
    ///   the fly when the corresponding `PrePairRequest` arrives.
    pub secret_key: Option<PairingSecretKeyMaterial>,
}

pub struct ProduceResult {
    /// Serialized outer [`derec_proto::DeRecMessage`] wire bytes carrying an encrypted inner
    /// [`derec_proto::PairRequestMessage`]. Ready to send over transport.
    pub envelope: Vec<u8>,
    pub initiator_contact_message: ContactMessage,
    pub secret_key: PairingSecretKeyMaterial,
}

pub struct ExtractResult {
    pub request: PairRequestMessage,
}

pub struct ProducePrePairResult {
    /// Serialized outer [`derec_proto::DeRecMessage`] wire bytes carrying a **plaintext**
    /// inner [`derec_proto::PrePairRequestMessage`]. Ready to send over transport.
    pub envelope: Vec<u8>,
}

pub struct PrePairExtractResult {
    pub request: PrePairRequestMessage,
}

/// Creates a [`derec_proto::ContactMessage`] used to bootstrap the DeRec *pairing* flow.
///
/// In DeRec, pairing begins with an **out-of-band contact transfer** (typically QR or
/// another side channel). Unlike normal DeRec protocol traffic, the contact message is
/// **not wrapped in a `DeRecMessage` envelope** and is **not encrypted**. It is sent as
/// plain protobuf bytes (serialize the returned `contact_message` with `.encode_to_vec()`).
///
/// Single entry point for all three `contact_mode` variants. Mode-specific
/// assembly happens in private helpers (`create_contact_inlined_keys`,
/// `create_contact_hashed_keys`, `create_contact_no_keys`) invoked here.
///
/// # Arguments
///
/// * `channel_id` — Identifier embedded in the contact and copied into subsequent
///   pairing messages by the recipient.
/// * `contact_mode` — Selects how the initiator's public pairing material is delivered:
///   - [`ContactMode::InlineKeys`]: keys are embedded directly in the contact.
///   - [`ContactMode::HashedKeys`]: only a SHA-384 binding hash is embedded; the peer
///     obtains the keys via a `PrePair` round-trip and verifies against the hash. The
///     transport endpoint advertised here MUST be ephemeral — the plaintext `PrePair*`
///     traffic must not be linkable to a long-lived endpoint.
///   - [`ContactMode::NoKeys`]: no key material and no commitment. Keys are generated
///     on the fly by the creator when the corresponding `PrePairRequest` arrives.
///     Trust rests entirely on the OOB delivery channel being fully trusted.
/// * `transport_protocol` — Endpoint the recipient uses to reach this initiator with
///   the next protocol message. The `uri` field must not be empty.
/// * `nonce` — Correlation nonce embedded in the contact.
///   - `None`: the library generates a fresh cryptographically-random `u64`. Suitable
///     default for `InlineKeys` / `HashedKeys` where the nonce is a security parameter.
///   - `Some(n)`: application-controlled value. Required for `NoKeys` where callers
///     typically pick a small human-typable value (4–6 decimal digits) for manual entry.
///     Also valid for `InlineKeys` / `HashedKeys` if the app wants deterministic control.
///
/// # Returns
///
/// [`CreateContactResult`] with:
///
/// - `contact_message`: decoded [`ContactMessage`] — serialize with `.encode_to_vec()`
///   before sending out-of-band.
/// - `secret_key`: `Some(...)` for `InlineKeys` and `HashedKeys` (must be persisted);
///   `None` for `NoKeys` (no key material at contact-creation time).
///
/// # Errors
///
/// - [`PairingError::EmptyTransportUri`] if `transport_protocol.uri` is empty.
/// - [`PairingError::ContactMessageKeygen`] if pairing key generation fails
///   (`InlineKeys` / `HashedKeys` only — `NoKeys` skips keygen).
///
/// # Example
///
/// ```
/// use derec_library::primitives::pairing::request;
/// use derec_library::types::ChannelId;
/// use derec_proto::{ContactMode, Protocol, TransportProtocol};
///
/// let request::CreateContactResult {
///     contact_message,
///     secret_key,
/// } = request::create_contact(
///     ChannelId(42),
///     ContactMode::InlineKeys,
///     TransportProtocol {
///         uri: "https://relay.example/derec".to_owned(),
///         protocol: Protocol::Https.into(),
///     },
///     None,
/// ).expect("Failed to create contact message");
///
/// assert!(secret_key.is_some());
/// let _ = contact_message;
/// ```
#[cfg_attr(
    feature = "logging",
    tracing::instrument(skip_all, fields(channel_id = channel_id.0, contact_mode = contact_mode as i32))
)]
pub fn create_contact(
    channel_id: ChannelId,
    contact_mode: ContactMode,
    transport_protocol: TransportProtocol,
    nonce: Option<u64>,
) -> Result<CreateContactResult, crate::Error> {
    if transport_protocol.uri.trim().is_empty() {
        #[cfg(feature = "logging")]
        tracing::warn!("transport URI is empty");

        return Err(PairingError::EmptyTransportUri.into());
    }

    let nonce = nonce.unwrap_or_else(|| rng().next_u64());

    let (contact_message, secret_key) = match contact_mode {
        ContactMode::InlineKeys => {
            let (pk, sk) = generate_pairing_keys()?;
            let msg = ContactMessage::inline_keys(channel_id, nonce, transport_protocol, pk);
            (msg, Some(PairingSecretKeyMaterial::Initiator(sk)))
        }
        ContactMode::HashedKeys => {
            let (pk, sk) = generate_pairing_keys()?;
            let msg = ContactMessage::hashed_keys(channel_id, nonce, transport_protocol, &pk);
            (msg, Some(PairingSecretKeyMaterial::Initiator(sk)))
        }
        ContactMode::NoKeys => {
            let msg = ContactMessage::no_keys(channel_id, nonce, transport_protocol);
            (msg, None)
        }
    };

    #[cfg(feature = "logging")]
    tracing::info!("contact message created");

    Ok(CreateContactResult {
        contact_message,
        secret_key,
    })
}

/// Produces a pairing request [`derec_proto::DeRecMessage`] envelope, continuing the DeRec
/// pairing flow.
///
/// This function is executed by the **Responder** (the party that scanned or otherwise
/// received the initiator's contact out-of-band).
///
/// Under the current protocol model:
///
/// 1. The initiator sends a [`derec_proto::ContactMessage`] out-of-band
/// 2. The responder decodes that contact, performs the responder-side pairing-request
///    cryptographic step, and constructs a [`derec_proto::PairRequestMessage`]
/// 3. The inner [`derec_proto::PairRequestMessage`] is protobuf-serialized and then encrypted
///    using the initiator's public ECIES key
/// 4. The encrypted bytes are placed into a plain [`derec_proto::DeRecMessage`] envelope
/// 5. The final result is serialized envelope bytes ready to be sent over the transport
///
/// Because pairing happens *before* a shared symmetric key exists, this function uses the
/// pairing-specific **asymmetric** encryption mechanism for the inner message.
///
/// The returned [`derec_cryptography::pairing::PairingSecretKeyMaterial`] must be retained
/// locally and later used to finalize pairing when the response arrives.
///
/// # Arguments
///
/// * `kind` - Role of the sender within the DeRec protocol (for example
///   `Owner`, `Helper`, or `Replica`)
/// * `transport_protocol` - Transport endpoint the initiator can use to reach this responder
///   for subsequent protocol traffic. The `uri` field must not be empty or whitespace-only.
/// * `contact_message` - The decoded [`derec_proto::ContactMessage`] received from the
///   initiator, as returned by [`create_contact`] and decoded by the caller.
/// * `communication_info` - Optional application-level identity metadata to advertise to the
///   peer (free-form key/value pairs). Pass `None` to send no metadata; the protocol treats
///   this as opaque.
///
/// # Returns
///
/// On success returns [`ProduceResult`] containing:
///
/// - `envelope`: serialized outer [`derec_proto::DeRecMessage`] envelope bytes
/// - `initiator_contact_message`: the decoded initiator [`derec_proto::ContactMessage`],
///   providing transport endpoint, public keys, channel identifier, and nonce
/// - `secret_key`: responder-side pairing secret state required later to derive the final
///   shared pairing key
///
/// # Errors
///
/// Returns [`crate::Error`] (specifically `Error::Pairing(...)`) in the following cases:
///
/// - [`PairingError::EmptyTransportUri`] if `transport_protocol.uri` is empty or whitespace
/// - [`PairingError::InvalidContactMessage`] if required contact fields are missing or the
///   contact transport protocol is absent or has an empty URI
/// - [`PairingError::PairRequestKeygen`] if ML-KEM encapsulation or key generation fails
/// - [`PairingError::PairingEncryption`] if inner-message encryption fails
///
/// # Security Notes
///
/// - The `contact_message` is peer-provided data; validate all required fields before use.
/// - The returned secret key material must be securely retained by the responder.
///
/// # Example
///
/// ```
/// use derec_library::primitives::pairing::request;
/// use derec_library::types::ChannelId;
/// use derec_proto::{ContactMode, Protocol, SenderKind, TransportProtocol};
///
/// // Initiator side: create a contact message out-of-band.
/// let request::CreateContactResult { contact_message, .. } = request::create_contact(
///     ChannelId(42),
///     ContactMode::InlineKeys,
///     TransportProtocol {
///         uri: "https://relay.example/initiator".to_owned(),
///         protocol: Protocol::Https.into(),
///     },
///     None,
/// ).expect("create_contact failed");
///
/// // Responder side: build the pairing request envelope from the received contact.
/// let request::ProduceResult { envelope, .. } = request::produce(
///     SenderKind::Helper,
///     TransportProtocol {
///         uri: "https://relay.example/responder".to_owned(),
///         protocol: Protocol::Https.into(),
///     },
///     &contact_message,
///     None,
///     None,
/// ).expect("produce failed");
///
/// assert!(!envelope.is_empty());
/// ```
#[cfg_attr(
    feature = "logging",
    tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id, kind = kind as i32))
)]
pub fn produce(
    kind: SenderKind,
    transport_protocol: TransportProtocol,
    contact_message: &ContactMessage,
    communication_info: Option<CommunicationInfo>,
    parameter_range: Option<derec_proto::ParameterRange>,
) -> Result<ProduceResult, crate::Error> {
    validate_inputs(
        &transport_protocol,
        contact_message,
        ContactMode::InlineKeys,
    )?;

    let (pairing_request_key_material, secret_key) =
        create_pairing_request_material(contact_message)?;

    let timestamp = current_timestamp();

    let request = PairRequestMessage {
        sender_kind: kind.into(),
        mlkem_ciphertext: pairing_request_key_material.mlkem_ciphertext,
        ecies_public_key: pairing_request_key_material.ecies_public_key.clone(),
        nonce: contact_message.nonce,
        communication_info,
        parameter_range,
        transport_protocol: Some(transport_protocol),
        timestamp: Some(timestamp),
    };

    // Encrypt with the INITIATOR's ECIES public key (from the contact) —
    // only the initiator's matching secret key can decrypt. The
    // responder's own freshly-generated pubkey travels in
    // `request.ecies_public_key` (above) for the initiator to ECDH against
    // when finishing the pairing; it is NOT the encryption key here.
    let envelope = DeRecMessageBuilder::pairing()
        .channel_id(contact_message.channel_id.into())
        .timestamp(timestamp)
        .message_body(MessageBody::PairRequest(request))
        .encrypt_pairing(
            contact_message
                .ecies_public_key
                .as_ref()
                .expect("validate_inputs guarantees ecies_public_key is Some"),
        )?
        .build()?
        .encode_to_vec();

    #[cfg(feature = "logging")]
    tracing::info!("pairing request envelope produced");

    Ok(ProduceResult {
        envelope,
        initiator_contact_message: contact_message.clone(),
        secret_key: PairingSecretKeyMaterial::Responder(secret_key),
    })
}

/// Produces a `PrePairRequestMessage` envelope, the first step of the
/// [`ContactMode::HashedKeys`] pairing flow.
///
/// When a [`ContactMessage`] arrives with `contactMode == HASHED_KEYS`, it carries
/// only a SHA-384 commitment to the initiator's public keys (so the contact stays
/// small enough for a QR code) — the actual ML-KEM and ECIES keys must be fetched
/// over the wire via `PrePair` before a [`PairRequestMessage`] can be built. This
/// function builds that fetch envelope on the responder (scanner) side.
///
/// The inner [`PrePairRequestMessage`] is **plaintext** — no shared key exists yet
/// and the keys it asks for cannot themselves be used for encryption — so the outer
/// [`DeRecMessage`] envelope is constructed directly rather than via the
/// encryption-enforcing [`DeRecMessageBuilder`]. The envelope's `channelId` is
/// taken from the [`ContactMessage`] so the contact creator can correlate the
/// request with the right local pairing state.
///
/// # Arguments
///
/// * `transport_protocol` - Transport endpoint the contact creator should use to
///   send the [`derec_proto::PrePairResponseMessage`] back. The `uri` field must
///   not be empty or whitespace-only. Because [`PrePair`-messages][PrePairRequestMessage]
///   travel as plaintext, this endpoint MUST be ephemeral (see the security note
///   on `PrePairRequestMessage`).
/// * `contact_message` - The decoded [`ContactMessage`] received out-of-band. Its
///   `contact_mode` must be [`ContactMode::HashedKeys`]; for
///   [`ContactMode::InlineKeys`] the responder already has the keys and should
///   call [`produce`] directly instead.
///
/// # Returns
///
/// On success returns [`ProducePrePairResult`] containing the serialized outer
/// [`DeRecMessage`] envelope bytes.
///
/// # Errors
///
/// Returns [`crate::Error`] (specifically `Error::Pairing(...)`) in the following cases:
///
/// - [`PairingError::EmptyTransportUri`] if `transport_protocol.uri` is empty or whitespace
/// - [`PairingError::InvalidContactMessage`] if `contact_message.contact_mode` is neither
///   [`ContactMode::HashedKeys`] nor [`ContactMode::NoKeys`] — including
///   [`ContactMode::InlineKeys`] (which carries the keys inline and has no
///   PrePair step) and any unknown enum value
///
/// # Security Notes
///
/// - The envelope is plaintext; do not include any sensitive material beyond
///   what `PrePairRequestMessage` already exposes.
/// - The transport endpoint advertised here is visible to passive observers.
#[cfg_attr(
    feature = "logging",
    tracing::instrument(skip_all, fields(channel_id = contact_message.channel_id))
)]
pub fn produce_pre_pair_request(
    transport_protocol: TransportProtocol,
    contact_message: &ContactMessage,
) -> Result<ProducePrePairResult, crate::Error> {
    validate_pre_pair_inputs(&transport_protocol, contact_message)?;

    let timestamp = current_timestamp();
    let request = PrePairRequestMessage {
        nonce: contact_message.nonce,
        transport_protocol: Some(transport_protocol),
        timestamp: Some(timestamp),
    };

    let protocol_version = ProtocolVersion::current();
    let envelope = DeRecMessage {
        protocol_version_major: protocol_version.major,
        protocol_version_minor: protocol_version.minor,
        sequence: 0,
        channel_id: contact_message.channel_id,
        timestamp: Some(timestamp),
        message: MessageBody::PrePairRequest(request).encode_to_vec(),
        trace_id: 0,
    }
    .encode_to_vec();

    #[cfg(feature = "logging")]
    tracing::info!("PrePair request envelope produced");

    Ok(ProducePrePairResult { envelope })
}

/// Decrypts and decodes an incoming [`derec_proto::PairRequestMessage`] from an outer
/// [`derec_proto::DeRecMessage`] envelope.
///
/// Because pairing happens *before* a shared symmetric key exists, the inner message is
/// decrypted using the pairing-specific **asymmetric** ECIES decryption mechanism.
///
/// This function:
///
/// 1. Decodes the outer [`derec_proto::DeRecMessage`] envelope from `envelope_bytes`
/// 2. Decrypts the inner message bytes using `ecies_secret_key`
/// 3. Decodes the decrypted bytes as a [`derec_proto::PairRequestMessage`]
/// 4. Validates the invariant `envelope.timestamp == request.timestamp`
///
/// # Arguments
///
/// * `envelope_bytes` - Serialized outer [`derec_proto::DeRecMessage`] bytes carrying an
///   asymmetrically-encrypted inner [`derec_proto::PairRequestMessage`], as produced by
///   [`produce`].
/// * `ecies_secret_key` - The initiator's ECIES secret key. Must correspond to the
///   `ecies_public_key` the initiator published in their [`derec_proto::ContactMessage`],
///   which is the key used by [`produce`] to encrypt the inner request.
///
/// # Returns
///
/// On success returns [`ExtractResult`] containing:
///
/// - `request`: the decrypted inner [`derec_proto::PairRequestMessage`]
///
/// # Errors
///
/// Returns [`crate::Error`] if:
///
/// - `envelope_bytes` cannot be decoded as a valid [`derec_proto::DeRecMessage`]
/// - ECIES decryption fails
/// - the decrypted bytes cannot be decoded as a [`derec_proto::PairRequestMessage`]
/// - `envelope.timestamp != request.timestamp`
/// - the inner message is not a [`derec_proto::PairRequestMessage`]
///
/// # Security: no freshness or replay protection
///
/// The timestamp check enforced here only binds the envelope to the
/// inner body (`envelope.timestamp == body.timestamp`). It does NOT
/// enforce a freshness window against the receiver's clock and does
/// NOT detect replays of a previously-captured ciphertext. Pairing
/// has a small extra mitigation (the per-channel `ContactMessage`
/// nonce is one-shot — once consumed by the initiator the same
/// `PairRequest` can no longer drive a fresh pairing forward), but
/// a recorded envelope can still be re-decoded and inspected at
/// any later time. Callers MUST add a freshness window and per-
/// channel anti-replay (monotonic counter or nonce log) on top
/// before driving any side-effecting state off the parsed body.
///
/// # Example
///
/// ```
/// use derec_library::primitives::pairing::request;
/// use derec_library::types::ChannelId;
/// use derec_proto::{ContactMode, Protocol, SenderKind, TransportProtocol};
///
/// // Initiator: create the out-of-band contact message.
/// let request::CreateContactResult {
///     contact_message,
///     secret_key: initiator_key,
/// } = request::create_contact(
///     ChannelId(42),
///     ContactMode::InlineKeys,
///     TransportProtocol {
///         uri: "https://relay.example/initiator".to_owned(),
///         protocol: Protocol::Https.into(),
///     },
///     None,
/// ).expect("create_contact failed");
///
/// // Responder: build the pairing request envelope.
/// let request::ProduceResult { envelope, .. } = request::produce(
///     SenderKind::Helper,
///     TransportProtocol {
///         uri: "https://relay.example/responder".to_owned(),
///         protocol: Protocol::Https.into(),
///     },
///     &contact_message,
///     None,
///     None,
/// ).expect("produce failed");
///
/// // Initiator: decrypt the pairing request with the ECIES secret key.
/// let request::ExtractResult { request: pair_request } =
///     request::extract(&envelope, initiator_key.as_ref().unwrap().ecies_secret_key())
///         .expect("extract failed");
///
/// assert_eq!(pair_request.nonce, contact_message.nonce);
/// ```
#[cfg_attr(
    feature = "logging",
    tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
)]
pub fn extract(
    envelope_bytes: &[u8],
    ecies_secret_key: &[u8],
) -> Result<ExtractResult, crate::Error> {
    let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;

    let plaintext =
        derec_cryptography::pairing::envelope::decrypt(&envelope.message, ecies_secret_key)
            .map_err(PairingError::PairingEncryption)?;

    let request = match MessageBody::decode_from_vec(plaintext.as_slice())
        .map_err(crate::Error::ProtobufDecode)?
    {
        MessageBody::PairRequest(r) => r,
        _ => {
            #[cfg(feature = "logging")]
            tracing::warn!("unexpected message type; expected PairRequestMessage");

            return Err(crate::Error::Invariant(
                "Invalid message. Expected: PairRequestMessage",
            ));
        }
    };

    verify_timestamps(envelope.timestamp, request.timestamp)?;

    if let Some(tp) = request.transport_protocol.as_ref() {
        tp.validate()?;
    }

    #[cfg(feature = "logging")]
    tracing::info!("pairing request extracted and validated");

    Ok(ExtractResult { request })
}

/// Decodes a plaintext [`PrePairRequestMessage`] from an outer
/// [`DeRecMessage`] envelope produced by [`produce_pre_pair_request`].
///
/// The `PrePair` flow exchanges its messages **in plaintext** inside the
/// envelope (no shared key exists yet, and the keys the message is asking
/// for cannot themselves be used for encryption). This function performs
/// no decryption — it decodes the envelope, decodes the inner
/// [`MessageBody`], and validates the envelope-vs-body timestamp invariant.
///
/// # Arguments
///
/// * `envelope_bytes` - Serialized outer [`DeRecMessage`] wire bytes, as
///   produced by [`produce_pre_pair_request`].
///
/// # Returns
///
/// On success returns [`PrePairExtractResult`] containing the decoded inner
/// [`PrePairRequestMessage`]. The caller can recover the routing
/// `channel_id` by decoding the envelope separately if it is not already
/// known from context.
///
/// # Errors
///
/// Returns [`crate::Error`] if:
///
/// - `envelope_bytes` cannot be decoded as a valid [`DeRecMessage`]
/// - the inner [`MessageBody`] cannot be decoded
/// - the inner [`MessageBody`] is not a [`PrePairRequestMessage`]
/// - `envelope.timestamp != request.timestamp`
///
/// # Security: no freshness or replay protection
///
/// The timestamp check enforced here only binds the envelope to the
/// inner body (`envelope.timestamp == body.timestamp`). It does NOT
/// enforce a freshness window against the receiver's clock and does
/// NOT detect replays of a previously-captured envelope. PrePair
/// envelopes are plaintext (no shared key yet), so a recorded
/// envelope can be replayed verbatim by anyone on path. Callers
/// MUST add a freshness window and per-channel anti-replay
/// (monotonic counter or nonce log) on top before driving any
/// side-effecting state off the parsed body.
#[cfg_attr(
    feature = "logging",
    tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
)]
pub fn extract_pre_pair(envelope_bytes: &[u8]) -> Result<PrePairExtractResult, crate::Error> {
    let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;

    let request = match crate::derec_message::extract_inner_plaintext_message(&envelope.message)? {
        MessageBody::PrePairRequest(r) => r,
        _ => {
            #[cfg(feature = "logging")]
            tracing::warn!("unexpected message type; expected PrePairRequestMessage");

            return Err(crate::Error::Invariant(
                "Invalid message. Expected: PrePairRequestMessage",
            ));
        }
    };

    verify_timestamps(envelope.timestamp, request.timestamp)?;

    if let Some(tp) = request.transport_protocol.as_ref() {
        tp.validate()?;
    }

    #[cfg(feature = "logging")]
    tracing::info!("PrePair request envelope decoded and validated");

    Ok(PrePairExtractResult { request })
}

fn validate_inputs(
    transport_protocol: &TransportProtocol,
    contact_message: &ContactMessage,
    expected_mode: ContactMode,
) -> Result<(), crate::Error> {
    if transport_protocol.uri.trim().is_empty() {
        #[cfg(feature = "logging")]
        tracing::warn!("transport URI is empty");

        return Err(PairingError::EmptyTransportUri.into());
    }
    transport_protocol.validate()?;

    super::validate_contact_for_mode(contact_message, expected_mode)?;

    let initiator_tp =
        contact_message
            .transport_protocol
            .as_ref()
            .ok_or(PairingError::InvalidContactMessage(
                "transport_protocol is missing",
            ))?;

    if initiator_tp.uri.trim().is_empty() {
        #[cfg(feature = "logging")]
        tracing::warn!("contact message transport_protocol.uri is empty");

        return Err(PairingError::InvalidContactMessage("transport_protocol.uri is empty").into());
    }
    initiator_tp.validate()?;

    Ok(())
}

fn validate_pre_pair_inputs(
    transport_protocol: &TransportProtocol,
    contact_message: &ContactMessage,
) -> Result<(), crate::Error> {
    let expected_mode = if contact_message.contact_mode == ContactMode::HashedKeys as i32 {
        ContactMode::HashedKeys
    } else if contact_message.contact_mode == ContactMode::NoKeys as i32 {
        ContactMode::NoKeys
    } else {
        return Err(PairingError::InvalidContactMessage(
            "contact_mode must be HashedKeys or NoKeys for PrePairRequest",
        )
        .into());
    };
    validate_inputs(transport_protocol, contact_message, expected_mode)
}

fn create_pairing_request_material(
    contact_message: &ContactMessage,
) -> Result<
    (
        cryptography_pairing::PairingRequestMessageMaterial,
        cryptography_pairing::ResponderSecretKeyMaterial,
    ),
    crate::Error,
> {
    let mlkem_encapsulation_key = contact_message
        .mlkem_encapsulation_key
        .as_ref()
        .expect("validate_inputs guarantees mlkem_encapsulation_key is Some")
        .clone();
    let ecies_public_key = contact_message
        .ecies_public_key
        .as_ref()
        .expect("validate_inputs guarantees ecies_public_key is Some")
        .clone();

    let contact_pk = PairingContactMessageMaterial {
        mlkem_encapsulation_key,
        ecies_public_key,
    };
    let seed = generate_seed::<32>();
    cryptography_pairing::pairing_request_message(*seed, &contact_pk)
        .map_err(|e| PairingError::PairRequestKeygen { source: e }.into())
}

fn generate_pairing_keys() -> Result<
    (
        PairingContactMessageMaterial,
        derec_cryptography::pairing::InitiatorSecretKeyMaterial,
    ),
    crate::Error,
> {
    let seed = generate_seed::<32>();
    cryptography_pairing::contact_message(*seed)
        .map_err(|e| PairingError::ContactMessageKeygen { source: e }.into())
}