Skip to main content

openrtc/client/
application_crypto_impl.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::{Arc, RwLock as StdRwLock};
3
4use iroh::endpoint::{RecvStream, SendStream};
5
6use crate::application_crypto::{self, APPLICATION_KEY_BYTES};
7use crate::application_crypto_streams::{
8    wrap_peer_streams, ApplicationStreamAccess, ApplicationStreamAdmission, PeerRecvStream,
9    PeerSendStream,
10};
11use crate::client::Client;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub(crate) struct ConnectionApplicationCryptoConfirmation {
15    transport_stable_id: u64,
16    confirmed: bool,
17}
18
19pub(crate) struct ApplicationKeyHandshakeOutcome {
20    #[cfg(not(target_arch = "wasm32"))]
21    pub local_public_key: [u8; crate::key_agreement::PUBLIC_KEY_BYTES],
22    #[cfg(not(target_arch = "wasm32"))]
23    pub key_changed: bool,
24    #[cfg(target_arch = "wasm32")]
25    pub reply_action: Option<&'static str>,
26}
27
28#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
29impl Client {
30    #[cfg(not(target_arch = "wasm32"))]
31    pub(crate) fn notify_connection_application_route_update(&self) {
32        self.connection_application_route_updates.notify_waiters();
33    }
34
35    #[cfg(target_arch = "wasm32")]
36    pub(crate) fn application_key_handshake_frame(
37        &self,
38        connection_id: &str,
39        action: &str,
40    ) -> anyhow::Result<serde_json::Value> {
41        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
42
43        let public_key = self
44            .get_or_create_connection_key_agreement(connection_id)
45            .map_err(|error| {
46                anyhow::anyhow!("application key agreement initialization failed: {error:?}")
47            })?
48            .public_key_bytes();
49        Ok(serde_json::json!({
50            "type": "handshake",
51            "action": action,
52            "capabilities": {
53                "applicationKeyAgreement": true,
54            },
55            "applicationKeyAgreement": {
56                "algorithm": crate::key_agreement::KEY_ALGORITHM,
57                "publicKey": URL_SAFE_NO_PAD.encode(public_key),
58            },
59        }))
60    }
61
62    pub(crate) async fn accept_application_key_handshake(
63        &self,
64        connection_id: &str,
65        remote_node_id: &str,
66        transport_stable_id: Option<u64>,
67        action: Option<&str>,
68        remote_public_key: [u8; crate::key_agreement::PUBLIC_KEY_BYTES],
69    ) -> anyhow::Result<ApplicationKeyHandshakeOutcome> {
70        if matches!(action, Some("response" | "ack")) && transport_stable_id.is_none() {
71            anyhow::bail!(
72                "application key confirmation is missing the Rust-owned transport stable id: action={}",
73                action.unwrap_or("unknown"),
74            );
75        }
76        let local_node_id = self
77            .current_node_id()
78            .await
79            .ok_or_else(|| anyhow::anyhow!("local node id is unavailable"))?;
80        let (local_public_key, key, key_changed) = if let Some(transport_stable_id) =
81            transport_stable_id
82        {
83            let remote_endpoint_id = remote_node_id
84                .parse::<iroh::EndpointId>()
85                .map_err(|error| anyhow::anyhow!("invalid remote endpoint id: {error}"))?;
86            let node = self.iroh_node.read().await;
87            let node = node
88                .as_ref()
89                .cloned()
90                .ok_or_else(|| anyhow::anyhow!("iroh node is unavailable"))?;
91            let confirmation_transport =
92                matches!(action, Some("response" | "ack")).then_some(transport_stable_id);
93            let applied = node
94                .with_current_connection_generation(
95                    remote_endpoint_id,
96                    transport_stable_id,
97                    || async {
98                        self.connection_manager
99                            .with_current_transport_generation(
100                                connection_id,
101                                remote_node_id,
102                                transport_stable_id,
103                                |_| {
104                                    self.derive_and_apply_application_key_handshake_after_owner_fence(
105                                        connection_id,
106                                        &local_node_id,
107                                        remote_node_id,
108                                        remote_public_key,
109                                        confirmation_transport,
110                                    )
111                                },
112                            )
113                            .await
114                    },
115                )
116                .await
117                .flatten();
118            let Some(outcome) = applied else {
119                let fence = self
120                    .incoming_stream_generation_fence(remote_endpoint_id, transport_stable_id)
121                    .await;
122                anyhow::bail!(
123                    "application key confirmation rejected for stale transport generation: incoming_stable_id={} physical_stable_id={:?} managed_stable_id={:?} manager_record_exists={}",
124                    fence.incoming_transport_stable_id,
125                    fence.physical_transport_stable_id,
126                    fence.managed_transport_stable_id,
127                    fence.manager_record_exists,
128                );
129            };
130            outcome?
131        } else {
132            // Initial legacy hello frames may precede manager publication and
133            // cannot confirm a route. Their key state is still applied as one
134            // atomic, non-routable security update.
135            self.derive_and_apply_application_key_handshake_after_owner_fence(
136                connection_id,
137                &local_node_id,
138                remote_node_id,
139                remote_public_key,
140                None,
141            )?
142        };
143        eprintln!(
144            "[OpenRTC][KEY-AGREEMENT] connection_id={} action={} key_changed={} confirmed={} key_fingerprint={} local_public_fingerprint={} remote_public_fingerprint={} local_node_id={} remote_node_id={}",
145            connection_id,
146            action.unwrap_or("hello"),
147            key_changed,
148            self.connection_application_crypto_is_confirmed(connection_id, transport_stable_id),
149            crate::key_agreement::key_fingerprint(&key),
150            crate::key_agreement::key_fingerprint(&local_public_key),
151            crate::key_agreement::key_fingerprint(&remote_public_key),
152            local_node_id,
153            remote_node_id,
154        );
155        #[cfg(target_arch = "wasm32")]
156        let reply_action = match action {
157            Some("ack") => None,
158            Some("response") => Some("ack"),
159            _ => Some("response"),
160        };
161        Ok(ApplicationKeyHandshakeOutcome {
162            #[cfg(not(target_arch = "wasm32"))]
163            local_public_key,
164            #[cfg(not(target_arch = "wasm32"))]
165            key_changed,
166            #[cfg(target_arch = "wasm32")]
167            reply_action,
168        })
169    }
170
171    /// Commit the derived key and its exact confirmation as one observable
172    /// application-security state. The caller must already hold both the
173    /// physical generation lock and the matching manager generation lock.
174    fn derive_and_apply_application_key_handshake_after_owner_fence(
175        &self,
176        connection_id: &str,
177        local_node_id: &str,
178        remote_node_id: &str,
179        remote_public_key: [u8; crate::key_agreement::PUBLIC_KEY_BYTES],
180        confirmation_transport_stable_id: Option<u64>,
181    ) -> anyhow::Result<(
182        [u8; crate::key_agreement::PUBLIC_KEY_BYTES],
183        [u8; APPLICATION_KEY_BYTES],
184        bool,
185    )> {
186        let _state = self
187            .connection_application_crypto_state
188            .write()
189            .map_err(|_| anyhow::anyhow!("application crypto state poisoned"))?;
190        let mut agreements = self
191            .connection_application_key_agreements
192            .write()
193            .map_err(|_| anyhow::anyhow!("application key agreement map poisoned"))?;
194        let agreement = match agreements.get(connection_id).cloned() {
195            Some(agreement) => agreement,
196            None => crate::key_agreement::KeyAgreement::generate().map_err(|error| {
197                anyhow::anyhow!("application key agreement initialization failed: {error:?}")
198            })?,
199        };
200        let key = agreement
201            .derive_crypto_key(&remote_public_key, local_node_id, remote_node_id)
202            .map_err(|error| anyhow::anyhow!("application key derivation failed: {error:?}"))?;
203        let local_public_key = agreement.public_key_bytes();
204
205        // Lock every security component before mutating any of them. A
206        // poisoned inner registry therefore fails closed without leaving a
207        // partial key epoch, and rejected generations cannot recreate an
208        // agreement after terminal cleanup.
209        let mut keys = self
210            .connection_application_crypto_keys
211            .write()
212            .map_err(|_| anyhow::anyhow!("application crypto key map poisoned"))?;
213        let mut required = self
214            .connection_application_crypto_required
215            .write()
216            .map_err(|_| anyhow::anyhow!("application crypto requirement map poisoned"))?;
217        let mut sequences = self
218            .connection_application_crypto_outbound_sequences
219            .write()
220            .map_err(|_| anyhow::anyhow!("application crypto sequence map poisoned"))?;
221        let mut confirmed = self
222            .connection_application_crypto_confirmed
223            .write()
224            .map_err(|_| anyhow::anyhow!("application crypto confirmation map poisoned"))?;
225
226        agreements
227            .entry(connection_id.to_string())
228            .or_insert(agreement);
229        let key_changed = keys.get(connection_id) != Some(&key);
230        if key_changed {
231            keys.insert(connection_id.to_string(), key);
232        }
233        required.insert(connection_id.to_string());
234        if key_changed {
235            sequences.insert(connection_id.to_string(), 0);
236        }
237        match confirmation_transport_stable_id {
238            Some(transport_stable_id) => {
239                confirmed.insert(
240                    connection_id.to_string(),
241                    ConnectionApplicationCryptoConfirmation {
242                        transport_stable_id,
243                        confirmed: true,
244                    },
245                );
246            }
247            None if key_changed => {
248                if let Some(state) = confirmed.get_mut(connection_id) {
249                    state.confirmed = false;
250                }
251            }
252            None => {}
253        }
254        drop(confirmed);
255        drop(sequences);
256        drop(required);
257        drop(keys);
258        drop(agreements);
259        drop(_state);
260        #[cfg(not(target_arch = "wasm32"))]
261        self.notify_connection_application_route_update();
262        Ok((local_public_key, key, key_changed))
263    }
264
265    pub(crate) async fn assert_application_crypto_generation(
266        &self,
267        connection_id: &str,
268        expected_endpoint: &str,
269        expected_transport_stable_id: Option<u64>,
270        expected_transport_generation: u64,
271        expected_route_generation: u64,
272    ) -> anyhow::Result<()> {
273        let current = self
274            .connection_manager
275            .get_by_connection_id(connection_id)
276            .await
277            .ok_or_else(|| {
278                anyhow::anyhow!("connection {connection_id} retired during key agreement")
279            })?;
280        let current_endpoint = current
281            .endpoint_id
282            .as_deref()
283            .or(current.node_id.as_deref());
284        anyhow::ensure!(
285            current_endpoint == Some(expected_endpoint)
286                && current.transport_stable_id == expected_transport_stable_id
287                && current.transport_generation == expected_transport_generation
288                && current.route_generation == expected_route_generation,
289            "connection {connection_id} generation changed during key agreement"
290        );
291        Ok(())
292    }
293
294    #[cfg_attr(not(any(target_arch = "wasm32", test)), allow(dead_code))]
295    pub(crate) fn set_connection_application_crypto_key(
296        &self,
297        connection_id: &str,
298        key: [u8; APPLICATION_KEY_BYTES],
299    ) {
300        let Ok(_state) = self.connection_application_crypto_state.write() else {
301            return;
302        };
303        let key_changed = self
304            .connection_application_crypto_keys
305            .write()
306            .map(|mut keys| {
307                if keys.get(connection_id) == Some(&key) {
308                    false
309                } else {
310                    keys.insert(connection_id.to_string(), key);
311                    true
312                }
313            })
314            .unwrap_or(false);
315        if key_changed {
316            if let Ok(mut confirmed) = self.connection_application_crypto_confirmed.write() {
317                if let Some(state) = confirmed.get_mut(connection_id) {
318                    state.confirmed = false;
319                }
320            }
321            #[cfg(not(target_arch = "wasm32"))]
322            self.notify_connection_application_route_update();
323            if let Ok(mut sequences) = self
324                .connection_application_crypto_outbound_sequences
325                .write()
326            {
327                sequences.insert(connection_id.to_string(), 0);
328            }
329        }
330    }
331
332    pub(crate) fn set_connection_application_crypto_required(&self, connection_id: &str) {
333        let Ok(_state) = self.connection_application_crypto_state.write() else {
334            return;
335        };
336        let changed = self
337            .connection_application_crypto_required
338            .write()
339            .map(|mut required| required.insert(connection_id.to_string()))
340            .unwrap_or(false);
341        #[cfg(target_arch = "wasm32")]
342        let _ = changed;
343        #[cfg(not(target_arch = "wasm32"))]
344        if changed {
345            self.notify_connection_application_route_update();
346        }
347    }
348
349    /// Require reciprocal application protection for every trusted native
350    /// user-device connection promoted after this call.
351    ///
352    /// Products with protected named channels should enable this before
353    /// initializing presence or auto-connect on every participating runtime.
354    #[cfg(not(target_arch = "wasm32"))]
355    pub fn require_app_crypto(&self, required: bool) {
356        self.trusted_user_device_application_crypto_required
357            .store(required, std::sync::atomic::Ordering::Release);
358    }
359
360    #[cfg(not(target_arch = "wasm32"))]
361    pub(crate) fn trusted_user_device_application_crypto_is_required(&self) -> bool {
362        self.trusted_user_device_application_crypto_required
363            .load(std::sync::atomic::Ordering::Acquire)
364    }
365
366    pub(crate) fn clear_connection_application_crypto_key(&self, connection_id: &str) {
367        let Ok(_state) = self.connection_application_crypto_state.write() else {
368            return;
369        };
370        if let Ok(mut keys) = self.connection_application_crypto_keys.write() {
371            keys.remove(connection_id);
372        }
373        if let Ok(mut required) = self.connection_application_crypto_required.write() {
374            required.remove(connection_id);
375        }
376        if let Ok(mut sequences) = self
377            .connection_application_crypto_outbound_sequences
378            .write()
379        {
380            sequences.remove(connection_id);
381        }
382        if let Ok(mut agreements) = self.connection_application_key_agreements.write() {
383            agreements.remove(connection_id);
384        }
385        if let Ok(mut confirmed) = self.connection_application_crypto_confirmed.write() {
386            confirmed.remove(connection_id);
387        }
388        #[cfg(not(target_arch = "wasm32"))]
389        self.notify_connection_application_route_update();
390    }
391
392    pub fn connection_application_crypto_key(
393        &self,
394        connection_id: &str,
395    ) -> Option<[u8; APPLICATION_KEY_BYTES]> {
396        self.application_crypto_key_for_connection(Some(connection_id))
397    }
398
399    pub(crate) fn connection_requires_application_crypto(&self, connection_id: &str) -> bool {
400        let Ok(_state) = self.connection_application_crypto_state.read() else {
401            return false;
402        };
403        self.connection_application_crypto_required
404            .read()
405            .ok()
406            .map(|required| required.contains(connection_id))
407            .unwrap_or(false)
408    }
409
410    pub(crate) fn connection_requires_application_crypto_confirmation(
411        &self,
412        connection_id: &str,
413    ) -> bool {
414        self.connection_requires_application_crypto(connection_id)
415            && self
416                .connection_application_key_agreements
417                .read()
418                .ok()
419                .is_some_and(|agreements| agreements.contains_key(connection_id))
420    }
421
422    #[cfg_attr(not(any(feature = "iroh-carrier-core", test)), allow(dead_code))]
423    pub(crate) fn confirm_connection_application_crypto(
424        &self,
425        connection_id: &str,
426        transport_stable_id: u64,
427    ) {
428        let Ok(_state) = self.connection_application_crypto_state.write() else {
429            return;
430        };
431        let changed = self
432            .connection_application_crypto_confirmed
433            .write()
434            .map(|mut confirmed| match confirmed.get_mut(connection_id) {
435                Some(state) if state.transport_stable_id == transport_stable_id => {
436                    let changed = !state.confirmed;
437                    state.confirmed = true;
438                    changed
439                }
440                Some(_) | None => false,
441            })
442            .unwrap_or(false);
443        #[cfg(target_arch = "wasm32")]
444        let _ = changed;
445        #[cfg(not(target_arch = "wasm32"))]
446        if changed {
447            self.notify_connection_application_route_update();
448        }
449    }
450
451    pub(crate) fn connection_application_crypto_is_confirmed(
452        &self,
453        connection_id: &str,
454        transport_stable_id: Option<u64>,
455    ) -> bool {
456        let Ok(_state) = self.connection_application_crypto_state.read() else {
457            return false;
458        };
459        let Some(transport_stable_id) = transport_stable_id else {
460            return false;
461        };
462        self.connection_application_crypto_confirmed
463            .read()
464            .ok()
465            .and_then(|confirmed| confirmed.get(connection_id).copied())
466            .is_some_and(|state| {
467                state.transport_stable_id == transport_stable_id && state.confirmed
468            })
469    }
470
471    #[cfg(target_arch = "wasm32")]
472    pub(crate) fn connection_application_crypto_confirmation_snapshot(
473        &self,
474        connection_id: &str,
475    ) -> Option<(u64, bool)> {
476        let _state = self.connection_application_crypto_state.read().ok()?;
477        self.connection_application_crypto_confirmed
478            .read()
479            .ok()
480            .and_then(|confirmed| confirmed.get(connection_id).copied())
481            .map(|state| (state.transport_stable_id, state.confirmed))
482    }
483
484    #[cfg(test)]
485    pub(crate) fn connection_application_crypto_has_bound_confirmation(
486        &self,
487        connection_id: &str,
488    ) -> bool {
489        let Ok(_state) = self.connection_application_crypto_state.read() else {
490            return false;
491        };
492        self.connection_application_crypto_confirmed
493            .read()
494            .ok()
495            .and_then(|confirmed| confirmed.get(connection_id).copied())
496            .is_some_and(|state| state.confirmed)
497    }
498
499    /// Bind reciprocal key confirmation to the exact physical generation
500    /// before that generation is published by the asynchronous manager. An
501    /// old ACK can then never reopen the prior route during replacement.
502    pub(crate) fn bind_connection_application_crypto_transport(
503        &self,
504        connection_id: &str,
505        transport_stable_id: u64,
506    ) {
507        let Ok(_state) = self.connection_application_crypto_state.write() else {
508            return;
509        };
510        let changed = self
511            .connection_application_crypto_confirmed
512            .write()
513            .map(|mut confirmed| match confirmed.get(connection_id) {
514                Some(state) if state.transport_stable_id == transport_stable_id => false,
515                _ => {
516                    confirmed.insert(
517                        connection_id.to_string(),
518                        ConnectionApplicationCryptoConfirmation {
519                            transport_stable_id,
520                            confirmed: false,
521                        },
522                    );
523                    true
524                }
525            })
526            .unwrap_or(false);
527        #[cfg(target_arch = "wasm32")]
528        let _ = changed;
529        #[cfg(not(target_arch = "wasm32"))]
530        if changed {
531            self.notify_connection_application_route_update();
532        }
533    }
534
535    pub(crate) fn clear_connection_application_crypto_confirmation(&self, connection_id: &str) {
536        let Ok(_state) = self.connection_application_crypto_state.write() else {
537            return;
538        };
539        let changed = self
540            .connection_application_crypto_confirmed
541            .write()
542            .map(|mut confirmed| {
543                confirmed.get_mut(connection_id).is_some_and(|state| {
544                    let changed = state.confirmed;
545                    state.confirmed = false;
546                    changed
547                })
548            })
549            .unwrap_or(false);
550        #[cfg(target_arch = "wasm32")]
551        let _ = changed;
552        #[cfg(not(target_arch = "wasm32"))]
553        if changed {
554            self.notify_connection_application_route_update();
555        }
556    }
557
558    /// Wait for the existing admission and key-confirmation owners to commit
559    /// the complete application route observed by an incoming product stream.
560    /// QUIC orders bytes within one stream but not across streams, so the
561    /// product stream can arrive just before a directional admission proof or
562    /// the control ACK even when the sender emitted control first.
563    ///
564    /// This does not advance admission, confirm a key, or retry a lifecycle
565    /// transition. It only waits on the typed route event and fails closed if
566    /// the key, token epoch, or exact transport proof does not converge.
567    #[cfg(not(target_arch = "wasm32"))]
568    pub(crate) async fn wait_for_connection_application_route(
569        &self,
570        connection_id: &str,
571        expected_transport_stable_id: u64,
572        expected_key: [u8; APPLICATION_KEY_BYTES],
573        expected_admission_fingerprint: Option<&str>,
574        expected_security_epoch_fingerprint: Option<&str>,
575        timeout: std::time::Duration,
576    ) -> bool {
577        let deadline = tokio::time::Instant::now() + timeout;
578        loop {
579            // Register the waiter before checking state so a concurrent ACK
580            // cannot notify between the state read and the await.
581            let notified = self.connection_application_route_updates.notified();
582            let same_epoch = self.connection_application_crypto_key(connection_id)
583                == Some(expected_key)
584                && self
585                    .session_token_registry
586                    .admission_fingerprint(connection_id)
587                    .as_deref()
588                    == expected_admission_fingerprint
589                && self
590                    .session_token_registry
591                    .application_security_epoch_fingerprint(connection_id)
592                    .as_deref()
593                    == expected_security_epoch_fingerprint;
594            if !same_epoch {
595                return false;
596            }
597            if self.native_application_stream_admitted_for_transport(
598                connection_id,
599                expected_transport_stable_id,
600            ) {
601                return true;
602            }
603            if tokio::time::timeout_at(deadline, notified).await.is_err() {
604                return false;
605            }
606        }
607    }
608
609    pub(crate) fn application_crypto_key_for_connection(
610        &self,
611        connection_id: Option<&str>,
612    ) -> Option<[u8; APPLICATION_KEY_BYTES]> {
613        let connection_id = connection_id?.trim();
614        if connection_id.is_empty() {
615            return None;
616        }
617        let _state = self.connection_application_crypto_state.read().ok()?;
618        self.connection_application_crypto_keys
619            .read()
620            .ok()?
621            .get(connection_id)
622            .copied()
623    }
624
625    fn application_crypto_requirement_and_key(
626        &self,
627        connection_id: &str,
628    ) -> (bool, Option<[u8; APPLICATION_KEY_BYTES]>) {
629        let Ok(_state) = self.connection_application_crypto_state.read() else {
630            return (false, None);
631        };
632        let required = self
633            .connection_application_crypto_required
634            .read()
635            .ok()
636            .is_some_and(|required| required.contains(connection_id));
637        let key = self
638            .connection_application_crypto_keys
639            .read()
640            .ok()
641            .and_then(|keys| keys.get(connection_id).copied());
642        (required, key)
643    }
644
645    pub(crate) fn get_or_create_connection_key_agreement(
646        &self,
647        connection_id: &str,
648    ) -> Result<crate::key_agreement::KeyAgreement, crate::key_agreement::KeyAgreementError> {
649        let connection_id = connection_id.trim();
650        let Ok(_state) = self.connection_application_crypto_state.write() else {
651            return crate::key_agreement::KeyAgreement::generate();
652        };
653        if let Some(existing) = self
654            .connection_application_key_agreements
655            .read()
656            .ok()
657            .and_then(|agreements| agreements.get(connection_id).cloned())
658        {
659            return Ok(existing);
660        }
661
662        let generated = crate::key_agreement::KeyAgreement::generate()?;
663        if let Ok(mut agreements) = self.connection_application_key_agreements.write() {
664            Ok(agreements
665                .entry(connection_id.to_string())
666                .or_insert_with(|| generated.clone())
667                .clone())
668        } else {
669            Ok(generated)
670        }
671    }
672
673    #[cfg_attr(
674        any(target_arch = "wasm32", not(feature = "transport-webrtc")),
675        allow(dead_code)
676    )]
677    pub(crate) fn connection_key_agreement_public_key(
678        &self,
679        connection_id: &str,
680    ) -> Option<[u8; crate::key_agreement::PUBLIC_KEY_BYTES]> {
681        let _state = self.connection_application_crypto_state.read().ok()?;
682        self.connection_application_key_agreements
683            .read()
684            .ok()?
685            .get(connection_id.trim())
686            .map(|agreement| agreement.public_key_bytes())
687    }
688
689    pub(crate) async fn application_crypto_key_for_endpoint(
690        &self,
691        endpoint_id: &iroh::EndpointId,
692    ) -> Option<[u8; APPLICATION_KEY_BYTES]> {
693        let endpoint_str = endpoint_id.to_string();
694        let records = self
695            .connection_manager
696            .get_by_endpoint_id(&endpoint_str)
697            .await;
698        for record in records {
699            if let Some(key) =
700                self.application_crypto_key_for_connection(Some(&record.connection_id))
701            {
702                return Some(key);
703            }
704        }
705        None
706    }
707
708    pub(crate) async fn application_crypto_key_for_connection_or_endpoint(
709        &self,
710        connection_id: Option<&str>,
711        endpoint_id: &iroh::EndpointId,
712    ) -> Option<[u8; APPLICATION_KEY_BYTES]> {
713        let endpoint_str = endpoint_id.to_string();
714
715        if let Some(connection_id) = connection_id.map(str::trim).filter(|id| !id.is_empty()) {
716            // A durable-device projection can briefly retain the prior browser
717            // connection id while the gateway roster has already supplied the replacement
718            // endpoint. Never bind that stale generation's crypto key to a
719            // stream opened on the new endpoint; validate physical ownership
720            // before preferring the projected connection id.
721            if let Some(record) = self
722                .connection_manager
723                .get_by_connection_id(connection_id)
724                .await
725            {
726                let record_matches_endpoint = record
727                    .endpoint_id
728                    .as_deref()
729                    .or(record.node_id.as_deref())
730                    .is_some_and(|value| value == endpoint_str);
731                if record_matches_endpoint {
732                    if let Some(key) =
733                        self.application_crypto_key_for_connection(Some(connection_id))
734                    {
735                        return Some(key);
736                    }
737                }
738            }
739        }
740
741        if let Some(record) = self
742            .connection_manager
743            .best_connection_for_peer(&endpoint_str)
744            .await
745        {
746            if let Some(key) =
747                self.application_crypto_key_for_connection(Some(&record.connection_id))
748            {
749                return Some(key);
750            }
751        }
752
753        let mut records = self
754            .connection_manager
755            .get_by_endpoint_id(&endpoint_str)
756            .await;
757        records.sort_by(|left, right| {
758            let left_connected = matches!(
759                left.state,
760                crate::connection_manager::ConnectionState::Connected
761            );
762            let right_connected = matches!(
763                right.state,
764                crate::connection_manager::ConnectionState::Connected
765            );
766            left_connected
767                .cmp(&right_connected)
768                .then_with(|| left.transport_generation.cmp(&right.transport_generation))
769                .then_with(|| left.updated_at_ms.cmp(&right.updated_at_ms))
770        });
771        for record in records.into_iter().rev() {
772            if let Some(key) =
773                self.application_crypto_key_for_connection(Some(&record.connection_id))
774            {
775                return Some(key);
776            }
777        }
778
779        None
780    }
781
782    async fn application_crypto_required_for_connection_or_endpoint(
783        &self,
784        connection_id: Option<&str>,
785        endpoint_id: &iroh::EndpointId,
786    ) -> bool {
787        if connection_id
788            .map(str::trim)
789            .filter(|value| !value.is_empty())
790            .is_some_and(|value| self.connection_requires_application_crypto(value))
791        {
792            return true;
793        }
794
795        let endpoint_id = endpoint_id.to_string();
796        self.connection_manager
797            .get_by_endpoint_id(&endpoint_id)
798            .await
799            .iter()
800            .any(|record| self.connection_requires_application_crypto(&record.connection_id))
801    }
802
803    pub(crate) async fn required_crypto_key(
804        &self,
805        connection_id: Option<&str>,
806        endpoint_id: &iroh::EndpointId,
807        operation: &str,
808    ) -> anyhow::Result<Option<[u8; APPLICATION_KEY_BYTES]>> {
809        if let Some(connection_id) = connection_id
810            .map(str::trim)
811            .filter(|value| !value.is_empty())
812        {
813            let endpoint_id_text = endpoint_id.to_string();
814            if self
815                .connection_manager
816                .get_by_connection_id(connection_id)
817                .await
818                .is_some_and(|record| {
819                    record
820                        .endpoint_id
821                        .as_deref()
822                        .or(record.node_id.as_deref())
823                        .is_some_and(|value| value == endpoint_id_text)
824                })
825            {
826                let (required, key) = self.application_crypto_requirement_and_key(connection_id);
827                return enforce_application_crypto_requirement(required, key, operation);
828            }
829        }
830
831        let key = self
832            .application_crypto_key_for_connection_or_endpoint(connection_id, endpoint_id)
833            .await;
834        let required = self
835            .application_crypto_required_for_connection_or_endpoint(connection_id, endpoint_id)
836            .await;
837        enforce_application_crypto_requirement(required, key, operation)
838    }
839
840    /// Wrap an incoming product stream against the exact logical connection
841    /// selected by the native ingress router. Product hosts should prefer this
842    /// over endpoint-only lookup so a retired same-endpoint record can never
843    /// supply a stale application key.
844    pub fn secure_connection_bi(
845        &self,
846        connection_id: &str,
847        send: SendStream,
848        recv: RecvStream,
849    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
850        self.secure_connection_bi_with_prefix(connection_id, send, recv, &[])
851    }
852
853    /// Wrap an incoming product stream against the exact logical connection
854    /// while restoring wire bytes consumed by the Rust admission classifier.
855    /// Embedding hosts must pass `IncomingStream::recv_prefix` here; dropping
856    /// it would corrupt the first protected frame.
857    pub fn secure_connection_bi_with_prefix(
858        &self,
859        connection_id: &str,
860        send: SendStream,
861        recv: RecvStream,
862        recv_prefix: &[u8],
863    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
864        let (required, key) = self.application_crypto_requirement_and_key(connection_id);
865        let key =
866            enforce_application_crypto_requirement(required, key, "incoming application stream")?;
867        self.wrap_application_streams(connection_id, key, send, recv, recv_prefix)
868    }
869
870    /// Observe existing admission and crypto authority throughout product I/O.
871    /// Weak references do not keep a retired Client alive. Same-scope bearer
872    /// renewal is allowed; a changed key, scope, device or confirmation is not.
873    pub(crate) fn application_stream_access(
874        &self,
875        connection_id: &str,
876        key: [u8; APPLICATION_KEY_BYTES],
877    ) -> anyhow::Result<ApplicationStreamAccess> {
878        self.application_stream_access_for_admission(
879            connection_id,
880            key,
881            ApplicationStreamAdmission::Latest,
882        )
883    }
884
885    pub(crate) fn application_stream_access_for_admission(
886        &self,
887        connection_id: &str,
888        key: [u8; APPLICATION_KEY_BYTES],
889        selected: ApplicationStreamAdmission,
890    ) -> anyhow::Result<ApplicationStreamAccess> {
891        use crate::session_token::SessionAdmission;
892        let admission = match &selected {
893            ApplicationStreamAdmission::Scope(scope) => self
894                .session_token_registry
895                .admission_for_scope(connection_id, scope)
896                .ok_or_else(|| {
897                    anyhow::anyhow!("requested application stream scope is not admitted")
898                })?,
899            _ => self.session_admission(connection_id),
900        };
901        anyhow::ensure!(
902            matches!(&admission, SessionAdmission::Accepted { .. })
903                || (matches!(selected, ApplicationStreamAdmission::Latest)
904                    && admission == SessionAdmission::Pending
905                    && self.session_token_registry.is_empty()),
906            "application stream is not admitted"
907        );
908        let confirmation = self
909            .connection_application_crypto_confirmed
910            .read()
911            .map_err(|_| anyhow::anyhow!("application crypto state unavailable"))?
912            .get(connection_id)
913            .copied();
914        let registry = Arc::downgrade(&self.session_token_registry);
915        let state = Arc::downgrade(&self.connection_application_crypto_state);
916        let keys = Arc::downgrade(&self.connection_application_crypto_keys);
917        let confirmations = Arc::downgrade(&self.connection_application_crypto_confirmed);
918        let id = connection_id.to_owned();
919        Ok(ApplicationStreamAccess::new(move || {
920            let (Some(registry), Some(state), Some(keys), Some(confirmations)) = (
921                registry.upgrade(),
922                state.upgrade(),
923                keys.upgrade(),
924                confirmations.upgrade(),
925            ) else {
926                return false;
927            };
928            let admitted = match &selected {
929                ApplicationStreamAdmission::Latest => {
930                    registry.admission(&id) == admission
931                        && (admission != SessionAdmission::Pending || registry.is_empty())
932                }
933                ApplicationStreamAdmission::Scope(scope) => {
934                    registry.admission_for_scope(&id, scope).as_ref() == Some(&admission)
935                }
936                ApplicationStreamAdmission::Envelope => {
937                    matches!(registry.admission(&id), SessionAdmission::Accepted { .. })
938                }
939            };
940            if !admitted {
941                tracing::debug!(target: "openrtc::stream", event = "access_denied",
942                    connection_ref = %crate::diagnostics::reference(&id), reason = "authority_changed",
943                    "Protected stream authority changed");
944                return false;
945            }
946            let Ok(_state) = state.read() else {
947                return false;
948            };
949            let Ok(keys) = keys.read() else {
950                return false;
951            };
952            let Ok(confirmations) = confirmations.read() else {
953                return false;
954            };
955            let key_current = keys.get(&id) == Some(&key);
956            let generation_current = confirmations.get(&id).copied() == confirmation;
957            drop(confirmations);
958            drop(keys);
959            drop(_state);
960            if !key_current || !generation_current {
961                tracing::debug!(target: "openrtc::stream", event = "access_denied",
962                    connection_ref = %crate::diagnostics::reference(&id),
963                    key_current, generation_current, "Protected stream crypto epoch changed");
964            }
965            key_current && generation_current
966        }))
967    }
968
969    pub(crate) fn wrap_application_streams(
970        &self,
971        connection_id: &str,
972        key: Option<[u8; APPLICATION_KEY_BYTES]>,
973        send: SendStream,
974        recv: RecvStream,
975        recv_prefix: &[u8],
976    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
977        self.wrap_application_streams_for_admission(
978            connection_id,
979            key,
980            send,
981            recv,
982            recv_prefix,
983            ApplicationStreamAdmission::Latest,
984        )
985    }
986
987    pub(crate) fn wrap_application_streams_for_admission(
988        &self,
989        connection_id: &str,
990        key: Option<[u8; APPLICATION_KEY_BYTES]>,
991        send: SendStream,
992        recv: RecvStream,
993        recv_prefix: &[u8],
994        selected: ApplicationStreamAdmission,
995    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
996        match key {
997            Some(key) => {
998                let access = match selected {
999                    ApplicationStreamAdmission::Latest => {
1000                        self.application_stream_access(connection_id, key)?
1001                    }
1002                    selected => {
1003                        self.application_stream_access_for_admission(connection_id, key, selected)?
1004                    }
1005                };
1006                Ok((
1007                    PeerSendStream::encrypted(send, key).with_access(access.clone()),
1008                    PeerRecvStream::encrypted_with_prefix(recv, key, recv_prefix)
1009                        .map_err(|error| {
1010                            anyhow::anyhow!(
1011                                "incoming application crypto stream wrap failed: {error}"
1012                            )
1013                        })?
1014                        .with_access(access),
1015                ))
1016            }
1017            None if recv_prefix.is_empty()
1018                && matches!(selected, ApplicationStreamAdmission::Latest) =>
1019            {
1020                Ok((PeerSendStream::plain(send), PeerRecvStream::plain(recv)))
1021            }
1022            None => anyhow::bail!(
1023                "incoming application stream carried an encrypted prefix without an application key"
1024            ),
1025        }
1026    }
1027
1028    /// Wrap a native product stream against the exact admitted physical
1029    /// transport generation that produced it. Native host bridges should use
1030    /// this boundary before exposing stream bytes outside Rust so application
1031    /// keys remain owned by the runtime and a same-endpoint replacement cannot
1032    /// lend its key to a retired stream.
1033    #[cfg(not(target_arch = "wasm32"))]
1034    pub async fn wrap_incoming_bi(
1035        &self,
1036        endpoint_id: &iroh::EndpointId,
1037        transport_stable_id: u64,
1038        send: SendStream,
1039        recv: RecvStream,
1040    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
1041        self.wrap_bi_with_prefix(endpoint_id, transport_stable_id, send, recv, &[])
1042            .await
1043    }
1044
1045    /// Generation-fenced incoming wrapper that also restores bytes inspected
1046    /// by the native admission router while classifying an out-of-order
1047    /// encrypted product frame.
1048    #[cfg(not(target_arch = "wasm32"))]
1049    pub async fn wrap_bi_with_prefix(
1050        &self,
1051        endpoint_id: &iroh::EndpointId,
1052        transport_stable_id: u64,
1053        send: SendStream,
1054        recv: RecvStream,
1055        recv_prefix: &[u8],
1056    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
1057        self.wrap_bi_with_prefix_for_admission(
1058            endpoint_id,
1059            transport_stable_id,
1060            send,
1061            recv,
1062            recv_prefix,
1063            ApplicationStreamAdmission::Latest,
1064        )
1065        .await
1066    }
1067
1068    #[cfg(not(target_arch = "wasm32"))]
1069    pub(crate) async fn wrap_bi_with_prefix_for_admission(
1070        &self,
1071        endpoint_id: &iroh::EndpointId,
1072        transport_stable_id: u64,
1073        send: SendStream,
1074        recv: RecvStream,
1075        recv_prefix: &[u8],
1076        selected: ApplicationStreamAdmission,
1077    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
1078        let current_transport_stable_id = self
1079            .get_connection(*endpoint_id)
1080            .await
1081            .map(|connection| crate::transport_generation::for_connection(&connection));
1082        if current_transport_stable_id != Some(transport_stable_id) {
1083            anyhow::bail!(
1084                "incoming application stream belongs to a retired transport generation: incoming={} current={:?}",
1085                transport_stable_id,
1086                current_transport_stable_id,
1087            );
1088        }
1089        let local_node_id = self.current_node_id().await.ok_or_else(|| {
1090            anyhow::anyhow!("local node id unavailable for incoming application stream")
1091        })?;
1092        let connection_id =
1093            Self::deterministic_connection_id(&local_node_id, &endpoint_id.to_string());
1094        if !self
1095            .native_application_stream_admitted_for_transport(&connection_id, transport_stable_id)
1096        {
1097            anyhow::bail!("incoming application stream transport generation is not admitted");
1098        }
1099        let key = enforce_application_crypto_requirement(
1100            self.connection_requires_application_crypto(&connection_id),
1101            self.application_crypto_key_for_connection(Some(&connection_id)),
1102            "incoming application stream",
1103        )?;
1104        self.wrap_application_streams_for_admission(
1105            &connection_id,
1106            key,
1107            send,
1108            recv,
1109            recv_prefix,
1110            selected,
1111        )
1112    }
1113
1114    /// Wrap an admitted incoming application stream with the same per-peer
1115    /// application-crypto boundary used by `open_peer_bi` on the sender.
1116    /// Product hosts should call this before parsing channel envelopes or
1117    /// application frames from the public incoming-stream queue.
1118    pub async fn secure_incoming_bi(
1119        &self,
1120        endpoint_id: &iroh::EndpointId,
1121        send: SendStream,
1122        recv: RecvStream,
1123    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
1124        let key = self
1125            .required_crypto_key(None, endpoint_id, "incoming application stream")
1126            .await?;
1127        if key.is_none() {
1128            return wrap_peer_streams(None, send, recv).map_err(Into::into);
1129        }
1130        let connection_id = self
1131            .peer_session(&endpoint_id.to_string())
1132            .await
1133            .and_then(|snapshot| snapshot.active_connection_id)
1134            .ok_or_else(|| {
1135                anyhow::anyhow!("protected incoming stream has no active logical connection")
1136            })?;
1137        self.wrap_application_streams(&connection_id, key, send, recv, &[])
1138    }
1139
1140    pub(crate) fn wrap_peer_streams_for_connection(
1141        &self,
1142        connection_id: Option<&str>,
1143        send: SendStream,
1144        recv: RecvStream,
1145    ) -> anyhow::Result<(PeerSendStream, PeerRecvStream)> {
1146        // SDK candidate/admission control deliberately does not use the product
1147        // access fence: it proves or restores the admission being checked.
1148        let key = self.application_crypto_key_for_connection(connection_id);
1149        wrap_peer_streams(key, send, recv)
1150            .map_err(|error| anyhow::anyhow!("application crypto stream wrap failed: {error}"))
1151    }
1152
1153    pub(crate) async fn assert_raw_peer_stream_allowed(
1154        &self,
1155        endpoint_id: &iroh::EndpointId,
1156    ) -> anyhow::Result<()> {
1157        if self
1158            .application_crypto_key_for_endpoint(endpoint_id)
1159            .await
1160            .is_some()
1161        {
1162            anyhow::bail!(
1163                "raw iroh stream open is not allowed while application crypto is active for this peer; use open_peer_bi/open_peer_uni instead"
1164            );
1165        }
1166        Ok(())
1167    }
1168
1169    pub(crate) fn protect_outbound_application_payload(
1170        &self,
1171        connection_id: &str,
1172        payload: &[u8],
1173    ) -> anyhow::Result<Vec<u8>> {
1174        self.protect_outbound_application_payload_with_type(connection_id, 0, payload)
1175    }
1176
1177    pub(crate) fn protect_outbound_application_payload_with_type(
1178        &self,
1179        connection_id: &str,
1180        type_id: u8,
1181        payload: &[u8],
1182    ) -> anyhow::Result<Vec<u8>> {
1183        let _state = self
1184            .connection_application_crypto_state
1185            .read()
1186            .map_err(|_| anyhow::anyhow!("application crypto state poisoned"))?;
1187        let keys = self
1188            .connection_application_crypto_keys
1189            .read()
1190            .map_err(|_| anyhow::anyhow!("application crypto key map poisoned"))?;
1191        let Some(key) = keys.get(connection_id) else {
1192            if self
1193                .connection_application_crypto_required
1194                .read()
1195                .ok()
1196                .is_some_and(|required| required.contains(connection_id))
1197            {
1198                anyhow::bail!(
1199                    "application crypto required for connection {connection_id} but no key is installed"
1200                );
1201            }
1202            return Ok(payload.to_vec());
1203        };
1204        if application_crypto::is_encrypted_payload(payload) {
1205            return Ok(payload.to_vec());
1206        }
1207        let sequence = self
1208            .connection_application_crypto_outbound_sequences
1209            .write()
1210            .ok()
1211            .and_then(|mut sequences| {
1212                let entry = sequences.entry(connection_id.to_string()).or_insert(0);
1213                let current = *entry;
1214                *entry = current.saturating_add(1);
1215                Some(current)
1216            })
1217            .unwrap_or(0);
1218        let mut nonce = [0u8; application_crypto::APPLICATION_NONCE_BYTES];
1219        getrandom::getrandom(&mut nonce)
1220            .map_err(|_| anyhow::anyhow!("application crypto random nonce failed"))?;
1221        application_crypto::protect_payload_with_nonce_and_sequence(
1222            key, type_id, payload, &nonce, sequence,
1223        )
1224        .map_err(|error| anyhow::anyhow!("application crypto protect failed: {:?}", error))
1225    }
1226
1227    #[cfg_attr(
1228        not(any(feature = "transport-webrtc", feature = "transport-moq")),
1229        allow(dead_code)
1230    )]
1231    pub(crate) fn open_inbound_application_payload(
1232        &self,
1233        connection_id: &str,
1234        payload: &[u8],
1235    ) -> anyhow::Result<Vec<u8>> {
1236        self.open_inbound_application_payload_with_type(connection_id, 0, payload)
1237    }
1238
1239    pub(crate) fn open_inbound_application_payload_with_type(
1240        &self,
1241        connection_id: &str,
1242        expected_type_id: u8,
1243        payload: &[u8],
1244    ) -> anyhow::Result<Vec<u8>> {
1245        let Some(key) = self.application_crypto_key_for_connection(Some(connection_id)) else {
1246            if self.connection_requires_application_crypto(connection_id) {
1247                anyhow::bail!(
1248                    "application crypto required for connection {connection_id} but no key is installed"
1249                );
1250            }
1251            return Ok(payload.to_vec());
1252        };
1253
1254        application_crypto::open_payload(&key, expected_type_id, payload, true)
1255            .map_err(|error| anyhow::anyhow!("application crypto open failed: {:?}", error))
1256    }
1257
1258    /// SDK-owned default messages never depend on an embedding application's
1259    /// stream reader. Called only by the sole native ingress router, after
1260    /// admission. Unknown channels retain their original ciphertext prefix.
1261    #[cfg(not(target_arch = "wasm32"))]
1262    pub(crate) async fn route_admitted_peer_message(
1263        &self,
1264        endpoint_id: iroh::EndpointId,
1265        transport_stable_id: u64,
1266        send: SendStream,
1267        mut recv: RecvStream,
1268        mut wire_prefix: Vec<u8>,
1269    ) -> anyhow::Result<crate::client::BiStreamDisposition> {
1270        use crate::stream_metadata::{decode_prefix, DecodeDecision, DEFAULT_PEER_CHANNEL_ID};
1271        let remote = endpoint_id.to_string();
1272        let local = self
1273            .current_node_id()
1274            .await
1275            .ok_or_else(|| anyhow::anyhow!("missing local identity"))?;
1276        let id = Self::deterministic_connection_id(&local, &remote);
1277        let key = self.application_crypto_key_for_connection(Some(&id));
1278        let mut decoder = key
1279            .map(|key| application_crypto::CryptoFrameDecoder::new(&key))
1280            .transpose()
1281            .map_err(|error| anyhow::anyhow!("peer stream decoder: {error:?}"))?;
1282        let mut plaintext = Vec::new();
1283        let mut decoded_wire = 0;
1284        let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
1285        // Same per-frame ceiling as the existing crypto wrapper, plus the
1286        // framing prefix. The existing ingress semaphore bounds concurrency.
1287        let inspection_limit = application_crypto::DEFAULT_MAX_RAW_FRAME_BYTES + 4;
1288        let envelope_len = loop {
1289            if let Some(decoder) = decoder.as_mut() {
1290                for frame in decoder
1291                    .push(&wire_prefix[decoded_wire..])
1292                    .map_err(|error| anyhow::anyhow!("peer stream prefix: {error:?}"))?
1293                {
1294                    plaintext.extend_from_slice(&frame);
1295                }
1296            } else {
1297                plaintext.extend_from_slice(&wire_prefix[decoded_wire..]);
1298            }
1299            decoded_wire = wire_prefix.len();
1300            match decode_prefix(&plaintext) {
1301                DecodeDecision::Decoded(prefix)
1302                    if prefix.channel.channel_id == DEFAULT_PEER_CHANNEL_ID =>
1303                {
1304                    break prefix.consumed_len;
1305                }
1306                DecodeDecision::NeedMore(_) => {}
1307                _ => {
1308                    return Ok(crate::client::BiStreamDisposition::Forward {
1309                        send,
1310                        recv,
1311                        recv_prefix: wire_prefix,
1312                    })
1313                }
1314            }
1315            anyhow::ensure!(
1316                wire_prefix.len() < inspection_limit,
1317                "peer stream prefix exceeds inspection bound"
1318            );
1319            let mut chunk = vec![0; (inspection_limit - wire_prefix.len()).min(4096)];
1320            let read = tokio::time::timeout_at(deadline, recv.read(&mut chunk))
1321                .await??
1322                .unwrap_or(0);
1323            if read == 0 {
1324                // Preserve incomplete/unclaimed prefixes for the host's own
1325                // protocol classifier; never reinterpret them as a message.
1326                return Ok(crate::client::BiStreamDisposition::Forward {
1327                    send,
1328                    recv,
1329                    recv_prefix: wire_prefix,
1330                });
1331            }
1332            wire_prefix.extend_from_slice(&chunk[..read]);
1333        };
1334        anyhow::ensure!(
1335            self.application_crypto_key_for_connection(Some(&id)) == key,
1336            "peer stream security epoch changed"
1337        );
1338        let (send, mut recv) = match key {
1339            Some(_) => {
1340                self.wrap_bi_with_prefix(
1341                    &endpoint_id,
1342                    transport_stable_id,
1343                    send,
1344                    recv,
1345                    &wire_prefix,
1346                )
1347                .await?
1348            }
1349            None => (
1350                PeerSendStream::plain(send),
1351                PeerRecvStream::plain(recv).with_plaintext_prefix(wire_prefix),
1352            ),
1353        };
1354        async fn read_exact(recv: &mut PeerRecvStream, bytes: &mut [u8]) -> anyhow::Result<bool> {
1355            let mut offset = 0;
1356            while offset < bytes.len() {
1357                let read = recv.read(&mut bytes[offset..]).await?;
1358                if read == 0 {
1359                    anyhow::ensure!(offset == 0, "truncated peer message frame");
1360                    return Ok(false);
1361                }
1362                offset += read;
1363            }
1364            Ok(true)
1365        }
1366        let mut envelope = vec![0; envelope_len];
1367        anyhow::ensure!(
1368            tokio::time::timeout_at(deadline, read_exact(&mut recv, &mut envelope)).await??,
1369            "missing peer envelope"
1370        );
1371        loop {
1372            let frame_deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
1373            let mut length = [0; 4];
1374            if !tokio::time::timeout_at(frame_deadline, read_exact(&mut recv, &mut length))
1375                .await??
1376            {
1377                break;
1378            }
1379            let length = u32::from_be_bytes(length) as usize;
1380            // Payload encryption adds nonce/version/tag framing; decoded
1381            // application bytes are checked again by open_inbound_frame.
1382            anyhow::ensure!(
1383                length > 0 && length <= crate::native_protocol::MAX_PEER_MESSAGE_BYTES + 128,
1384                "invalid peer message frame length {length}"
1385            );
1386            let mut frame = vec![0; length];
1387            anyhow::ensure!(
1388                tokio::time::timeout_at(frame_deadline, read_exact(&mut recv, &mut frame))
1389                    .await??,
1390                "missing peer message body"
1391            );
1392            anyhow::ensure!(
1393                self.application_crypto_key_for_connection(Some(&id)) == key,
1394                "peer message security epoch changed"
1395            );
1396            anyhow::ensure!(
1397                self.open_inbound_frame(
1398                    &id,
1399                    Some(&remote),
1400                    "iroh",
1401                    Some(transport_stable_id),
1402                    &frame
1403                )
1404                .await?,
1405                "invalid peer message body"
1406            );
1407        }
1408        let _ = send.finish();
1409        Ok(crate::client::BiStreamDisposition::Consumed)
1410    }
1411
1412    #[cfg(not(target_arch = "wasm32"))]
1413    pub async fn open_inbound_frame(
1414        &self,
1415        connection_id: &str,
1416        remote_node_id: Option<&str>,
1417        transport: &str,
1418        expected_transport_stable_id: Option<u64>,
1419        frame: &[u8],
1420    ) -> anyhow::Result<bool> {
1421        let Some((&type_id, protected_payload)) = frame.split_first() else {
1422            return Ok(false);
1423        };
1424        if type_id != 0 {
1425            return Ok(false);
1426        }
1427        if !application_crypto::is_encrypted_payload(protected_payload)
1428            && self.connection_requires_application_crypto(connection_id)
1429        {
1430            return Ok(false);
1431        }
1432
1433        let generation = self
1434            .current_native_peer_data_generation(connection_id, expected_transport_stable_id)
1435            .await
1436            .ok_or_else(|| anyhow::anyhow!("native peer-data route generation is stale"))?;
1437        if self.session_registry_active() || self.offline_admission_is_required(connection_id) {
1438            anyhow::ensure!(
1439                self.native_application_stream_admitted_for_transport(
1440                    connection_id,
1441                    generation.transport_stable_id
1442                ),
1443                "peer message transport is not admitted"
1444            );
1445        }
1446        let payload = self.open_inbound_application_payload(connection_id, protected_payload)?;
1447        anyhow::ensure!(
1448            payload.len() <= crate::native_protocol::MAX_PEER_MESSAGE_BYTES,
1449            "peer message exceeds payload limit"
1450        );
1451        self.emit_native_peer_data(crate::client::NativePeerDataEvent {
1452            connection_id: connection_id.to_string(),
1453            remote_node_id: remote_node_id
1454                .map(str::trim)
1455                .filter(|value| !value.is_empty())
1456                .map(ToOwned::to_owned),
1457            transport: transport.to_string(),
1458            transport_stable_id: generation.transport_stable_id,
1459            transport_generation: generation.transport_generation,
1460            route_generation: generation.route_generation,
1461            payload,
1462        });
1463        Ok(true)
1464    }
1465
1466    pub(crate) fn connection_ids_requiring_application_crypto(
1467        &self,
1468        connection_ids: &[String],
1469    ) -> bool {
1470        let Ok(_state) = self.connection_application_crypto_state.read() else {
1471            return false;
1472        };
1473        self.connection_application_crypto_required
1474            .read()
1475            .ok()
1476            .map(|required| {
1477                connection_ids
1478                    .iter()
1479                    .any(|connection_id| required.contains(connection_id))
1480            })
1481            .unwrap_or(false)
1482    }
1483}
1484
1485fn enforce_application_crypto_requirement(
1486    required: bool,
1487    key: Option<[u8; APPLICATION_KEY_BYTES]>,
1488    operation: &str,
1489) -> anyhow::Result<Option<[u8; APPLICATION_KEY_BYTES]>> {
1490    if required && key.is_none() {
1491        anyhow::bail!(
1492            "application crypto is required for {operation}, but the current connection has no installed key"
1493        );
1494    }
1495    Ok(key)
1496}
1497
1498#[cfg(test)]
1499mod security_tests {
1500    use super::enforce_application_crypto_requirement;
1501    use crate::application_crypto::APPLICATION_KEY_BYTES;
1502
1503    #[tokio::test]
1504    async fn scoped_application_stream_access_preserves_sibling_withdrawal_and_fences_selected_authority(
1505    ) {
1506        use crate::{
1507            application_crypto_streams::ApplicationStreamAdmission, client::ClientBuilder,
1508            session_token::AdmissionMechanism,
1509        };
1510        for change in [
1511            "revoke",
1512            "expire",
1513            "device",
1514            "reject",
1515            "key",
1516            "generation",
1517            "owner-drop",
1518        ] {
1519            let client = ClientBuilder::new_provider_neutral(
1520                "scoped-stream-access-test".into(),
1521                Box::new(|| None),
1522            )
1523            .build();
1524            let registry = client.session_token_registry.clone();
1525            registry.register("app-old".into(), "ticket:app", 16);
1526            registry
1527                .validate_and_consume_for_connection("app-old", Some("peer"))
1528                .unwrap();
1529            client.set_connection_application_crypto_key("peer", [7; APPLICATION_KEY_BYTES]);
1530            let access = client
1531                .application_stream_access_for_admission(
1532                    "peer",
1533                    [7; APPLICATION_KEY_BYTES],
1534                    ApplicationStreamAdmission::Scope("ticket:app".into()),
1535                )
1536                .unwrap();
1537            access.check().unwrap();
1538            assert!(client
1539                .application_stream_access_for_admission(
1540                    "peer",
1541                    [7; APPLICATION_KEY_BYTES],
1542                    ApplicationStreamAdmission::Scope("ticket:missing".into())
1543                )
1544                .is_err());
1545            registry.register("vpn".into(), "ticket:vpn", 16);
1546            registry
1547                .validate_and_consume_for_connection("vpn", Some("peer"))
1548                .unwrap();
1549            access
1550                .check()
1551                .expect("adding a sibling scope preserves selected app permission");
1552            registry.revoke("vpn");
1553            access
1554                .check()
1555                .expect("withdrawing a sibling scope preserves selected app permission");
1556            registry.register("app-renewed".into(), "ticket:app", 16);
1557            registry
1558                .validate_and_consume_for_connection("app-renewed", Some("peer"))
1559                .unwrap();
1560            registry.revoke("app-old");
1561            access
1562                .check()
1563                .expect("renewal of selected authority preserves its stream");
1564            registry.register("vpn-new".into(), "ticket:vpn", 16);
1565            registry
1566                .validate_and_consume_for_connection("vpn-new", Some("peer"))
1567                .unwrap();
1568            access
1569                .check()
1570                .expect("adding a sibling scope preserves selected app permission");
1571            match change {
1572                "revoke" => {
1573                    registry.revoke("app-renewed");
1574                }
1575                "expire" => registry.register_with_expiry_ms(
1576                    "app-renewed".into(),
1577                    "ticket:app",
1578                    16,
1579                    Some(0),
1580                ),
1581                "device" => registry.mark_accepted(
1582                    "peer",
1583                    AdmissionMechanism::SessionToken,
1584                    Some("ticket:app".into()),
1585                    Some("changed-authority".into()),
1586                ),
1587                "reject" => registry.mark_rejected("peer", "explicit rejection"),
1588                "key" => {
1589                    client.set_connection_application_crypto_key("peer", [8; APPLICATION_KEY_BYTES])
1590                }
1591                "generation" => {
1592                    client
1593                        .connection_application_crypto_confirmed
1594                        .write()
1595                        .unwrap()
1596                        .insert(
1597                            "peer".into(),
1598                            super::ConnectionApplicationCryptoConfirmation {
1599                                transport_stable_id: 2,
1600                                confirmed: true,
1601                            },
1602                        );
1603                }
1604                "owner-drop" => drop(client),
1605                _ => unreachable!(),
1606            }
1607            assert!(
1608                access.check().is_err(),
1609                "selected authority must deny after {change}"
1610            );
1611            registry.register("app-fresh".into(), "ticket:app", 16);
1612            let fresh = registry.validate_and_consume_for_connection("app-fresh", Some("peer"));
1613            if change == "reject" {
1614                assert!(
1615                    fresh.is_err(),
1616                    "explicit rejection cannot fall back to a retained grant"
1617                );
1618            } else {
1619                fresh.unwrap();
1620            }
1621            assert!(
1622                access.check().is_err(),
1623                "a denied scoped stream must stay denied after {change}"
1624            );
1625        }
1626    }
1627
1628    #[tokio::test]
1629    async fn application_stream_access_preserves_renewal_but_fences_retirement() {
1630        use crate::client::ClientBuilder;
1631        for change in [
1632            "key",
1633            "generation",
1634            "scope",
1635            "revoke",
1636            "forget",
1637            "owner-drop",
1638        ] {
1639            let client =
1640                ClientBuilder::new_provider_neutral("stream-access-test".into(), Box::new(|| None))
1641                    .build();
1642            let registry = &client.session_token_registry;
1643            registry.register("old".into(), "room:test", 16);
1644            registry
1645                .validate_and_consume_for_connection("old", Some("peer"))
1646                .unwrap();
1647            client.set_connection_application_crypto_key("peer", [7; APPLICATION_KEY_BYTES]);
1648            let access = client
1649                .application_stream_access("peer", [7; APPLICATION_KEY_BYTES])
1650                .unwrap();
1651            access.check().unwrap();
1652            registry.register("renewed".into(), "room:test", 16);
1653            registry
1654                .validate_and_consume_for_connection("renewed", Some("peer"))
1655                .unwrap();
1656            registry.revoke("old");
1657            access
1658                .check()
1659                .expect("same-authorization renewal preserves an open stream");
1660            match change {
1661                "key" => {
1662                    client.set_connection_application_crypto_key("peer", [8; APPLICATION_KEY_BYTES])
1663                }
1664                "generation" => {
1665                    client
1666                        .connection_application_crypto_confirmed
1667                        .write()
1668                        .unwrap()
1669                        .insert(
1670                            "peer".into(),
1671                            super::ConnectionApplicationCryptoConfirmation {
1672                                transport_stable_id: 2,
1673                                confirmed: true,
1674                            },
1675                        );
1676                }
1677                "scope" => registry.mark_accepted(
1678                    "peer",
1679                    crate::session_token::AdmissionMechanism::SessionToken,
1680                    Some("room:other".into()),
1681                    None,
1682                ),
1683                "revoke" => {
1684                    registry.revoke("renewed");
1685                }
1686                "forget" => registry.forget_connection("peer"),
1687                "owner-drop" => drop(client),
1688                _ => unreachable!(),
1689            }
1690            assert_eq!(
1691                access.check().expect_err(change).kind(),
1692                std::io::ErrorKind::PermissionDenied
1693            );
1694        }
1695    }
1696
1697    #[test]
1698    fn required_application_streams_never_downgrade_to_plaintext() {
1699        let error = enforce_application_crypto_requirement(true, None, "test product stream")
1700            .expect_err("required crypto without a key must fail closed");
1701        assert!(error.to_string().contains("no installed key"));
1702
1703        let key = [7_u8; APPLICATION_KEY_BYTES];
1704        assert_eq!(
1705            enforce_application_crypto_requirement(true, Some(key), "test product stream")
1706                .expect("installed key should satisfy the requirement"),
1707            Some(key),
1708        );
1709        assert_eq!(
1710            enforce_application_crypto_requirement(false, None, "manual low-level stream")
1711                .expect("manual low-level plaintext remains an explicit opt-in"),
1712            None,
1713        );
1714    }
1715}
1716
1717pub(crate) fn new_connection_application_crypto_key_map(
1718) -> Arc<StdRwLock<HashMap<String, [u8; APPLICATION_KEY_BYTES]>>> {
1719    Arc::new(StdRwLock::new(HashMap::new()))
1720}
1721
1722pub(crate) fn new_connection_application_crypto_required_set() -> Arc<StdRwLock<HashSet<String>>> {
1723    Arc::new(StdRwLock::new(HashSet::new()))
1724}
1725
1726pub(crate) fn new_connection_application_crypto_confirmed_map(
1727) -> Arc<StdRwLock<HashMap<String, ConnectionApplicationCryptoConfirmation>>> {
1728    Arc::new(StdRwLock::new(HashMap::new()))
1729}
1730
1731pub(crate) fn new_connection_application_crypto_outbound_sequences(
1732) -> Arc<StdRwLock<HashMap<String, u64>>> {
1733    Arc::new(StdRwLock::new(HashMap::new()))
1734}
1735
1736pub(crate) fn new_connection_application_key_agreement_map(
1737) -> Arc<StdRwLock<HashMap<String, crate::key_agreement::KeyAgreement>>> {
1738    Arc::new(StdRwLock::new(HashMap::new()))
1739}