ezk_sdp_types/attributes/
crypto.rs

1use bytes::Bytes;
2use bytesstr::BytesStr;
3use internal::{ws, IResult};
4use nom::branch::alt;
5use nom::bytes::complete::{tag, take_while1};
6use nom::character::complete::{char, digit1};
7use nom::combinator::{map, map_res, not, opt, peek};
8use nom::error::context;
9use nom::multi::{separated_list0, separated_list1};
10use nom::sequence::{preceded, separated_pair, terminated, tuple};
11use std::fmt;
12
13/// Crypto attribte (for SRTP only) (`a=crypto`)
14///
15/// [RFC4568](https://www.rfc-editor.org/rfc/rfc4568)
16#[derive(Debug, Clone)]
17pub struct SrtpCrypto {
18    /// Unique identifier in a media description
19    pub tag: u32,
20
21    /// Crypto suite describing the encryption and authentication algorithm to use
22    pub suite: SrtpSuite,
23
24    /// One or more keys to use
25    pub keys: Vec<SrtpKeyingMaterial>,
26
27    /// Additional SRTP params
28    pub params: Vec<SrtpSessionParam>,
29}
30
31impl SrtpCrypto {
32    pub fn parse<'i>(src: &Bytes, i: &'i str) -> IResult<&'i str, Self> {
33        context(
34            "parsing srtp-crypto attribute",
35            map(
36                ws((
37                    // tag
38                    number,
39                    // suite
40                    SrtpSuite::parse(src),
41                    // keying material
42                    parse_srtp_key_params(src),
43                    // session params
44                    separated_list0(
45                        take_while1(char::is_whitespace),
46                        SrtpSessionParam::parse(src),
47                    ),
48                )),
49                |(tag, suite, keys, params)| Self {
50                    tag,
51                    suite,
52                    keys,
53                    params,
54                },
55            ),
56        )(i)
57    }
58}
59
60impl fmt::Display for SrtpCrypto {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{} {}", self.tag, self.suite)?;
63
64        if !self.keys.is_empty() {
65            write!(f, " ")?;
66        }
67
68        let mut keys = self.keys.iter().peekable();
69
70        while let Some(key) = keys.next() {
71            write!(f, "inline:{key}")?;
72
73            if keys.peek().is_some() {
74                write!(f, ";")?;
75            }
76        }
77
78        for param in &self.params {
79            write!(f, " {param}")?;
80        }
81
82        Ok(())
83    }
84}
85
86macro_rules! suite {
87    ($($suite:ident),* $(,)?) => {
88        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
89        #[allow(non_camel_case_types)]
90        pub enum SrtpSuite {
91            $($suite,)*
92            Ext(BytesStr),
93        }
94
95        impl SrtpSuite {
96            pub fn parse(src: &Bytes) -> impl Fn(&str) -> IResult<&str, Self> + '_ {
97                move |i| {
98                    context(
99                        "parsing srtp suite",
100                        alt((
101                            $(
102                            map(tag(stringify!($suite)), |_| Self::$suite),
103                            )*
104                            map(take_while1(is_alphanumeric_or_underscore), move |suite| {
105                                Self::Ext(BytesStr::from_parse(src, suite))
106                            }),
107                        )),
108                    )(i)
109                }
110            }
111
112            pub fn as_str(&self) -> &str {
113                match self {
114                    $(Self::$suite => stringify!($suite),)*
115                    Self::Ext(ext) => ext,
116                }
117            }
118        }
119
120        impl fmt::Display for SrtpSuite {
121            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122                write!(f, "{}", self.as_str())
123            }
124        }
125    };
126}
127
128suite! {
129    AES_CM_128_HMAC_SHA1_80,
130    AES_CM_128_HMAC_SHA1_32,
131    F8_128_HMAC_SHA1_80,
132    AES_192_CM_HMAC_SHA1_80,
133    AES_192_CM_HMAC_SHA1_32,
134    AES_256_CM_HMAC_SHA1_80,
135    AES_256_CM_HMAC_SHA1_32,
136    AEAD_AES_128_GCM,
137    AEAD_AES_256_GCM,
138}
139
140impl SrtpSuite {
141    pub fn key_and_salt_len(&self) -> Option<(usize, usize)> {
142        match self {
143            SrtpSuite::AES_CM_128_HMAC_SHA1_80
144            | SrtpSuite::AES_CM_128_HMAC_SHA1_32
145            | SrtpSuite::F8_128_HMAC_SHA1_80 => Some((16, 14)),
146            SrtpSuite::AES_192_CM_HMAC_SHA1_80 | SrtpSuite::AES_192_CM_HMAC_SHA1_32 => {
147                Some((24, 14))
148            }
149            SrtpSuite::AES_256_CM_HMAC_SHA1_80 | SrtpSuite::AES_256_CM_HMAC_SHA1_32 => {
150                Some((32, 14))
151            }
152            SrtpSuite::AEAD_AES_128_GCM => Some((16, 12)),
153            SrtpSuite::AEAD_AES_256_GCM => Some((32, 12)),
154            SrtpSuite::Ext(_) => None,
155        }
156    }
157}
158
159/// Parameters for an SRTP sessions
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum SrtpSessionParam {
162    /// The SRTP Key Derivation Rate is the rate at which a pseudo-random function is applied to a master key
163    Kdr(u32),
164    /// SRTP messages are not encrypted
165    UnencryptedSrtp,
166    /// SRTCP messages are not encrypted
167    UnencryptedSrtcp,
168    /// SRTP messages are not authenticated
169    UnauthenticatedSrtp,
170    //// Use forward error correction for the RTP packets
171    FecOrder(SrtpFecOrder),
172    /// Use separate master key(s) for a Forward Error Correction (FEC) stream
173    FecKey(Vec<SrtpKeyingMaterial>),
174    /// Window Size Hint
175    WindowSizeHint(u32),
176    /// Unknown parameter
177    Ext(BytesStr),
178}
179
180/// Order of forward error correction (FEC) relative to SRTP services
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum SrtpFecOrder {
183    /// FEC is applied before SRTP processing by the sender
184    /// FEC is applied after SRTP processing by the receiver
185    FecSrtp,
186
187    /// FEC is applied after SRTP processing by the sender
188    /// FEC is applied before SRTP processing by the receiver
189    SrtpFec,
190}
191
192impl SrtpSessionParam {
193    pub fn parse(src: &Bytes) -> impl Fn(&str) -> IResult<&str, Self> + '_ {
194        move |i| {
195            context(
196                "parsing srtp-session-param",
197                alt((
198                    map(preceded(tag("KDR="), number), Self::Kdr),
199                    map(tag("UNENCRYPTED_SRTP"), |_| Self::UnencryptedSrtp),
200                    map(tag("UNENCRYPTED_SRTCP"), |_| Self::UnencryptedSrtcp),
201                    map(tag("UNAUTHENTICATED_SRTP"), |_| Self::UnauthenticatedSrtp),
202                    preceded(
203                        tag("FEC_ORDER="),
204                        alt((
205                            map(tag("FEC_SRTP"), |_| Self::FecOrder(SrtpFecOrder::FecSrtp)),
206                            map(tag("SRTP_FEC"), |_| Self::FecOrder(SrtpFecOrder::SrtpFec)),
207                        )),
208                    ),
209                    map(
210                        preceded(tag("FEC_KEY="), parse_srtp_key_params(src)),
211                        Self::FecKey,
212                    ),
213                    map(preceded(tag("WSH="), number), Self::WindowSizeHint),
214                    map(
215                        preceded(peek(not(char('-'))), take_while1(is_visible_char)),
216                        |ext| Self::Ext(BytesStr::from_parse(src, ext)),
217                    ),
218                )),
219            )(i)
220        }
221    }
222}
223
224impl fmt::Display for SrtpSessionParam {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            SrtpSessionParam::Kdr(v) => write!(f, "KDR={v}"),
228            SrtpSessionParam::UnencryptedSrtp => write!(f, "UNENCRYPTED_SRTP"),
229            SrtpSessionParam::UnencryptedSrtcp => write!(f, "UNENCRYPTED_SRTCP"),
230            SrtpSessionParam::UnauthenticatedSrtp => write!(f, "UNAUTHENTICATED_SRTP"),
231            SrtpSessionParam::FecOrder(order) => {
232                let order = match order {
233                    SrtpFecOrder::FecSrtp => "FEC_SRTP",
234                    SrtpFecOrder::SrtpFec => "SRTP_FEC",
235                };
236
237                write!(f, "FEC_ORDER={order}")
238            }
239            SrtpSessionParam::FecKey(keys) => {
240                if keys.is_empty() {
241                    return Ok(());
242                }
243
244                write!(f, "FEC_KEY=")?;
245
246                let mut keys = keys.iter().peekable();
247
248                while let Some(key) = keys.next() {
249                    write!(f, "inline:{key}")?;
250
251                    if keys.peek().is_some() {
252                        write!(f, ";")?;
253                    }
254                }
255
256                Ok(())
257            }
258            SrtpSessionParam::WindowSizeHint(v) => write!(f, "WSH={v}"),
259            SrtpSessionParam::Ext(ext) => write!(f, "{ext}"),
260        }
261    }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
265pub struct SrtpKeyingMaterial {
266    /// Concatenated master key and salt, base64 encoded
267    pub key_and_salt: BytesStr,
268
269    /// Master key lifetime (max number of SRTP/SRTCP packets using this master key)
270    pub lifetime: Option<u32>,
271
272    /// Master key index and length of the MKI field in SRTP packets
273    pub mki: Option<(u32, u32)>,
274}
275
276impl SrtpKeyingMaterial {
277    pub fn parse(src: &Bytes) -> impl Fn(&str) -> IResult<&str, Self> + '_ {
278        move |i| {
279            context(
280                "parsing keying material",
281                map(
282                    tuple((
283                        // key and salt
284                        take_while1(is_base64_char),
285                        // lifetime
286                        opt(map(
287                            terminated(
288                                preceded(char('|'), tuple((opt(tag("2^")), number))),
289                                // Do not parse the mki here by mistake
290                                peek(not(char(':'))),
291                            ),
292                            |(exp, n)| {
293                                if exp.is_some() {
294                                    2u32.pow(n)
295                                } else {
296                                    n
297                                }
298                            },
299                        )),
300                        // mki
301                        opt(preceded(
302                            char('|'),
303                            separated_pair(number, char(':'), number),
304                        )),
305                    )),
306                    |(key_and_salt, lifetime, mki)| Self {
307                        key_and_salt: BytesStr::from_parse(src, key_and_salt),
308                        lifetime,
309                        mki,
310                    },
311                ),
312            )(i)
313        }
314    }
315}
316
317impl fmt::Display for SrtpKeyingMaterial {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "{}", self.key_and_salt)?;
320
321        if let Some(lifetime) = self.lifetime {
322            if lifetime.is_power_of_two() {
323                write!(f, "|2^{}", lifetime.trailing_zeros())?;
324            } else {
325                write!(f, "|{lifetime}")?;
326            }
327        }
328
329        if let Some((mki, mki_length)) = self.mki {
330            write!(f, "|{mki}:{mki_length}")?;
331        }
332
333        Ok(())
334    }
335}
336
337fn parse_srtp_key_params(
338    src: &Bytes,
339) -> impl FnMut(&str) -> IResult<&str, Vec<SrtpKeyingMaterial>> + '_ {
340    move |i| {
341        separated_list1(
342            char(';'),
343            preceded(tag("inline:"), SrtpKeyingMaterial::parse(src)),
344        )(i)
345    }
346}
347
348fn is_alphanumeric_or_underscore(c: char) -> bool {
349    matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_')
350}
351
352fn is_visible_char(c: char) -> bool {
353    matches!(c, '\u{21}'..='\u{7E}')
354}
355
356fn is_base64_char(c: char) -> bool {
357    c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=')
358}
359
360fn number(i: &str) -> IResult<&str, u32> {
361    context("parsing number", map_res(digit1, str::parse))(i)
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn srtp_session_param_kdr() {
370        let i = BytesStr::from_static("KDR=5");
371        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
372
373        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
374
375        let SrtpSessionParam::Kdr(5) = param else {
376            panic!("expected Kdr got {param:?}")
377        };
378
379        assert_eq!(param.to_string(), "KDR=5");
380    }
381
382    #[test]
383    fn srtp_session_param_unencrypted_srtp() {
384        let i = BytesStr::from_static("UNENCRYPTED_SRTP");
385        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
386
387        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
388
389        let SrtpSessionParam::UnencryptedSrtp = param else {
390            panic!("expected UnencryptedSrtp got {param:?}")
391        };
392
393        assert_eq!(param.to_string(), "UNENCRYPTED_SRTP");
394    }
395
396    #[test]
397    fn srtp_session_param_unencrypted_srtcp() {
398        let i = BytesStr::from_static("UNENCRYPTED_SRTCP");
399        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
400
401        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
402
403        let SrtpSessionParam::UnencryptedSrtcp = param else {
404            panic!("expected UnencryptedSrtcp got {param:?}")
405        };
406
407        assert_eq!(param.to_string(), "UNENCRYPTED_SRTCP");
408    }
409
410    #[test]
411    fn srtp_session_param_unauthenticated_srtp() {
412        let i = BytesStr::from_static("UNAUTHENTICATED_SRTP");
413        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
414
415        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
416
417        let SrtpSessionParam::UnauthenticatedSrtp = param else {
418            panic!("expected UnauthenticatedSrtp got {param:?}")
419        };
420
421        assert_eq!(param.to_string(), "UNAUTHENTICATED_SRTP");
422    }
423
424    #[test]
425    fn srtp_session_param_fec_order1() {
426        let i = BytesStr::from_static("FEC_ORDER=SRTP_FEC");
427        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
428
429        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
430
431        let SrtpSessionParam::FecOrder(SrtpFecOrder::SrtpFec) = param else {
432            panic!("expected FecOrder(SrtpFecOrder::SrtpFec) got {param:?}")
433        };
434
435        assert_eq!(param.to_string(), "FEC_ORDER=SRTP_FEC");
436    }
437
438    #[test]
439    fn srtp_session_param_fec_order2() {
440        let i = BytesStr::from_static("FEC_ORDER=FEC_SRTP");
441        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
442
443        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
444
445        let SrtpSessionParam::FecOrder(SrtpFecOrder::FecSrtp) = param else {
446            panic!("expected FecOrder(SrtpFecOrder::FecSrtp) got {param:?}")
447        };
448
449        assert_eq!(param.to_string(), "FEC_ORDER=FEC_SRTP");
450    }
451
452    #[test]
453    fn srtp_session_param_fec_key1() {
454        let i = BytesStr::from_static(
455            "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4",
456        );
457        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
458
459        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
460
461        let SrtpSessionParam::FecKey(key) = &param else {
462            panic!("expected FecKey(..) got {param:?}")
463        };
464
465        assert_eq!(key.len(), 1);
466
467        assert_eq!(
468            key[0].key_and_salt,
469            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"
470        );
471        assert_eq!(key[0].lifetime, Some(1048576));
472        assert_eq!(key[0].mki, Some((1, 4)));
473
474        assert_eq!(
475            param.to_string(),
476            "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4"
477        );
478    }
479
480    #[test]
481    fn srtp_session_param_fec_key2() {
482        let i = BytesStr::from_static(
483            "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2",
484        );
485        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
486
487        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
488
489        let SrtpSessionParam::FecKey(key) = &param else {
490            panic!("expected FecKey(..) got {param:?}")
491        };
492
493        assert_eq!(key.len(), 2);
494
495        assert_eq!(
496            key[0].key_and_salt,
497            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"
498        );
499        assert_eq!(key[0].lifetime, Some(1048576));
500        assert_eq!(key[0].mki, Some((1, 4)));
501
502        assert_eq!(
503            key[1].key_and_salt,
504            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"
505        );
506        assert_eq!(key[1].lifetime, Some(16384));
507        assert_eq!(key[1].mki, Some((1, 2)));
508
509        assert_eq!(
510            param.to_string(),
511            "FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2"
512        );
513    }
514
515    #[test]
516    fn srtp_session_param_window_size_hint() {
517        let i = BytesStr::from_static("WSH=5");
518        let (rem, param) = SrtpSessionParam::parse(i.as_ref())(&i).unwrap();
519
520        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
521
522        let SrtpSessionParam::WindowSizeHint(5) = param else {
523            panic!("expected WindowSizeHint got {param:?}")
524        };
525
526        assert_eq!(param.to_string(), "WSH=5");
527    }
528
529    #[test]
530    fn keying_material_missing_lifetime() {
531        let i = BytesStr::from_static("d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4");
532        let (rem, key) = SrtpKeyingMaterial::parse(i.as_ref())(&i).unwrap();
533
534        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
535
536        assert_eq!(key.key_and_salt, "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj");
537        assert_eq!(key.lifetime, None);
538        assert_eq!(key.mki, Some((1, 4)));
539
540        assert_eq!(
541            key.to_string(),
542            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4"
543        );
544    }
545
546    #[test]
547    fn keying_material_missing_mki() {
548        let i = BytesStr::from_static("d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^10");
549        let (rem, key) = SrtpKeyingMaterial::parse(i.as_ref())(&i).unwrap();
550
551        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
552
553        assert_eq!(key.key_and_salt, "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj");
554        assert_eq!(key.lifetime, Some(1024));
555        assert_eq!(key.mki, None);
556
557        assert_eq!(
558            key.to_string(),
559            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^10"
560        );
561    }
562
563    #[test]
564    fn keying_material_only_key_and_salt() {
565        let i = BytesStr::from_static("d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj");
566        let (rem, key) = SrtpKeyingMaterial::parse(i.as_ref())(&i).unwrap();
567
568        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
569
570        assert_eq!(key.key_and_salt, "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj");
571        assert_eq!(key.lifetime, None);
572        assert_eq!(key.mki, None);
573
574        assert_eq!(key.to_string(), "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj");
575    }
576
577    #[test]
578    fn parse_everything() {
579        let i = BytesStr::from_static(
580            "\
5811 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2 \
582KDR=100 \
583UNENCRYPTED_SRTP \
584UNENCRYPTED_SRTCP \
585UNAUTHENTICATED_SRTP \
586FEC_ORDER=FEC_SRTP \
587FEC_ORDER=SRTP_FEC \
588FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4 \
589WSH=123",
590        );
591        let (rem, c) = SrtpCrypto::parse(i.as_ref(), &i).unwrap();
592        assert!(rem.is_empty(), "rem is not empty: {rem:?}");
593
594        assert_eq!(c.tag, 1);
595        assert_eq!(c.suite, SrtpSuite::AES_CM_128_HMAC_SHA1_80);
596
597        assert_eq!(c.keys.len(), 2);
598
599        assert_eq!(
600            c.keys[0].key_and_salt,
601            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"
602        );
603        assert_eq!(c.keys[0].lifetime, Some(1048576));
604        assert_eq!(c.keys[0].mki, Some((1, 4)));
605
606        assert_eq!(
607            c.keys[1].key_and_salt,
608            "d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj"
609        );
610        assert_eq!(c.keys[1].lifetime, Some(16384));
611        assert_eq!(c.keys[1].mki, Some((1, 2)));
612
613        assert_eq!(c.params[0], SrtpSessionParam::Kdr(100));
614        assert_eq!(c.params[1], SrtpSessionParam::UnencryptedSrtp);
615        assert_eq!(c.params[2], SrtpSessionParam::UnencryptedSrtcp);
616        assert_eq!(c.params[3], SrtpSessionParam::UnauthenticatedSrtp);
617        assert_eq!(
618            c.params[4],
619            SrtpSessionParam::FecOrder(SrtpFecOrder::FecSrtp)
620        );
621        assert_eq!(
622            c.params[5],
623            SrtpSessionParam::FecOrder(SrtpFecOrder::SrtpFec)
624        );
625        assert_eq!(c.params[7], SrtpSessionParam::WindowSizeHint(123));
626
627        assert_eq!(c.to_string(), "1 AES_CM_128_HMAC_SHA1_80 inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:4;inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^14|1:2 KDR=100 UNENCRYPTED_SRTP UNENCRYPTED_SRTCP UNAUTHENTICATED_SRTP FEC_ORDER=FEC_SRTP FEC_ORDER=SRTP_FEC FEC_KEY=inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|1:4 WSH=123");
628    }
629}