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 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 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 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 #[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 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 #[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 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 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 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 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 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 #[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 #[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 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 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 #[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 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 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 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}