Skip to main content

srt_runtime/packet/
key_material.rs

1//! Key Material message — `draft-sharabayko-srt-01` §3.2.2, Figures 10-11.
2//!
3//! Carried either as a Handshake Extension (§3.2.1.2, `Extension Type`
4//! `SRT_CMD_KMREQ`/`SRT_CMD_KMRSP`) or as the CIF of a User-Defined control
5//! packet (`Subtype` `SRT_CMD_KMREQ`/`SRT_CMD_KMRSP`, §3.2.2).
6//!
7//! ```text
8//! word0   S(1) V(3) PT(4) | Sign(16) | Resv1(6) KK(2)
9//! word1   KEKI(32)
10//! word2   Cipher(8) Auth(8) SE(8) Resv2(8)
11//! word3   Resv3(16) SLen/4(8) KLen/4(8)
12//! ..      Salt (SLen bytes)
13//! ..      ICV (8 bytes) | xSEK (KLen bytes) | [oSEK (KLen bytes)]
14//! ```
15//!
16//! `S`, `V`, `PT`, `Sign`, `Resv1`, `Resv2`, `Resv3` are fixed-value fields
17//! (the spec gives each a mandated `value = {..}`); they are validated on
18//! parse and not stored (matching the crate's reserved-bit policy — see the
19//! crate root docs), except `PT`, which acts as this struct's discriminating
20//! magic number (must be `2`, "Keying Material Message").
21//!
22//! This module only carries the wrapped-key *bytes* — it performs no AES
23//! key-wrap/unwrap. Actual encryption/decryption is an explicit follow-up
24//! (see the crate root docs).
25
26use super::{Error, Result, be32, put_be32};
27
28const S_FIXED: u8 = 0;
29const V_FIXED: u8 = 1;
30const PT_KEYING_MATERIAL: u8 = 2;
31const SIGN_FIXED: u16 = 0x2029; // 'HAI' PnP Vendor ID, big-endian.
32const RESV1_FIXED: u8 = 0;
33const RESV2_FIXED: u8 = 0;
34const RESV3_FIXED: u16 = 0;
35
36/// `KK` wire values of the Key Material message (§3.2.2). Distinct from the
37/// data-packet `KK` field ([`super::EncryptionKeyField`]) — same 2-bit shape,
38/// different meaning.
39pub const KM_KK_NO_SEK: u8 = 0b00;
40/// Even key provided.
41pub const KM_KK_EVEN: u8 = 0b01;
42/// Odd key provided.
43pub const KM_KK_ODD: u8 = 0b10;
44/// Both keys provided.
45pub const KM_KK_BOTH: u8 = 0b11;
46
47/// `KK`: which SEK(s) (odd/even) this Key Material message provides
48/// (`draft-sharabayko-srt-01` §3.2.2).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize))]
51#[non_exhaustive]
52pub enum KmKeyFlag {
53    /// `00b`: no SEK provided (spec: "invalid extension format").
54    NoSek,
55    /// `01b`: even key provided.
56    Even,
57    /// `10b`: odd key provided.
58    Odd,
59    /// `11b`: both even and odd keys provided.
60    Both,
61}
62
63impl KmKeyFlag {
64    /// Decode the 2-bit `KK` field.
65    pub fn from_bits(v: u8) -> Self {
66        match v & 0b11 {
67            KM_KK_EVEN => KmKeyFlag::Even,
68            KM_KK_ODD => KmKeyFlag::Odd,
69            KM_KK_BOTH => KmKeyFlag::Both,
70            _ => KmKeyFlag::NoSek,
71        }
72    }
73
74    /// The 2-bit wire value.
75    pub fn to_bits(self) -> u8 {
76        match self {
77            KmKeyFlag::NoSek => KM_KK_NO_SEK,
78            KmKeyFlag::Even => KM_KK_EVEN,
79            KmKeyFlag::Odd => KM_KK_ODD,
80            KmKeyFlag::Both => KM_KK_BOTH,
81        }
82    }
83
84    /// Number of SEKs this flag indicates (`n` in the Wrap-field length
85    /// formula, §3.2.2: `n = (KK + 1) / 2`).
86    pub fn key_count(self) -> u8 {
87        match self {
88            KmKeyFlag::NoSek => 0,
89            KmKeyFlag::Even | KmKeyFlag::Odd => 1,
90            KmKeyFlag::Both => 2,
91        }
92    }
93
94    /// Spec label.
95    pub fn name(&self) -> &'static str {
96        match self {
97            KmKeyFlag::NoSek => "no SEK",
98            KmKeyFlag::Even => "even key",
99            KmKeyFlag::Odd => "odd key",
100            KmKeyFlag::Both => "both keys",
101        }
102    }
103}
104
105broadcast_common::impl_spec_display!(KmKeyFlag);
106
107/// `Cipher` wire values (§3.2.2).
108pub const CIPHER_NONE: u8 = 0;
109/// AES-CTR.
110pub const CIPHER_AES_CTR: u8 = 2;
111
112/// `Cipher`: encryption cipher and mode (`draft-sharabayko-srt-01` §3.2.2).
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize))]
115#[non_exhaustive]
116pub enum Cipher {
117    /// `0`: none, or a KEKI-indexed crypto context.
118    None,
119    /// `2`: AES-CTR (SP800-38A).
120    AesCtr,
121    /// A value not defined above (includes `1`).
122    Reserved(u8),
123}
124
125impl Cipher {
126    /// Decode the 8-bit `Cipher` field.
127    pub fn from_bits(v: u8) -> Self {
128        match v {
129            CIPHER_NONE => Cipher::None,
130            CIPHER_AES_CTR => Cipher::AesCtr,
131            other => Cipher::Reserved(other),
132        }
133    }
134
135    /// The wire value.
136    pub fn to_bits(self) -> u8 {
137        match self {
138            Cipher::None => CIPHER_NONE,
139            Cipher::AesCtr => CIPHER_AES_CTR,
140            Cipher::Reserved(v) => v,
141        }
142    }
143
144    /// Spec label.
145    pub fn name(&self) -> &'static str {
146        match self {
147            Cipher::None => "none / KEKI-indexed",
148            Cipher::AesCtr => "AES-CTR",
149            Cipher::Reserved(_) => "reserved",
150        }
151    }
152}
153
154broadcast_common::impl_spec_display!(Cipher, Reserved);
155
156/// `Authentication` wire values (§3.2.2).
157pub const KM_AUTH_NONE: u8 = 0;
158
159/// `Authentication`: message authentication code algorithm
160/// (`draft-sharabayko-srt-01` §3.2.2).
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize))]
163#[non_exhaustive]
164pub enum KmAuth {
165    /// `0`: none, or a KEKI-indexed crypto context (the only defined value).
166    None,
167    /// A value not defined above.
168    Reserved(u8),
169}
170
171impl KmAuth {
172    /// Decode the 8-bit `Auth` field.
173    pub fn from_bits(v: u8) -> Self {
174        match v {
175            KM_AUTH_NONE => KmAuth::None,
176            other => KmAuth::Reserved(other),
177        }
178    }
179
180    /// The wire value.
181    pub fn to_bits(self) -> u8 {
182        match self {
183            KmAuth::None => KM_AUTH_NONE,
184            KmAuth::Reserved(v) => v,
185        }
186    }
187
188    /// Spec label.
189    pub fn name(&self) -> &'static str {
190        match self {
191            KmAuth::None => "none / KEKI-indexed",
192            KmAuth::Reserved(_) => "reserved",
193        }
194    }
195}
196
197broadcast_common::impl_spec_display!(KmAuth, Reserved);
198
199/// `SE` (Stream Encapsulation) wire values (§3.2.2).
200pub const STREAM_ENCAP_UNSPECIFIED: u8 = 0;
201/// MPEG-TS/UDP.
202pub const STREAM_ENCAP_MPEG_TS_UDP: u8 = 1;
203/// MPEG-TS/SRT.
204pub const STREAM_ENCAP_MPEG_TS_SRT: u8 = 2;
205
206/// `SE`: stream encapsulation (`draft-sharabayko-srt-01` §3.2.2).
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize))]
209#[non_exhaustive]
210pub enum StreamEncapsulation {
211    /// `0`: unspecified, or a KEKI-indexed crypto context.
212    Unspecified,
213    /// `1`: MPEG-TS/UDP.
214    MpegTsUdp,
215    /// `2`: MPEG-TS/SRT.
216    MpegTsSrt,
217    /// A value not defined above.
218    Reserved(u8),
219}
220
221impl StreamEncapsulation {
222    /// Decode the 8-bit `SE` field.
223    pub fn from_bits(v: u8) -> Self {
224        match v {
225            STREAM_ENCAP_UNSPECIFIED => StreamEncapsulation::Unspecified,
226            STREAM_ENCAP_MPEG_TS_UDP => StreamEncapsulation::MpegTsUdp,
227            STREAM_ENCAP_MPEG_TS_SRT => StreamEncapsulation::MpegTsSrt,
228            other => StreamEncapsulation::Reserved(other),
229        }
230    }
231
232    /// The wire value.
233    pub fn to_bits(self) -> u8 {
234        match self {
235            StreamEncapsulation::Unspecified => STREAM_ENCAP_UNSPECIFIED,
236            StreamEncapsulation::MpegTsUdp => STREAM_ENCAP_MPEG_TS_UDP,
237            StreamEncapsulation::MpegTsSrt => STREAM_ENCAP_MPEG_TS_SRT,
238            StreamEncapsulation::Reserved(v) => v,
239        }
240    }
241
242    /// Spec label.
243    pub fn name(&self) -> &'static str {
244        match self {
245            StreamEncapsulation::Unspecified => "unspecified / KEKI-indexed",
246            StreamEncapsulation::MpegTsUdp => "MPEG-TS/UDP",
247            StreamEncapsulation::MpegTsSrt => "MPEG-TS/SRT",
248            StreamEncapsulation::Reserved(_) => "reserved",
249        }
250    }
251}
252
253broadcast_common::impl_spec_display!(StreamEncapsulation, Reserved);
254
255/// Key Material message (`draft-sharabayko-srt-01` §3.2.2, Figures 10-11).
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize))]
258pub struct KeyMaterial<'a> {
259    /// Which SEK(s) [`Self::x_sek`] / [`Self::o_sek`] carry.
260    pub kk: KmKeyFlag,
261    /// Key Encryption Key Index (big-endian; `0` = default stream key).
262    pub keki: u32,
263    /// Encryption cipher and mode.
264    pub cipher: Cipher,
265    /// Message authentication code algorithm.
266    pub auth: KmAuth,
267    /// Stream encapsulation.
268    pub se: StreamEncapsulation,
269    /// Salt / IV (`SLen` bytes; `0` if absent, else 16 bytes / 128 bits per
270    /// the only length the spec defines).
271    pub salt: &'a [u8],
272    /// 64-bit AES key-wrap Integrity Check Vector.
273    pub icv: [u8; 8],
274    /// The (even or odd, per [`Self::kk`]) SEK, wrapped. `KLen` bytes
275    /// (16/24/32, matching the handshake's `Encryption Field`).
276    pub x_sek: &'a [u8],
277    /// The odd SEK, wrapped, present only when [`Self::kk`] is
278    /// [`KmKeyFlag::Both`] (same length as [`Self::x_sek`]).
279    pub o_sek: Option<&'a [u8]>,
280}
281
282impl<'a> KeyMaterial<'a> {
283    /// Parse a Key Material message from `bytes` (exactly the message —
284    /// either a handshake extension's contents or a User-Defined control
285    /// packet's CIF).
286    pub fn parse(bytes: &'a [u8]) -> Result<Self> {
287        if bytes.len() < 16 {
288            return Err(Error::BufferTooShort {
289                need: 16,
290                have: bytes.len(),
291                what: "key material fixed header",
292            });
293        }
294        let word0 = be32(bytes, 0);
295        let s = (word0 >> 31) as u8;
296        let v = ((word0 >> 28) & 0b111) as u8;
297        let pt = ((word0 >> 24) & 0b1111) as u8;
298        let sign = ((word0 >> 8) & 0xFFFF) as u16;
299        let resv1 = ((word0 >> 2) & 0b0011_1111) as u8;
300        let kk_bits = (word0 & 0b11) as u8;
301
302        if s != S_FIXED {
303            return Err(Error::InvalidKeyMaterial {
304                field: "S",
305                reason: "must be 0",
306            });
307        }
308        if v != V_FIXED {
309            return Err(Error::InvalidKeyMaterial {
310                field: "V",
311                reason: "must be 1",
312            });
313        }
314        if pt != PT_KEYING_MATERIAL {
315            return Err(Error::InvalidKeyMaterial {
316                field: "PT",
317                reason: "must be 2 (Keying Material Message)",
318            });
319        }
320        if sign != SIGN_FIXED {
321            return Err(Error::InvalidKeyMaterial {
322                field: "Sign",
323                reason: "must be 0x2029 ('HAI' PnP Vendor ID)",
324            });
325        }
326        if resv1 != RESV1_FIXED {
327            return Err(Error::InvalidKeyMaterial {
328                field: "Resv1",
329                reason: "must be 0",
330            });
331        }
332        let kk = KmKeyFlag::from_bits(kk_bits);
333
334        let keki = be32(bytes, 4);
335
336        let word2 = be32(bytes, 8);
337        let cipher = Cipher::from_bits((word2 >> 24) as u8);
338        let auth = KmAuth::from_bits((word2 >> 16) as u8);
339        let se = StreamEncapsulation::from_bits((word2 >> 8) as u8);
340        let resv2 = (word2 & 0xFF) as u8;
341        if resv2 != RESV2_FIXED {
342            return Err(Error::InvalidKeyMaterial {
343                field: "Resv2",
344                reason: "must be 0",
345            });
346        }
347
348        let word3 = be32(bytes, 12);
349        let resv3 = (word3 >> 16) as u16;
350        let slen4 = ((word3 >> 8) & 0xFF) as usize;
351        let klen4 = (word3 & 0xFF) as usize;
352        if resv3 != RESV3_FIXED {
353            return Err(Error::InvalidKeyMaterial {
354                field: "Resv3",
355                reason: "must be 0",
356            });
357        }
358
359        let slen = slen4 * 4;
360        let klen = klen4 * 4;
361        if !matches!(klen, 16 | 24 | 32) {
362            return Err(Error::InvalidKeyMaterial {
363                field: "KLen",
364                reason: "must be 16, 24, or 32 bytes (AES-128/192/256)",
365            });
366        }
367
368        let mut offset = 16usize;
369        let salt = bytes
370            .get(offset..offset + slen)
371            .ok_or(Error::BufferTooShort {
372                need: offset + slen,
373                have: bytes.len(),
374                what: "key material salt",
375            })?;
376        offset += slen;
377
378        let n = usize::from(kk.key_count());
379        let wrap_len = 8 + n * klen;
380        let wrap = bytes
381            .get(offset..offset + wrap_len)
382            .ok_or(Error::BufferTooShort {
383                need: offset + wrap_len,
384                have: bytes.len(),
385                what: "key material wrap field",
386            })?;
387        offset += wrap_len;
388
389        if offset != bytes.len() {
390            return Err(Error::UnexpectedTrailingBytes {
391                what: "key material message",
392                extra: bytes.len() - offset,
393            });
394        }
395
396        let mut icv = [0u8; 8];
397        icv.copy_from_slice(&wrap[0..8]);
398        let (x_sek, o_sek) = if n >= 1 {
399            let x = &wrap[8..8 + klen];
400            let o = if n == 2 {
401                Some(&wrap[8 + klen..8 + 2 * klen])
402            } else {
403                None
404            };
405            (x, o)
406        } else {
407            (&wrap[8..8], None)
408        };
409
410        Ok(KeyMaterial {
411            kk,
412            keki,
413            cipher,
414            auth,
415            se,
416            salt,
417            icv,
418            x_sek,
419            o_sek,
420        })
421    }
422
423    /// Number of bytes [`Self::serialize_into`] will write.
424    pub fn serialized_len(&self) -> usize {
425        16 + self.salt.len() + 8 + self.x_sek.len() + self.o_sek.map_or(0, <[u8]>::len)
426    }
427
428    /// Serialize this Key Material message into `buf`.
429    pub fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
430        let len = self.serialized_len();
431        if buf.len() < len {
432            return Err(Error::OutputBufferTooSmall {
433                need: len,
434                have: buf.len(),
435            });
436        }
437        if !self.salt.len().is_multiple_of(4) {
438            return Err(Error::InvalidKeyMaterial {
439                field: "Salt",
440                reason: "length must be a whole number of 4-byte words",
441            });
442        }
443        if !matches!(self.x_sek.len(), 16 | 24 | 32) {
444            return Err(Error::InvalidKeyMaterial {
445                field: "xSEK",
446                reason: "length must be 16, 24, or 32 bytes",
447            });
448        }
449        let expects_both = self.kk == KmKeyFlag::Both;
450        match (&self.o_sek, expects_both) {
451            (Some(o), true) if o.len() == self.x_sek.len() => {}
452            (None, false) => {}
453            _ => {
454                return Err(Error::InvalidKeyMaterial {
455                    field: "KK/oSEK",
456                    reason: "oSEK must be present with the same length as xSEK iff KK is Both",
457                });
458            }
459        }
460
461        let word0 = (u32::from(S_FIXED) << 31)
462            | (u32::from(V_FIXED) << 28)
463            | (u32::from(PT_KEYING_MATERIAL) << 24)
464            | (u32::from(SIGN_FIXED) << 8)
465            | (u32::from(RESV1_FIXED) << 2)
466            | u32::from(self.kk.to_bits());
467        put_be32(buf, 0, word0);
468        put_be32(buf, 4, self.keki);
469        let word2 = (u32::from(self.cipher.to_bits()) << 24)
470            | (u32::from(self.auth.to_bits()) << 16)
471            | (u32::from(self.se.to_bits()) << 8)
472            | u32::from(RESV2_FIXED);
473        put_be32(buf, 8, word2);
474        let slen4 = (self.salt.len() / 4) as u32;
475        let klen4 = (self.x_sek.len() / 4) as u32;
476        let word3 = (u32::from(RESV3_FIXED) << 16) | (slen4 << 8) | klen4;
477        put_be32(buf, 12, word3);
478
479        let mut off = 16;
480        buf[off..off + self.salt.len()].copy_from_slice(self.salt);
481        off += self.salt.len();
482        buf[off..off + 8].copy_from_slice(&self.icv);
483        off += 8;
484        buf[off..off + self.x_sek.len()].copy_from_slice(self.x_sek);
485        off += self.x_sek.len();
486        if let Some(o) = self.o_sek {
487            buf[off..off + o.len()].copy_from_slice(o);
488            off += o.len();
489        }
490        debug_assert_eq!(off, len);
491        Ok(len)
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    fn sample_even_only() -> KeyMaterial<'static> {
500        KeyMaterial {
501            kk: KmKeyFlag::Even,
502            keki: 0,
503            cipher: Cipher::AesCtr,
504            auth: KmAuth::None,
505            se: StreamEncapsulation::MpegTsSrt,
506            salt: &[0xAA; 16],
507            icv: [1, 2, 3, 4, 5, 6, 7, 8],
508            x_sek: &[0xEE; 16],
509            o_sek: None,
510        }
511    }
512
513    #[test]
514    fn round_trips_hand_computed_bytes() {
515        let km = sample_even_only();
516        let mut buf = alloc::vec![0u8; km.serialized_len()];
517        let n = km.serialize_into(&mut buf).unwrap();
518        assert_eq!(n, 16 + 16 + 8 + 16);
519
520        // word0: S=0,V=1,PT=2,Sign=0x2029,Resv1=0,KK=01
521        let expected_word0 = (1u32 << 28) | (2u32 << 24) | (0x2029u32 << 8) | 0b01u32;
522        assert_eq!(&buf[0..4], &expected_word0.to_be_bytes());
523        // word3: Resv3=0, SLen/4=4, KLen/4=4
524        assert_eq!(&buf[12..16], &[0, 0, 4, 4]);
525        assert_eq!(&buf[16..32], &[0xAAu8; 16][..]);
526        assert_eq!(&buf[32..40], &[1, 2, 3, 4, 5, 6, 7, 8]);
527        assert_eq!(&buf[40..56], &[0xEEu8; 16][..]);
528
529        assert_eq!(KeyMaterial::parse(&buf).unwrap(), km);
530    }
531
532    #[test]
533    fn both_keys_round_trip() {
534        let km = KeyMaterial {
535            kk: KmKeyFlag::Both,
536            keki: 7,
537            cipher: Cipher::AesCtr,
538            auth: KmAuth::None,
539            se: StreamEncapsulation::MpegTsSrt,
540            salt: &[0; 16],
541            icv: [0; 8],
542            x_sek: &[1; 24],
543            o_sek: Some(&[2; 24]),
544        };
545        let mut buf = alloc::vec![0u8; km.serialized_len()];
546        km.serialize_into(&mut buf).unwrap();
547        assert_eq!(KeyMaterial::parse(&buf).unwrap(), km);
548    }
549
550    #[test]
551    fn no_salt_round_trips() {
552        let km = KeyMaterial {
553            kk: KmKeyFlag::Odd,
554            keki: 0,
555            cipher: Cipher::None,
556            auth: KmAuth::None,
557            se: StreamEncapsulation::Unspecified,
558            salt: &[],
559            icv: [9; 8],
560            x_sek: &[3; 32],
561            o_sek: None,
562        };
563        let mut buf = alloc::vec![0u8; km.serialized_len()];
564        km.serialize_into(&mut buf).unwrap();
565        assert_eq!(KeyMaterial::parse(&buf).unwrap(), km);
566    }
567
568    #[test]
569    fn bad_signature_errs_without_panic() {
570        let km = sample_even_only();
571        let mut buf = alloc::vec![0u8; km.serialized_len()];
572        km.serialize_into(&mut buf).unwrap();
573        buf[1] = 0x00; // corrupt the Sign field
574        assert!(matches!(
575            KeyMaterial::parse(&buf),
576            Err(Error::InvalidKeyMaterial { field: "Sign", .. })
577        ));
578    }
579
580    #[test]
581    fn inconsistent_kk_o_sek_errs() {
582        let mut km = sample_even_only();
583        km.o_sek = Some(&[0xEE; 16]); // KK=Even must not carry oSEK
584        let mut buf = alloc::vec![0u8; 16 + 16 + 8 + 16 + 16];
585        assert!(matches!(
586            km.serialize_into(&mut buf),
587            Err(Error::InvalidKeyMaterial { .. })
588        ));
589    }
590}