1use alloc::vec::Vec;
23
24use zerodds_security::crypto::{CryptoHandle, CryptographicPlugin, ReceiverMac};
25use zerodds_security::error::SecurityError;
26
27pub const SEC_PREFIX: u8 = 0x31;
29pub const SEC_POSTFIX: u8 = 0x32;
31pub const SEC_BODY: u8 = 0x30;
33pub const SRTPS_PREFIX: u8 = 0x33;
35pub const SRTPS_POSTFIX: u8 = 0x34;
37
38const FLAG_LE: u8 = 0x01;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum SecurityRtpsError {
44 Truncated(&'static str),
46 UnexpectedSubmessageId {
49 pos: usize,
51 expected: u8,
53 got: u8,
55 },
56 BigEndianNotSupported,
58 InconsistentLength,
61 Crypto(SecurityError),
63}
64
65impl core::fmt::Display for SecurityRtpsError {
66 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67 match self {
68 Self::Truncated(what) => write!(f, "secured submessage truncated at {what}"),
69 Self::UnexpectedSubmessageId { pos, expected, got } => write!(
70 f,
71 "secured submessage #{pos} id 0x{got:02x}, expected 0x{expected:02x}"
72 ),
73 Self::BigEndianNotSupported => write!(
74 f,
75 "big-endian SEC_* not supported (Single-Endianness-Pfad, LE per Default)"
76 ),
77 Self::InconsistentLength => write!(f, "SEC_BODY length header != payload"),
78 Self::Crypto(e) => write!(f, "crypto plugin: {e}"),
79 }
80 }
81}
82
83#[cfg(feature = "std")]
84impl std::error::Error for SecurityRtpsError {}
85
86impl From<SecurityError> for SecurityRtpsError {
87 fn from(e: SecurityError) -> Self {
88 Self::Crypto(e)
89 }
90}
91
92pub fn encode_secured_submessage(
101 plugin: &dyn CryptographicPlugin,
102 local: CryptoHandle,
103 remote_list: &[CryptoHandle],
104 plaintext: &[u8],
105) -> Result<Vec<u8>, SecurityRtpsError> {
106 let sec_prefix_body = [0u8; 16];
110
111 let mut aad_extension = Vec::with_capacity(4 + 16);
117 aad_extension.extend_from_slice(&[0u8; 4]); aad_extension.extend_from_slice(&sec_prefix_body);
119
120 let ciphertext = plugin.encrypt_submessage(local, remote_list, plaintext, &aad_extension)?;
122
123 let mut out = Vec::with_capacity(4 + 16 + 4 + 4 + ciphertext.len() + 4);
125 push_header(&mut out, SEC_PREFIX, 16);
126 out.extend_from_slice(&sec_prefix_body);
127
128 let ct_len = u32::try_from(ciphertext.len())
130 .map_err(|_| SecurityRtpsError::Truncated("ciphertext > u32"))?;
131 let body_len = u16::try_from(4 + ciphertext.len())
132 .map_err(|_| SecurityRtpsError::Truncated("SEC_BODY > u16"))?;
133 push_header(&mut out, SEC_BODY, body_len);
134 out.extend_from_slice(&ct_len.to_le_bytes());
135 out.extend_from_slice(&ciphertext);
136
137 push_header(&mut out, SEC_POSTFIX, 0);
140
141 Ok(out)
142}
143
144pub fn decode_secured_submessage(
151 plugin: &dyn CryptographicPlugin,
152 local: CryptoHandle,
153 remote: CryptoHandle,
154 secured_bytes: &[u8],
155) -> Result<Vec<u8>, SecurityRtpsError> {
156 let mut cur = Cursor::new(secured_bytes);
157
158 let (id, _flags, plen) = read_header(&mut cur, "SEC_PREFIX")?;
161 if id != SEC_PREFIX {
162 return Err(SecurityRtpsError::UnexpectedSubmessageId {
163 pos: 0,
164 expected: SEC_PREFIX,
165 got: id,
166 });
167 }
168 let sec_prefix_body = cur.read_bytes(plen as usize, "SEC_PREFIX body")?;
169 let mut aad_extension = Vec::with_capacity(4 + sec_prefix_body.len());
170 aad_extension.extend_from_slice(&[0u8; 4]);
171 aad_extension.extend_from_slice(sec_prefix_body);
172
173 let (id, _flags, blen) = read_header(&mut cur, "SEC_BODY header")?;
175 if id != SEC_BODY {
176 return Err(SecurityRtpsError::UnexpectedSubmessageId {
177 pos: 1,
178 expected: SEC_BODY,
179 got: id,
180 });
181 }
182 let ct_len_raw = cur.read_u32_le("SEC_BODY length")?;
183 if (ct_len_raw as usize) + 4 != (blen as usize) {
184 return Err(SecurityRtpsError::InconsistentLength);
185 }
186 let ciphertext = cur.read_bytes(ct_len_raw as usize, "SEC_BODY ciphertext")?;
187
188 let (id, _flags, postlen) = read_header(&mut cur, "SEC_POSTFIX")?;
190 if id != SEC_POSTFIX {
191 return Err(SecurityRtpsError::UnexpectedSubmessageId {
192 pos: 2,
193 expected: SEC_POSTFIX,
194 got: id,
195 });
196 }
197 cur.skip(postlen as usize, "SEC_POSTFIX body")?;
198
199 let plain = plugin.decrypt_submessage(local, remote, ciphertext, &aad_extension)?;
200 Ok(plain)
201}
202
203fn push_header(out: &mut Vec<u8>, id: u8, length: u16) {
204 out.push(id);
205 out.push(FLAG_LE);
206 out.extend_from_slice(&length.to_le_bytes());
207}
208
209pub const MAX_RECEIVER_MACS: usize = 256;
214
215pub fn encode_secured_submessage_multi(
232 plugin: &dyn CryptographicPlugin,
233 local: CryptoHandle,
234 receivers: &[(CryptoHandle, u32)],
235 plaintext: &[u8],
236) -> Result<Vec<u8>, SecurityRtpsError> {
237 let sec_prefix_body = [0u8; 16];
239 let mut aad_extension = Vec::with_capacity(4 + 16);
240 aad_extension.extend_from_slice(&[0u8; 4]);
241 aad_extension.extend_from_slice(&sec_prefix_body);
242
243 let (ciphertext, macs) =
244 plugin.encrypt_submessage_multi(local, receivers, plaintext, &aad_extension)?;
245 if macs.len() > MAX_RECEIVER_MACS {
246 return Err(SecurityRtpsError::Truncated(
247 "receiver-specific mac count exceeds cap",
248 ));
249 }
250
251 let postfix_body_len = 4usize.saturating_add(macs.len().saturating_mul(ReceiverMac::WIRE_SIZE));
252 let postfix_body_len_u16 = u16::try_from(postfix_body_len)
253 .map_err(|_| SecurityRtpsError::Truncated("SEC_POSTFIX > u16"))?;
254
255 let mut out = Vec::with_capacity(4 + 16 + 4 + 4 + ciphertext.len() + 4 + postfix_body_len);
256
257 push_header(&mut out, SEC_PREFIX, 16);
259 out.extend_from_slice(&sec_prefix_body);
260
261 let ct_len = u32::try_from(ciphertext.len())
263 .map_err(|_| SecurityRtpsError::Truncated("ciphertext > u32"))?;
264 let body_len = u16::try_from(4 + ciphertext.len())
265 .map_err(|_| SecurityRtpsError::Truncated("SEC_BODY > u16"))?;
266 push_header(&mut out, SEC_BODY, body_len);
267 out.extend_from_slice(&ct_len.to_le_bytes());
268 out.extend_from_slice(&ciphertext);
269
270 push_header(&mut out, SEC_POSTFIX, postfix_body_len_u16);
272 let n =
273 u32::try_from(macs.len()).map_err(|_| SecurityRtpsError::Truncated("mac count > u32"))?;
274 out.extend_from_slice(&n.to_le_bytes());
275 for m in &macs {
276 out.extend_from_slice(&m.key_id.to_le_bytes());
277 out.extend_from_slice(&m.mac);
278 }
279
280 Ok(out)
281}
282
283pub fn decode_secured_submessage_multi(
298 plugin: &dyn CryptographicPlugin,
299 local: CryptoHandle,
300 remote: CryptoHandle,
301 own_key_id: u32,
302 own_mac_key_handle: CryptoHandle,
303 secured_bytes: &[u8],
304) -> Result<Vec<u8>, SecurityRtpsError> {
305 let mut cur = Cursor::new(secured_bytes);
306
307 let (id, _flags, plen) = read_header(&mut cur, "SEC_PREFIX")?;
310 if id != SEC_PREFIX {
311 return Err(SecurityRtpsError::UnexpectedSubmessageId {
312 pos: 0,
313 expected: SEC_PREFIX,
314 got: id,
315 });
316 }
317 let sec_prefix_body = cur.read_bytes(plen as usize, "SEC_PREFIX body")?;
318 let mut aad_extension = Vec::with_capacity(4 + sec_prefix_body.len());
319 aad_extension.extend_from_slice(&[0u8; 4]);
320 aad_extension.extend_from_slice(sec_prefix_body);
321
322 let (id, _flags, blen) = read_header(&mut cur, "SEC_BODY header")?;
324 if id != SEC_BODY {
325 return Err(SecurityRtpsError::UnexpectedSubmessageId {
326 pos: 1,
327 expected: SEC_BODY,
328 got: id,
329 });
330 }
331 let ct_len_raw = cur.read_u32_le("SEC_BODY length")?;
332 if (ct_len_raw as usize) + 4 != (blen as usize) {
333 return Err(SecurityRtpsError::InconsistentLength);
334 }
335 let ciphertext = cur.read_bytes(ct_len_raw as usize, "SEC_BODY ciphertext")?;
336
337 let (id, _flags, postlen) = read_header(&mut cur, "SEC_POSTFIX")?;
339 if id != SEC_POSTFIX {
340 return Err(SecurityRtpsError::UnexpectedSubmessageId {
341 pos: 2,
342 expected: SEC_POSTFIX,
343 got: id,
344 });
345 }
346 let macs = if postlen == 0 {
347 Vec::new()
348 } else {
349 let count = cur.read_u32_le("SEC_POSTFIX mac count")? as usize;
350 if count > MAX_RECEIVER_MACS {
351 return Err(SecurityRtpsError::Truncated(
352 "SEC_POSTFIX mac count exceeds cap",
353 ));
354 }
355 let expected_body = 4usize.saturating_add(count.saturating_mul(ReceiverMac::WIRE_SIZE));
356 if expected_body != postlen as usize {
357 return Err(SecurityRtpsError::InconsistentLength);
358 }
359 let mut out = Vec::with_capacity(count);
360 for _ in 0..count {
361 let key_id = cur.read_u32_le("SEC_POSTFIX mac key_id")?;
362 let mac_bytes = cur.read_bytes(16, "SEC_POSTFIX mac body")?;
363 let mut mac = [0u8; 16];
364 mac.copy_from_slice(mac_bytes);
365 out.push(ReceiverMac { key_id, mac });
366 }
367 out
368 };
369
370 let plain = plugin.decrypt_submessage_with_receiver_mac(
371 local,
372 remote,
373 own_key_id,
374 own_mac_key_handle,
375 ciphertext,
376 &macs,
377 &aad_extension,
378 )?;
379 Ok(plain)
380}
381
382struct Cursor<'a> {
383 bytes: &'a [u8],
384 pos: usize,
385}
386
387impl<'a> Cursor<'a> {
388 fn new(bytes: &'a [u8]) -> Self {
389 Self { bytes, pos: 0 }
390 }
391
392 fn need(&self, n: usize, what: &'static str) -> Result<(), SecurityRtpsError> {
393 if self.pos + n > self.bytes.len() {
394 return Err(SecurityRtpsError::Truncated(what));
395 }
396 Ok(())
397 }
398
399 fn read_bytes(&mut self, n: usize, what: &'static str) -> Result<&'a [u8], SecurityRtpsError> {
400 self.need(n, what)?;
401 let out = &self.bytes[self.pos..self.pos + n];
402 self.pos += n;
403 Ok(out)
404 }
405
406 fn skip(&mut self, n: usize, what: &'static str) -> Result<(), SecurityRtpsError> {
407 self.need(n, what)?;
408 self.pos += n;
409 Ok(())
410 }
411
412 fn read_u32_le(&mut self, what: &'static str) -> Result<u32, SecurityRtpsError> {
413 self.need(4, what)?;
414 let mut b = [0u8; 4];
415 b.copy_from_slice(&self.bytes[self.pos..self.pos + 4]);
416 self.pos += 4;
417 Ok(u32::from_le_bytes(b))
418 }
419}
420
421fn read_header(
422 cur: &mut Cursor<'_>,
423 what: &'static str,
424) -> Result<(u8, u8, u16), SecurityRtpsError> {
425 cur.need(4, what)?;
426 let id = cur.bytes[cur.pos];
427 let flags = cur.bytes[cur.pos + 1];
428 if flags & FLAG_LE == 0 {
429 return Err(SecurityRtpsError::BigEndianNotSupported);
430 }
431 let mut l = [0u8; 2];
432 l.copy_from_slice(&cur.bytes[cur.pos + 2..cur.pos + 4]);
433 cur.pos += 4;
434 Ok((id, flags, u16::from_le_bytes(l)))
435}
436
437#[cfg(test)]
438#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
439mod tests {
440 use super::*;
441 use zerodds_security::authentication::{IdentityHandle, SharedSecretHandle};
442 use zerodds_security::error::SecurityErrorKind;
443 use zerodds_security_crypto::AesGcmCryptoPlugin;
444
445 fn make_plugin() -> (AesGcmCryptoPlugin, CryptoHandle, CryptoHandle) {
446 let mut p = AesGcmCryptoPlugin::new();
447 let local = p
448 .register_local_participant(IdentityHandle(1), &[])
449 .unwrap();
450 let remote = p
451 .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
452 .unwrap();
453 (p, local, remote)
454 }
455
456 #[test]
457 fn encode_produces_three_submessages() {
458 let (p, local, remote) = make_plugin();
459 let plain = b"plain-rtps-submessage-bytes";
460 let secured = encode_secured_submessage(&p, local, &[remote], plain).unwrap();
461 assert_eq!(secured[0], SEC_PREFIX);
463 assert!(secured.contains(&SEC_BODY));
465 assert!(secured.contains(&SEC_POSTFIX));
466 }
467
468 #[test]
469 fn roundtrip_matches_plaintext() {
470 let (p, local, remote) = make_plugin();
471 let plain = b"hello secure dds";
472 let secured = encode_secured_submessage(&p, local, &[remote], plain).unwrap();
473 let back = decode_secured_submessage(&p, local, remote, &secured).unwrap();
474 assert_eq!(back, plain);
475 }
476
477 #[test]
478 fn tampered_ciphertext_fails_verify() {
479 let (p, local, remote) = make_plugin();
480 let plain = b"0123456789abcdef";
481 let mut secured = encode_secured_submessage(&p, local, &[remote], plain).unwrap();
482
483 secured[30 + 12] ^= 0x10;
487
488 let err = decode_secured_submessage(&p, local, remote, &secured).unwrap_err();
489 match err {
490 SecurityRtpsError::Crypto(e) => assert_eq!(e.kind, SecurityErrorKind::CryptoFailed),
491 other => panic!("expected Crypto, got {other:?}"),
492 }
493 }
494
495 #[test]
496 fn wrong_prefix_id_rejected() {
497 let (p, local, remote) = make_plugin();
498 let mut secured = encode_secured_submessage(&p, local, &[remote], b"abc").unwrap();
499 secured[0] = 0x15; let err = decode_secured_submessage(&p, local, remote, &secured).unwrap_err();
501 assert!(matches!(
502 err,
503 SecurityRtpsError::UnexpectedSubmessageId {
504 pos: 0,
505 expected: SEC_PREFIX,
506 ..
507 }
508 ));
509 }
510
511 #[test]
512 fn big_endian_flag_rejected() {
513 let (p, local, remote) = make_plugin();
514 let mut secured = encode_secured_submessage(&p, local, &[remote], b"x").unwrap();
515 secured[1] = 0x00; let err = decode_secured_submessage(&p, local, remote, &secured).unwrap_err();
517 assert!(matches!(err, SecurityRtpsError::BigEndianNotSupported));
518 }
519
520 #[test]
521 fn truncated_input_rejected() {
522 let (p, local, remote) = make_plugin();
523 let err = decode_secured_submessage(&p, local, remote, &[SEC_PREFIX, 0x01]).unwrap_err();
524 assert!(matches!(err, SecurityRtpsError::Truncated(_)));
525 }
526
527 #[test]
528 fn constants_match_spec() {
529 assert_eq!(SEC_BODY, 0x30);
530 assert_eq!(SEC_PREFIX, 0x31);
531 assert_eq!(SEC_POSTFIX, 0x32);
532 assert_eq!(SRTPS_PREFIX, 0x33);
533 assert_eq!(SRTPS_POSTFIX, 0x34);
534 }
535
536 fn make_plugin_with_three_receivers() -> (
544 AesGcmCryptoPlugin,
545 CryptoHandle,
546 [CryptoHandle; 3],
547 [CryptoHandle; 3],
548 ) {
549 let mut p = AesGcmCryptoPlugin::new();
550 let sender = p
551 .register_local_participant(IdentityHandle(1), &[])
552 .unwrap();
553
554 let r1_sender = p.register_local_endpoint(sender, true, &[]).unwrap();
560 let r2_sender = p.register_local_endpoint(sender, true, &[]).unwrap();
561 let r3_sender = p.register_local_endpoint(sender, true, &[]).unwrap();
562
563 let t1 = p
568 .create_local_participant_crypto_tokens(r1_sender, CryptoHandle(0))
569 .unwrap();
570 let t2 = p
571 .create_local_participant_crypto_tokens(r2_sender, CryptoHandle(0))
572 .unwrap();
573 let t3 = p
574 .create_local_participant_crypto_tokens(r3_sender, CryptoHandle(0))
575 .unwrap();
576
577 let r1_recv = p
578 .register_matched_remote_participant(sender, IdentityHandle(2), SharedSecretHandle(1))
579 .unwrap();
580 let r2_recv = p
581 .register_matched_remote_participant(sender, IdentityHandle(3), SharedSecretHandle(2))
582 .unwrap();
583 let r3_recv = p
584 .register_matched_remote_participant(sender, IdentityHandle(4), SharedSecretHandle(3))
585 .unwrap();
586 p.set_remote_participant_crypto_tokens(sender, r1_recv, &t1)
587 .unwrap();
588 p.set_remote_participant_crypto_tokens(sender, r2_recv, &t2)
589 .unwrap();
590 p.set_remote_participant_crypto_tokens(sender, r3_recv, &t3)
591 .unwrap();
592
593 (
594 p,
595 sender,
596 [r1_sender, r2_sender, r3_sender],
597 [r1_recv, r2_recv, r3_recv],
598 )
599 }
600
601 fn bindings_with_ids(handles: &[CryptoHandle]) -> Vec<(CryptoHandle, u32)> {
602 handles
606 .iter()
607 .enumerate()
608 .map(|(i, h)| (*h, 1000u32 + (i as u32) + 1))
609 .collect()
610 }
611
612 #[test]
613 fn multi_mac_encode_produces_one_ciphertext_and_three_macs() {
614 let (p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
615 let receivers = bindings_with_ids(&r_sender);
616 let plain = b"hetero-broadcast-with-3-macs";
617 let wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
618
619 let ptr = wire.windows(1).position(|w| w[0] == SEC_POSTFIX);
621 assert!(ptr.is_some());
622 }
623
624 #[test]
625 fn multi_mac_roundtrip_each_receiver_validates_own_mac() {
626 let (p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
630 let receivers = bindings_with_ids(&r_sender);
631 let plain = b"multi-mac-dod";
632 let wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
633
634 for (idx, (handle, key_id)) in receivers.iter().enumerate() {
635 let back = decode_secured_submessage_multi(&p, sender, sender, *key_id, *handle, &wire)
636 .unwrap_or_else(|e| panic!("receiver {idx} must decode: {e:?}"));
637 assert_eq!(back, plain);
638 }
639 }
640
641 #[test]
642 fn multi_mac_reader_without_matching_key_id_rejects() {
643 let (mut p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
644 let receivers = bindings_with_ids(&r_sender);
645 let plain = b"rogue-attempt";
646 let wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
647
648 let foreign = p.register_local_endpoint(sender, true, &[]).unwrap();
651 let err =
652 decode_secured_submessage_multi(&p, sender, sender, 9999, foreign, &wire).unwrap_err();
653 match err {
654 SecurityRtpsError::Crypto(e) => assert_eq!(e.kind, SecurityErrorKind::CryptoFailed),
655 other => panic!("expected Crypto-Fail, got {other:?}"),
656 }
657 }
658
659 #[test]
660 fn multi_mac_tampered_ciphertext_fails_even_with_correct_key_id() {
661 let (p, sender, r_sender, _r_recv) = make_plugin_with_three_receivers();
662 let receivers = bindings_with_ids(&r_sender);
663 let plain = b"honest-plaintext";
664 let mut wire = encode_secured_submessage_multi(&p, sender, &receivers, plain).unwrap();
665
666 wire[32] ^= 0x20;
668
669 let (own_h, own_id) = receivers[0];
670 let err =
671 decode_secured_submessage_multi(&p, sender, sender, own_id, own_h, &wire).unwrap_err();
672 match err {
673 SecurityRtpsError::Crypto(e) => assert_eq!(e.kind, SecurityErrorKind::CryptoFailed),
674 other => panic!("expected Crypto-Fail, got {other:?}"),
675 }
676 }
677
678 #[test]
679 fn multi_mac_count_cap_enforced() {
680 let (p, sender, _r_sender, _r_recv) = make_plugin_with_three_receivers();
681 let ct = b"ciphertext-x"; let mut wire = Vec::new();
686 wire.push(SEC_PREFIX);
688 wire.push(FLAG_LE);
689 wire.extend_from_slice(&16u16.to_le_bytes());
690 wire.extend_from_slice(&[0u8; 16]);
691 wire.push(SEC_BODY);
693 wire.push(FLAG_LE);
694 let body_len = 4 + ct.len() as u16;
695 wire.extend_from_slice(&body_len.to_le_bytes());
696 wire.extend_from_slice(&(ct.len() as u32).to_le_bytes());
697 wire.extend_from_slice(ct);
698 wire.push(SEC_POSTFIX);
700 wire.push(FLAG_LE);
701 let bad_body_len = 4u16 + ((MAX_RECEIVER_MACS as u16 + 1) * 20);
702 wire.extend_from_slice(&bad_body_len.to_le_bytes());
703 wire.extend_from_slice(&((MAX_RECEIVER_MACS as u32) + 1).to_le_bytes());
704 let err =
708 decode_secured_submessage_multi(&p, sender, sender, 0, sender, &wire).unwrap_err();
709 assert!(matches!(err, SecurityRtpsError::Truncated(_)));
710 }
711
712 #[test]
713 fn multi_mac_empty_mac_list_falls_back_to_normal_decrypt() {
714 let (p, sender, _, _) = make_plugin_with_three_receivers();
718 let plain = b"legacy-encoded-path";
719 let wire = encode_secured_submessage(&p, sender, &[sender], plain).unwrap();
720 let back = decode_secured_submessage_multi(&p, sender, sender, 0, sender, &wire).unwrap();
721 assert_eq!(back, plain);
722 }
723}