Skip to main content

zerodds_security_rtps/
srtps.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Message-Level-Schutz: `SRTPS_PREFIX` + `SRTPS_POSTFIX`.
5//!
6//! Spec §7.3.7: wickelt eine **ganze** RTPS-Message (mit eingebetteten
7//! Submessages) in einen äußeren Schutz-Layer. Genutzt für
8//! `rtps_protection_kind=ENCRYPT` im Governance-XML.
9//!
10//! zerodds-lint: allow no_dyn_in_safe
11//! (Wie `codec.rs` nimmt der Wrapper `&dyn CryptographicPlugin`.)
12//!
13//! ```text
14//! [ RTPS-Header (20 byte, plaintext) ]
15//! [ SRTPS_PREFIX (4+16 byte) ]
16//! [ <encrypted body> ... ]
17//! [ SRTPS_POSTFIX (4+0 byte, leere MAC-Liste im Single-Receiver-Modus) ]
18//! ```
19//!
20//! Der RTPS-Header (ersten 20 byte) bleibt **plaintext**, damit
21//! Receiver die Magic "RTPS" + Version + VendorId + Prefix sehen
22//! können, ohne erst zu entschluesseln. Alles dahinter wird via
23//! AES-GCM verschluesselt + authentifiziert.
24
25use alloc::vec::Vec;
26
27use zerodds_security::crypto::{CryptoHandle, CryptographicPlugin};
28
29use crate::codec::{SRTPS_POSTFIX, SRTPS_PREFIX, SecurityRtpsError};
30
31/// RTPS-Header-Groesse (Spec §8.3.3.1).
32pub const RTPS_HEADER_LEN: usize = 20;
33
34const FLAG_LE: u8 = 0x01;
35
36/// `PreSharedKeyFlag` im SRTPS_PREFIX-Submessage-Header — Spec
37/// DDS-Security 1.2 §10.9.1.
38///
39/// Wenn gesetzt, signalisiert der Sender, dass die Master-Keys aus
40/// einem **Pre-Shared-Key** abgeleitet sind (DDS:Crypto:PSK:AES-GCM-
41/// GMAC:1.2) statt aus einem X.509-DH-Handshake. Decoder duerfen das
42/// Bit beobachten, um den korrekten Crypto-Plugin auszuwaehlen — auf
43/// dem Wire bleibt der Body-Layout (TransformIdentifier, Body, MACs)
44/// identisch, nur die Key-Herkunft unterscheidet sich.
45///
46/// Bit-Position 0x02 (Bit 1) — neben `FLAG_LE` (Bit 0). Andere
47/// Submessage-Flags (Bits 2..7) sind reserviert.
48pub const PRE_SHARED_KEY_FLAG: u8 = 0x02;
49
50fn push_header(out: &mut Vec<u8>, id: u8, length: u16) {
51    out.push(id);
52    out.push(FLAG_LE);
53    out.extend_from_slice(&length.to_le_bytes());
54}
55
56fn push_header_with_flags(out: &mut Vec<u8>, id: u8, flags: u8, length: u16) {
57    out.push(id);
58    out.push(flags);
59    out.extend_from_slice(&length.to_le_bytes());
60}
61
62/// Wie [`encode_secured_rtps_message`], aber setzt zusaetzlich den
63/// `PreSharedKeyFlag` im SRTPS_PREFIX (Spec §10.9.1) — fuer den
64/// PSK-Crypto-Pfad.
65///
66/// # Errors
67/// Wie [`encode_secured_rtps_message`].
68pub fn encode_secured_rtps_message_psk(
69    plugin: &dyn CryptographicPlugin,
70    local: CryptoHandle,
71    remote_list: &[CryptoHandle],
72    message: &[u8],
73) -> Result<Vec<u8>, SecurityRtpsError> {
74    if message.len() < RTPS_HEADER_LEN {
75        return Err(SecurityRtpsError::Truncated("rtps-message header"));
76    }
77    let (header, body) = message.split_at(RTPS_HEADER_LEN);
78
79    // AAD-Extension per DDS-Security 1.2 §7.4.6.6 (RTPS-Message-
80    // Protection): reserved-4 || RTPS-Header[0..20]. Schützt den
81    // Header gegen Tampering — der Plugin's `mat.aad(extension)`
82    // prependet zusätzlich `transformation_kind || key_id || session_id`.
83    let mut aad_extension = Vec::with_capacity(4 + RTPS_HEADER_LEN);
84    aad_extension.extend_from_slice(&[0u8; 4]);
85    aad_extension.extend_from_slice(header);
86
87    let ciphertext = plugin
88        .encrypt_submessage(local, remote_list, body, &aad_extension)
89        .map_err(SecurityRtpsError::Crypto)?;
90
91    let body_len = u16::try_from(ciphertext.len())
92        .map_err(|_| SecurityRtpsError::Truncated("SRTPS body > u16"))?;
93
94    let mut out = Vec::with_capacity(RTPS_HEADER_LEN + 4 + 16 + 4 + ciphertext.len() + 4);
95    out.extend_from_slice(header);
96
97    // SRTPS_PREFIX mit PSK-Flag.
98    push_header_with_flags(&mut out, SRTPS_PREFIX, FLAG_LE | PRE_SHARED_KEY_FLAG, 16);
99    out.extend_from_slice(&[0u8; 16]);
100
101    push_header(&mut out, crate::codec::SEC_BODY, body_len);
102    out.extend_from_slice(&ciphertext);
103
104    push_header(&mut out, SRTPS_POSTFIX, 0);
105
106    Ok(out)
107}
108
109/// Liest den `PreSharedKeyFlag`-Bit aus dem SRTPS_PREFIX einer
110/// secured RTPS-Message. Liefert `None` wenn die Wire kein gueltiges
111/// SRTPS-Wrapping ist.
112#[must_use]
113pub fn srtps_psk_flag(wire: &[u8]) -> Option<bool> {
114    if wire.len() < RTPS_HEADER_LEN + 4 {
115        return None;
116    }
117    if wire[RTPS_HEADER_LEN] != SRTPS_PREFIX {
118        return None;
119    }
120    Some(wire[RTPS_HEADER_LEN + 1] & PRE_SHARED_KEY_FLAG != 0)
121}
122
123/// Schuetzt eine **ganze** RTPS-Message. Die ersten 20 byte (Header)
124/// bleiben plaintext; alles dahinter (Submessage-Stream) wird
125/// verschluesselt + authentifiziert. Output:
126///
127/// ```text
128/// [ header (20) | SRTPS_PREFIX | encrypted body | SRTPS_POSTFIX ]
129/// ```
130///
131/// # Errors
132/// Input zu kurz fuer den Header oder Crypto-Plugin-Fehler.
133pub fn encode_secured_rtps_message(
134    plugin: &dyn CryptographicPlugin,
135    local: CryptoHandle,
136    remote_list: &[CryptoHandle],
137    message: &[u8],
138) -> Result<Vec<u8>, SecurityRtpsError> {
139    if message.len() < RTPS_HEADER_LEN {
140        return Err(SecurityRtpsError::Truncated("rtps-message header"));
141    }
142    let (header, body) = message.split_at(RTPS_HEADER_LEN);
143
144    // AAD-Extension per DDS-Security 1.2 §7.4.6.6 (RTPS-Message-
145    // Protection): reserved-4 || RTPS-Header[0..20]. Schützt den
146    // Header gegen Tampering — der Plugin's `mat.aad(extension)`
147    // prependet zusätzlich `transformation_kind || key_id || session_id`.
148    let mut aad_extension = Vec::with_capacity(4 + RTPS_HEADER_LEN);
149    aad_extension.extend_from_slice(&[0u8; 4]);
150    aad_extension.extend_from_slice(header);
151
152    let ciphertext = plugin
153        .encrypt_submessage(local, remote_list, body, &aad_extension)
154        .map_err(SecurityRtpsError::Crypto)?;
155
156    let body_len = u16::try_from(ciphertext.len())
157        .map_err(|_| SecurityRtpsError::Truncated("SRTPS body > u16"))?;
158
159    let mut out = Vec::with_capacity(
160        RTPS_HEADER_LEN    // plaintext header
161            + 4 + 16           // SRTPS_PREFIX
162            + 4 + ciphertext.len() // crypted body framed
163            + 4, // SRTPS_POSTFIX
164    );
165    out.extend_from_slice(header);
166
167    // SRTPS_PREFIX: 16-byte TransformIdentifier (0x00..0x00 = Single-Plugin-Pfad; Multi-Plugin-Identifier sind im DCPS-Runtime hand-allokiert).
168    push_header(&mut out, SRTPS_PREFIX, 16);
169    out.extend_from_slice(&[0u8; 16]);
170
171    // Cipherbody als einzelne Submessage-artige Struktur einhaengen.
172    // Hier setzen wir die CT-Bytes direkt — der Empfaenger weiss ueber
173    // den Submessage-Laengen-Header vom POSTFIX den Body-Umfang nicht,
174    // deshalb brauchen wir einen eigenen Length-Marker. Wir nutzen
175    // einen synthetischen SEC_BODY-Shape: [0x30 0x01 len_lo len_hi ...].
176    push_header(&mut out, crate::codec::SEC_BODY, body_len);
177    out.extend_from_slice(&ciphertext);
178
179    // SRTPS_POSTFIX leer — Single-Receiver-Modus.
180    push_header(&mut out, SRTPS_POSTFIX, 0);
181
182    Ok(out)
183}
184
185/// Unwrap eine ganze RTPS-Message. Erwartet das gleiche Format wie
186/// [`encode_secured_rtps_message`]. Liefert die rekonstruierte
187/// plaintext-Message (`[header | body]`).
188///
189/// # Errors
190/// Tampered Header, Wire-Inkonsistenz, Crypto-Verify-Fail.
191pub fn decode_secured_rtps_message(
192    plugin: &dyn CryptographicPlugin,
193    local: CryptoHandle,
194    remote: CryptoHandle,
195    wire: &[u8],
196) -> Result<Vec<u8>, SecurityRtpsError> {
197    if wire.len() < RTPS_HEADER_LEN {
198        return Err(SecurityRtpsError::Truncated("rtps-message header"));
199    }
200    let header = &wire[..RTPS_HEADER_LEN];
201    let rest = &wire[RTPS_HEADER_LEN..];
202
203    // SRTPS_PREFIX: 4 byte header + 16 byte body.
204    if rest.len() < 4 + 16 {
205        return Err(SecurityRtpsError::Truncated("SRTPS_PREFIX"));
206    }
207    if rest[0] != SRTPS_PREFIX {
208        return Err(SecurityRtpsError::UnexpectedSubmessageId {
209            pos: 0,
210            expected: SRTPS_PREFIX,
211            got: rest[0],
212        });
213    }
214    if rest[1] & FLAG_LE == 0 {
215        return Err(SecurityRtpsError::BigEndianNotSupported);
216    }
217    // prefix-body length (muss 16 sein, aber wir folgen blind dem
218    // Wert aus dem Header, damit zukuenftige Extensions stoeren).
219    let mut plen_b = [0u8; 2];
220    plen_b.copy_from_slice(&rest[2..4]);
221    let plen = u16::from_le_bytes(plen_b) as usize;
222    let after_prefix = 4 + plen;
223    if rest.len() < after_prefix {
224        return Err(SecurityRtpsError::Truncated("SRTPS_PREFIX body"));
225    }
226    let rest = &rest[after_prefix..];
227
228    // Body-Submessage (SEC_BODY-Kind).
229    if rest.len() < 4 {
230        return Err(SecurityRtpsError::Truncated("SRTPS body header"));
231    }
232    if rest[0] != crate::codec::SEC_BODY {
233        return Err(SecurityRtpsError::UnexpectedSubmessageId {
234            pos: 1,
235            expected: crate::codec::SEC_BODY,
236            got: rest[0],
237        });
238    }
239    if rest[1] & FLAG_LE == 0 {
240        return Err(SecurityRtpsError::BigEndianNotSupported);
241    }
242    let mut blen_b = [0u8; 2];
243    blen_b.copy_from_slice(&rest[2..4]);
244    let blen = u16::from_le_bytes(blen_b) as usize;
245    let after_body = 4 + blen;
246    if rest.len() < after_body {
247        return Err(SecurityRtpsError::Truncated("SRTPS body payload"));
248    }
249    let ciphertext = &rest[4..after_body];
250    let after_body_rest = &rest[after_body..];
251
252    // SRTPS_POSTFIX.
253    if after_body_rest.len() < 4 {
254        return Err(SecurityRtpsError::Truncated("SRTPS_POSTFIX"));
255    }
256    if after_body_rest[0] != SRTPS_POSTFIX {
257        return Err(SecurityRtpsError::UnexpectedSubmessageId {
258            pos: 2,
259            expected: SRTPS_POSTFIX,
260            got: after_body_rest[0],
261        });
262    }
263
264    // AAD-Extension symmetrisch zum Encoder.
265    let mut aad_extension = Vec::with_capacity(4 + RTPS_HEADER_LEN);
266    aad_extension.extend_from_slice(&[0u8; 4]);
267    aad_extension.extend_from_slice(header);
268
269    let plain_body = plugin
270        .decrypt_submessage(local, remote, ciphertext, &aad_extension)
271        .map_err(SecurityRtpsError::Crypto)?;
272
273    let mut out = Vec::with_capacity(RTPS_HEADER_LEN + plain_body.len());
274    out.extend_from_slice(header);
275    out.extend_from_slice(&plain_body);
276    Ok(out)
277}
278
279#[cfg(test)]
280#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
281mod tests {
282    use super::*;
283    use zerodds_security::authentication::{IdentityHandle, SharedSecretHandle};
284    use zerodds_security::error::SecurityErrorKind;
285    use zerodds_security_crypto::AesGcmCryptoPlugin;
286
287    fn make_plugin() -> (AesGcmCryptoPlugin, CryptoHandle, CryptoHandle) {
288        let mut p = AesGcmCryptoPlugin::new();
289        let local = p
290            .register_local_participant(IdentityHandle(1), &[])
291            .unwrap();
292        let remote = p
293            .register_matched_remote_participant(local, IdentityHandle(2), SharedSecretHandle(1))
294            .unwrap();
295        (p, local, remote)
296    }
297
298    fn fake_rtps_message(body: &[u8]) -> Vec<u8> {
299        // 20-byte header: "RTPS" + version(2) + vendor(2) + prefix(12)
300        let mut m = Vec::with_capacity(RTPS_HEADER_LEN + body.len());
301        m.extend_from_slice(b"RTPS\x02\x05\x01\x02");
302        m.extend_from_slice(&[0u8; 12]); // guid prefix
303        m.extend_from_slice(body);
304        m
305    }
306
307    #[test]
308    fn encode_keeps_header_in_plaintext() {
309        let (p, local, remote) = make_plugin();
310        let msg = fake_rtps_message(b"[DATA submessage plaintext]");
311        let wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
312        // Erste 4 byte = "RTPS" magic.
313        assert_eq!(&wire[..4], b"RTPS");
314        // Rest der 20 byte ist identisch zu msg[..20].
315        assert_eq!(&wire[..RTPS_HEADER_LEN], &msg[..RTPS_HEADER_LEN]);
316        // SRTPS_PREFIX folgt direkt.
317        assert_eq!(wire[RTPS_HEADER_LEN], SRTPS_PREFIX);
318    }
319
320    #[test]
321    fn encode_body_is_not_in_wire_plain() {
322        let (p, local, remote) = make_plugin();
323        let secret_body = b"TOP-SECRET submessage body";
324        let msg = fake_rtps_message(secret_body);
325        let wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
326        assert!(
327            !wire.windows(secret_body.len()).any(|w| w == secret_body),
328            "plaintext body muss verschluesselt sein"
329        );
330    }
331
332    #[test]
333    fn message_roundtrip_recovers_body() {
334        let (p, local, remote) = make_plugin();
335        let body = b"[HEARTBEAT][DATA][GAP]";
336        let msg = fake_rtps_message(body);
337        let wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
338        let back = decode_secured_rtps_message(&p, local, remote, &wire).unwrap();
339        assert_eq!(back, msg);
340    }
341
342    #[test]
343    fn message_too_short_rejected() {
344        let (p, local, remote) = make_plugin();
345        let err = encode_secured_rtps_message(&p, local, &[remote], &[0u8; 10]).unwrap_err();
346        assert!(matches!(err, SecurityRtpsError::Truncated(_)));
347    }
348
349    #[test]
350    fn tampered_ciphertext_fails_verify() {
351        let (p, local, remote) = make_plugin();
352        let msg = fake_rtps_message(b"secure submessage stream");
353        let mut wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
354        // Flip byte im ciphertext-Bereich (nach header + prefix + body-hdr).
355        let flip_idx = RTPS_HEADER_LEN + 4 + 16 + 4 + 12;
356        wire[flip_idx] ^= 0x10;
357        let err = decode_secured_rtps_message(&p, local, remote, &wire).unwrap_err();
358        match err {
359            SecurityRtpsError::Crypto(e) => {
360                assert_eq!(e.kind, SecurityErrorKind::CryptoFailed);
361            }
362            other => panic!("expected Crypto error, got {other:?}"),
363        }
364    }
365
366    #[test]
367    fn missing_srtps_prefix_rejected() {
368        let (p, local, remote) = make_plugin();
369        let msg = fake_rtps_message(b"x");
370        let mut wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
371        wire[RTPS_HEADER_LEN] = 0x15; // fake andere submessage
372        let err = decode_secured_rtps_message(&p, local, remote, &wire).unwrap_err();
373        assert!(matches!(
374            err,
375            SecurityRtpsError::UnexpectedSubmessageId {
376                pos: 0,
377                expected: SRTPS_PREFIX,
378                ..
379            }
380        ));
381    }
382
383    #[test]
384    fn psk_encode_sets_pre_shared_key_flag() {
385        let (p, local, remote) = make_plugin();
386        let msg = fake_rtps_message(b"psk-protected body");
387        let wire = encode_secured_rtps_message_psk(&p, local, &[remote], &msg).unwrap();
388        assert_eq!(wire[RTPS_HEADER_LEN], SRTPS_PREFIX);
389        // Flags-Byte traegt sowohl LE als auch PRE_SHARED_KEY_FLAG.
390        let flags = wire[RTPS_HEADER_LEN + 1];
391        assert!(flags & FLAG_LE != 0);
392        assert!(flags & PRE_SHARED_KEY_FLAG != 0);
393        assert_eq!(srtps_psk_flag(&wire), Some(true));
394    }
395
396    #[test]
397    fn non_psk_encode_does_not_set_pre_shared_key_flag() {
398        let (p, local, remote) = make_plugin();
399        let msg = fake_rtps_message(b"non-psk body");
400        let wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
401        assert_eq!(srtps_psk_flag(&wire), Some(false));
402    }
403
404    #[test]
405    fn psk_encoded_message_decodes_with_classic_decoder() {
406        // Spec §10.9: das Wire-Layout ist identisch — der PSK-Flag im
407        // SRTPS_PREFIX ist informativ. Der klassische Decoder darf den
408        // body trotzdem auspacken (das LE-Bit ist gesetzt).
409        let (p, local, remote) = make_plugin();
410        let msg = fake_rtps_message(b"interop-test");
411        let wire = encode_secured_rtps_message_psk(&p, local, &[remote], &msg).unwrap();
412        let back = decode_secured_rtps_message(&p, local, remote, &wire).unwrap();
413        assert_eq!(back, msg);
414    }
415
416    #[test]
417    fn srtps_psk_flag_returns_none_for_non_srtps() {
418        assert_eq!(srtps_psk_flag(&[]), None);
419        assert_eq!(srtps_psk_flag(&[0u8; 30]), None);
420    }
421
422    #[test]
423    fn big_endian_srtps_rejected() {
424        let (p, local, remote) = make_plugin();
425        let msg = fake_rtps_message(b"x");
426        let mut wire = encode_secured_rtps_message(&p, local, &[remote], &msg).unwrap();
427        // Flags-Byte beim SRTPS_PREFIX auf BE setzen.
428        wire[RTPS_HEADER_LEN + 1] = 0x00;
429        let err = decode_secured_rtps_message(&p, local, remote, &wire).unwrap_err();
430        assert!(matches!(err, SecurityRtpsError::BigEndianNotSupported));
431    }
432}