pssh_box/
lib.rs

1//! Parsing and serialization support for pssh boxes, as used in DRM systems.
2//!
3//! This crate defines Rust data structures allowing you to store, parse and serialize Protection System
4//! Specific Header (**PSSH**) boxes, which provide data for the initialization of a Digital Rights
5//! Management (DRM) system. PSSH boxes are used:
6//!
7//! - in an MP4 box of type `pssh` in an MP4 fragment (CMAF/MP4/ISOBMFF containers)
8//!
9//! - in a `<cenc:pssh>` element in a DASH MPD manifest
10//!
11//! - in DRM initialization data passed to the Encrypted Media Extension of a web browser
12//!
13//! - in an EXT-X-SESSION-KEY field of an m3u8 playlist.
14//!
15//! A PSSH box includes information for a single DRM system. This library supports the PSSH data formats
16//! for the following DRM systems:
17//!
18//! - Widevine, owned by Google, widely used for DASH streaming
19//! - PlayReady, owned by Microsoft, widely used for DASH streaming
20//! - WisePlay, owned by Huawei
21//! - Irdeto
22//! - Marlin
23//! - Nagra
24//! - FairPlay (the unofficial version used by Netflix)
25//! - Common Encryption
26//!
27//! PSSH boxes contain (depending on the DRM system) information on the key_ID for which to obtain a
28//! content key, the encryption scheme used (e.g. cenc, cbc1, cens or cbcs), the URL of the licence
29//! server, and checksum data.
30
31
32pub mod playready;
33pub mod widevine;
34pub mod irdeto;
35pub mod nagra;
36pub mod wiseplay;
37
38use std::fmt;
39use std::io::{self, Cursor, Read, Write};
40use hex_literal::hex;
41use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
42use zerocopy::FromBytes;
43use serde::{Serialize, Deserialize};
44use prost::Message;
45use base64::prelude::{Engine as _, BASE64_STANDARD};
46use base64::engine;
47use anyhow::{Result, Context, anyhow};
48use tracing::trace;
49use crate::widevine::WidevinePsshData;
50use crate::playready::PlayReadyPsshData;
51use crate::irdeto::IrdetoPsshData;
52use crate::nagra::NagraPsshData;
53use crate::wiseplay::WisePlayPsshData;
54
55
56/// The version of this crate.
57pub fn version() -> &'static str {
58    env!("CARGO_PKG_VERSION")
59}
60
61pub trait ToBytes {
62    fn to_bytes(&self) -> Vec<u8>;
63}
64
65/// Data in a PSSH box whose format is dependent on the DRM system used.
66#[non_exhaustive]
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub enum PsshData {
69    Widevine(WidevinePsshData),
70    PlayReady(PlayReadyPsshData),
71    Irdeto(IrdetoPsshData),
72    WisePlay(WisePlayPsshData),
73    Nagra(NagraPsshData),
74    Marlin(Vec<u8>),
75    CommonEnc(Vec<u8>),
76    FairPlay(Vec<u8>),
77}
78
79impl ToBytes for PsshData {
80    fn to_bytes(&self) -> Vec<u8> {
81        match self {
82            PsshData::Widevine(wv) => wv.to_bytes(),
83            PsshData::PlayReady(pr) => pr.to_bytes(),
84            PsshData::Irdeto(ir) => ir.to_bytes(),
85            PsshData::WisePlay(c) => c.to_bytes(),
86            PsshData::Nagra(n) => n.to_bytes(),
87            PsshData::Marlin(m) => m.to_vec(),
88            PsshData::CommonEnc(c) => c.to_vec(),
89            PsshData::FairPlay(c) => c.to_vec(),
90        }
91    }
92}
93
94impl fmt::Display for PsshData {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            PsshData::Widevine(wv) => {
98                let mut items = Vec::new();
99                let mut keys = Vec::new();
100                let json = wv.to_json();
101                if let Some(alg) = json.get("algorithm") {
102                    if let Some(a) = alg.as_str() {
103                        items.push(String::from(a));
104                    }
105                }
106                if let Some(content_id) = json.get("content_id") {
107                    if let Some(cid_hex) = content_id.as_str() {
108                        if let Ok(cid_octets) = hex::decode(cid_hex) {
109                            if let Ok(cid) = String::from_utf8(cid_octets) {
110                                items.push(format!("content_id: \"{cid}\""));
111                            }
112                        }
113                    }
114                }
115                if let Some(kav) = json.get("key_id") {
116                    if let Some(ka) = kav.as_array() {
117                        for kv in ka {
118                            if let Some(k) = kv.as_str() {
119                                keys.push(String::from(k));
120                            }
121                        }
122                    }
123                }
124                if keys.len() == 1 {
125                    if let Some(key) = keys.first() {
126                        items.push(format!("key_id: {key}"));
127                    }
128                }
129                if keys.len() > 1 {
130                    items.push(format!("key_ids: {}", keys.join(", ")));
131                }
132                if let Some(jo) = json.as_object() {
133                    for (k, v) in jo.iter() {
134                        if k.ne("algorithm") && k.ne("key_id") && k.ne("content_id") {
135                            items.push(format!("{k}: {v}"));
136                        }
137                    }
138                }
139                write!(f, "WidevinePSSHData<{}>", items.join(", "))
140            },
141            PsshData::PlayReady(pr) => write!(f, "PlayReadyPSSHData<{pr:?}>"),
142            PsshData::Irdeto(pd) => write!(f, "IrdetoPSSHData<{}>", pd.xml),
143            PsshData::Marlin(pd) => write!(f, "  MarlinPSSHData<len {} octets>", pd.len()),
144            PsshData::Nagra(pd) => write!(f, "NagraPSSHData<{pd:?}>"),
145            PsshData::WisePlay(pd) => write!(f, "WisePlayPSSHData<{}>", pd.json),
146            PsshData::CommonEnc(pd) => write!(f, "CommonPSSHData<len {} octets>", pd.len()),
147            PsshData::FairPlay(pd) => write!(f, "FairPlayPSSHData<len {} octets>", pd.len()),
148        }
149    }
150}
151
152/// The identifier for a DRM system.
153#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, FromBytes)]
154pub struct DRMSystemId {
155    id: [u8; 16],
156}
157
158impl TryFrom<&[u8]> for DRMSystemId {
159    type Error = ();
160
161    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
162        if let Ok(id) = value.try_into() {
163            Ok(DRMSystemId { id })
164        } else {
165            Err(())
166        }
167    }
168}
169
170impl TryFrom<Vec<u8>> for DRMSystemId {
171    type Error = ();
172
173    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
174        if value.len() == 16 {
175            DRMSystemId::try_from(&value[0..16])
176        } else {
177            Err(())
178        }
179    }
180}
181
182impl TryFrom<&str> for DRMSystemId {
183    type Error = ();
184
185    fn try_from(value: &str) -> Result<Self, Self::Error> {
186        if value.len() == 32 {
187            if let Ok(id) = hex::decode(value) {
188                return DRMSystemId::try_from(id);
189            }
190        }
191        Err(())
192    }
193}
194
195impl ToBytes for DRMSystemId {
196    fn to_bytes(&self) -> Vec<u8> {
197        self.id.into()
198    }
199}
200
201impl fmt::Display for DRMSystemId {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        // See list at https://dashif.org/identifiers/content_protection/
204        let family = if self.id == hex!("1077efecc0b24d02ace33c1e52e2fb4b") {
205            "Common"
206        } else if self.id == hex!("69f908af481646ea910ccd5dcccb0a3a") {
207            "CENC"
208        } else if self.id == hex!("6770616363656e6364726d746f6f6c31") {
209            // See https://github.com/gpac/testsuite/blob/b1c1f23079431221b582f3c7674706c6b6044cf2/media/encryption/tpl_roll.xml#L6
210            "GPAC"
211        } else if self.id == hex!("edef8ba979d64acea3c827dcd51d21ed") {
212            "Widevine"
213        } else if self.id == hex!("9a04f07998404286ab92e65be0885f95") {
214            "PlayReady"
215        } else if self.id == hex!("6dd8b3c345f44a68bf3a64168d01a4a6") {
216            "ABV"
217        } else if self.id == hex!("f239e769efa348509c16a903c6932efb") {
218            "Adobe Primetime"
219        } else if self.id == hex!("616c7469636173742d50726f74656374") {
220            "Alticast"
221        } else if self.id == hex!("94ce86fb07ff4f43adb893d2fa968ca2") {
222            "Apple FairPlay"
223        } else if self.id == hex!("29701fe43cc74a348c5bae90c7439a47") {
224            // Unofficial FairPlay systemID used by Netflix for DASH streaming,
225            // see https://forums.developer.apple.com/thread/6185
226            "Apple FairPlay-Netflix variant"
227        } else if self.id == hex!("3ea8778f77424bf9b18be834b2acbd47") {
228            "ClearKey AES-128"
229        } else if self.id == hex!("be58615b19c4468488b3c8c57e99e957") {
230            "ClearKey SAMPLE-AES"
231        } else if self.id == hex!("e2719d58a985b3c9781ab030af78d30e") {
232            "ClearKey DASH-IF"
233        } else if self.id == hex!("45d481cb8fe049c0ada9ab2d2455b2f2") {
234            "CoreTrust"
235        } else if self.id == hex!("80a6be7e14484c379e70d5aebe04c8d2") {
236            "Irdeto"
237        } else if self.id == hex!("5e629af538da4063897797ffbd9902d4") {
238            "Marlin"
239        } else if self.id == hex!("adb41c242dbf4a6d958b4457c0d27b95") {
240            "Nagra"
241        } else if self.id == hex!("1f83e1e86ee94f0dba2f5ec4e3ed1a66") {
242            "SecureMedia"
243        } else if self.id == hex!("3d5e6d359b9a41e8b843dd3c6e72c42c") {
244            // WisePlay (from Huawei) and ChinaDRM are apparently different DRM systems that are
245            // identified by the same system id.
246            "WisePlay-ChinaDRM"
247        } else if self.id == hex!("793b79569f944946a94223e7ef7e44b4") {
248            "VisionCrypt"
249        } else {
250            "Unknown"
251        };
252        let hex = hex::encode(self.id);
253        write!(f, "{}/DRMSystemId<{}-{}-{}-{}-{}>",
254               family,
255               &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
256    }
257}
258
259impl fmt::Debug for DRMSystemId {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "DRMSystemId<{}>", hex::encode(self.id))
262    }
263}
264
265pub const COMMON_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("1077efecc0b24d02ace33c1e52e2fb4b") };
266pub const CENC_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("69f908af481646ea910ccd5dcccb0a3a") };
267pub const WIDEVINE_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("edef8ba979d64acea3c827dcd51d21ed") };
268pub const PLAYREADY_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("9a04f07998404286ab92e65be0885f95") };
269pub const FAIRPLAYNFLX_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("29701fe43cc74a348c5bae90c7439a47") };
270pub const IRDETO_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("80a6be7e14484c379e70d5aebe04c8d2") };
271pub const MARLIN_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("5e629af538da4063897797ffbd9902d4") };
272pub const NAGRA_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("adb41c242dbf4a6d958b4457c0d27b95") };
273pub const WISEPLAY_SYSTEM_ID: DRMSystemId = DRMSystemId { id: hex!("3d5e6d359b9a41e8b843dd3c6e72c42c") };
274
275/// The Content Key or default_KID.
276#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, FromBytes)]
277pub struct DRMKeyId {
278    id: [u8; 16],
279}
280
281impl TryFrom<&[u8]> for DRMKeyId {
282    type Error = ();
283
284    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
285        if let Ok(id) = value.try_into() {
286            Ok(DRMKeyId { id })
287        } else {
288            Err(())
289        }
290    }
291}
292
293impl TryFrom<Vec<u8>> for DRMKeyId {
294    type Error = ();
295
296    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
297        if value.len() == 16 {
298            DRMKeyId::try_from(&value[0..16])
299        } else {
300            Err(())
301        }
302    }
303}
304
305impl TryFrom<&str> for DRMKeyId {
306    type Error = ();
307
308    fn try_from(value: &str) -> Result<Self, Self::Error> {
309        if value.len() == 32 {
310            if let Ok(id) = hex::decode(value) {
311                return DRMKeyId::try_from(id);
312            }
313        }
314        // UUID-style format, like 5ade6a1e-c0d4-43c6-92f2-2d36862ba8dd
315        if value.len() == 36 {
316            let v36 = value.as_bytes();
317            if v36[8] == b'-' &&
318                v36[13] == b'-' &&
319                v36[18] == b'-' &&
320                v36[23] == b'-'
321            {
322                let maybe_hex = value.replace('-', "");
323                if let Ok(id) = hex::decode(maybe_hex) {
324                    return DRMKeyId::try_from(id);
325                }
326            }
327        }
328        Err(())
329    }
330}
331
332impl ToBytes for DRMKeyId {
333    fn to_bytes(&self) -> Vec<u8> {
334        self.id.into()
335    }
336}
337
338impl fmt::Display for DRMKeyId {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        // example: 72c3ed2c-7a5f-4aad-902f-cbef1efe89a9
341        let hex = hex::encode(self.id);
342        write!(f, "DRMKeyId<{}-{}-{}-{}-{}>",
343               &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
344    }
345}
346
347impl fmt::Debug for DRMKeyId {
348    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349        write!(f, "DRMKeyId<{}>", hex::encode(self.id))
350    }
351}
352
353
354/// A PSSH box, also called a ProtectionSystemSpecificHeaderBox in ISO 23001-7:2012.
355#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
356pub struct PsshBox {
357    pub version: u8,
358    pub flags: u32,
359    pub system_id: DRMSystemId,
360    pub key_ids: Vec<DRMKeyId>,
361    pub pssh_data: PsshData,
362}
363
364impl PsshBox {
365    /// Return an empty v1 Widevine PSSH box.
366    pub fn new_widevine() -> PsshBox {
367        let empty = WidevinePsshData {
368            provider: None,
369            ..Default::default()
370        };
371        PsshBox {
372            version: 1,
373            flags: 0,
374            system_id: WIDEVINE_SYSTEM_ID,
375            key_ids: vec![],
376            pssh_data: PsshData::Widevine(empty),
377        }
378    }
379
380    /// Return an empty v1 PlayReady PSSH box.
381    pub fn new_playready() -> PsshBox {
382        let empty = PlayReadyPsshData::new();
383        PsshBox {
384            version: 1,
385            flags: 0,
386            system_id: PLAYREADY_SYSTEM_ID,
387            key_ids: vec![],
388            pssh_data: PsshData::PlayReady(empty),
389        }
390    }
391
392    pub fn add_key_id(&mut self, kid: DRMKeyId) {
393        self.key_ids.push(kid);
394    }
395
396    pub fn to_base64(self) -> String {
397        BASE64_STANDARD.encode(self.to_bytes())
398    }
399
400    pub fn to_hex(self) -> String {
401        hex::encode(self.to_bytes())
402    }
403}
404
405/// This to_string() method provides the most compact representation possible on a single line; see
406/// the pprint() function for a more verbose layout.
407impl fmt::Display for PsshBox {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        let mut keys = Vec::new();
410        if self.version == 1 {
411            for key in &self.key_ids {
412                keys.push(hex::encode(key.id));
413            }
414        }
415        let key_str = match keys.len() {
416            0 => String::from(""),
417            1 => format!("key_id: {}, ", keys.first().unwrap()),
418            _ => format!("key_ids: {}, ", keys.join(", ")),
419        };
420        match &self.pssh_data {
421            PsshData::Widevine(wv) => {
422                let mut items = Vec::new();
423                let json = wv.to_json();
424                if let Some(alg) = json.get("algorithm") {
425                    if let Some(a) = alg.as_str() {
426                        items.push(String::from(a));
427                    }
428                }
429                // We are merging keys potentially present in the v1 PSSH box data with those
430                // present in the Widevine PSSH data.
431                if let Some(kav) = json.get("key_id") {
432                    if let Some(ka) = kav.as_array() {
433                        for kv in ka {
434                            if let Some(k) = kv.as_str() {
435                                keys.push(String::from(k));
436                            }
437                        }
438                    }
439                }
440                if keys.len() == 1 {
441                    items.push(format!("key_id: {}", keys.first().unwrap()));
442                }
443                if keys.len() > 1 {
444                    items.push(format!("key_ids: {}", keys.join(", ")));
445                }
446                if let Some(jo) = json.as_object() {
447                    for (k, v) in jo.iter() {
448                        if k.ne("algorithm") && k.ne("key_id") {
449                            items.push(format!("{k}: {v}"));
450                        }
451                    }
452                }
453                write!(f, "WidevinePSSH<{}>", items.join(", "))
454            },
455            PsshData::PlayReady(pr) => write!(f, "PlayReadyPSSH<{key_str}{pr:?}>"),
456            PsshData::Irdeto(pd) => write!(f, "IrdetoPSSH<{key_str}{}>", pd.xml),
457            PsshData::Marlin(pd) => write!(f, "  MarlinPSSH<{key_str}pssh data len {} octets>", pd.len()),
458            PsshData::Nagra(pd) => write!(f, "NagraPSSH<{key_str}{pd:?}>"),
459            PsshData::WisePlay(pd) => write!(f, "WisePlayPSSH<{key_str}{}>", pd.json),
460            PsshData::CommonEnc(pd) => write!(f, "CommonPSSH<{key_str}pssh data len {} octets>", pd.len()),
461            PsshData::FairPlay(pd) => write!(f, "FairPlayPSSH<{key_str}pssh data len {} octets>", pd.len()),
462        }
463    }
464}
465
466
467impl ToBytes for PsshBox {
468    #[allow(unused_must_use)]
469    fn to_bytes(self: &PsshBox) -> Vec<u8> {
470        let mut out = Vec::new();
471        let pssh_data_bytes = self.pssh_data.to_bytes();
472        let mut total_length: u32 = 4 // box size
473            + 4     // BMFF box header 'pssh'
474            + 4     // version+flags
475            + 16    // system_id
476            + 4     // pssh_data length
477            + pssh_data_bytes.len() as u32;
478        if self.version == 1 {
479            total_length += 4 // key_id count
480                + self.key_ids.len() as u32 * 16;
481        }
482        out.write_u32::<BigEndian>(total_length);
483        out.write_all(b"pssh");
484        let version_and_flags: u32 = self.flags ^ ((self.version as u32) << 24);
485        out.write_u32::<BigEndian>(version_and_flags);
486        out.write_all(&self.system_id.id);
487        if self.version == 1 {
488            out.write_u32::<BigEndian>(self.key_ids.len() as u32);
489            for k in &self.key_ids {
490                out.write_all(&k.id);
491            }
492        }
493        out.write_u32::<BigEndian>(pssh_data_bytes.len() as u32);
494        out.write_all(&pssh_data_bytes);
495        out
496    }
497}
498
499
500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
501pub struct PsshBoxVec(Vec<PsshBox>);
502
503impl PsshBoxVec {
504    pub fn new() -> PsshBoxVec {
505        PsshBoxVec(Vec::new())
506    }
507
508    pub fn contains(&self, bx: &PsshBox) -> bool {
509        self.0.contains(bx)
510    }
511
512    pub fn add(&mut self, bx: PsshBox) {
513        self.0.push(bx);
514    }
515
516    pub fn len(&self) -> usize {
517        self.0.len()
518    }
519
520    pub fn is_empty(&self) -> bool {
521        self.0.is_empty()
522    }
523
524    pub fn iter(&self) -> impl Iterator<Item=&PsshBox>{
525        self.0.iter()
526    }
527
528    pub fn to_base64(self) -> String {
529        let mut buf = Vec::new();
530        for bx in self.0 {
531            buf.append(&mut bx.to_bytes());
532        }
533        BASE64_STANDARD.encode(buf)
534    }
535
536    pub fn to_hex(self) -> String {
537        let mut buf = Vec::new();
538        for bx in self.0 {
539            buf.append(&mut bx.to_bytes());
540        }
541        hex::encode(buf)
542    }
543}
544
545impl Default for PsshBoxVec {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551impl IntoIterator for PsshBoxVec {
552    type Item = PsshBox;
553    type IntoIter = std::vec::IntoIter<Self::Item>;
554
555    fn into_iter(self) -> Self::IntoIter {
556        self.0.into_iter()
557    }
558}
559
560impl std::ops::Index<usize> for PsshBoxVec {
561    type Output = PsshBox;
562
563    fn index(&self, index: usize) -> &PsshBox {
564        &self.0[index]
565    }
566}
567
568impl fmt::Display for PsshBoxVec {
569    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        let mut items = Vec::new();
571        for pssh in self.iter() {
572            items.push(pssh.to_string());
573        }
574        // Print one PsshBox per line, without a trailing newline.
575        write!(f, "{}", items.join("\n"))
576    }
577}
578
579// Initialization Data is always composed of one or more concatenated 'pssh' boxes. The CDM must be
580// able to examine multiple 'pssh' boxes in the Initialization Data to find a 'pssh' box that it
581// supports.
582
583/// Parse one or more PSSH boxes from some initialization data encoded in base64 format.
584pub fn from_base64(init_data: &str) -> Result<PsshBoxVec> {
585    let b64_tolerant_config = engine::GeneralPurposeConfig::new()
586        .with_decode_allow_trailing_bits(true)
587        .with_decode_padding_mode(engine::DecodePaddingMode::Indifferent);
588    let b64_tolerant_engine = engine::GeneralPurpose::new(&base64::alphabet::STANDARD, b64_tolerant_config);
589    if init_data.len() < 8 {
590        return Err(anyhow!("insufficient length for init data"));
591    }
592    // We start by attempting to base64 decode the full string and parse that.
593    if let Ok(buf) = b64_tolerant_engine.decode(init_data) {
594        return from_bytes(&buf);
595    }
596    // If that doesn't work, attempt to decode PSSH boxes from subsequences of the init data. We
597    // look at a sliding window that starts at start and ends at start + the length we see from the
598    // PSSH box header.
599    let total_len = init_data.len();
600    let mut start = 0;
601    let mut boxes = Vec::new();
602    while start < total_len - 1 {
603        let buf = b64_tolerant_engine.decode(&init_data[start..start+7])
604            .context("base64 decoding first 32-bit length word")?;
605        let mut rdr = Cursor::new(buf);
606        let box_size: u32 = rdr.read_u32::<BigEndian>()
607            .context("reading PSSH box size")?;
608        trace!("box size from header = {box_size}");
609        // The number of octets that we obtain from decoding box_size chars worth of base64
610        let wanted_octets = (box_size.div_ceil(3) * 4) as usize;
611        let end = start + wanted_octets;
612        trace!("attempting to decode {wanted_octets} octets out of {}", init_data.len());
613        if end > init_data.len() {
614            // FIXME actually we shouldn't fail here, but rather break and return any boxes that we
615            // did manage to parse
616            return Err(anyhow!("insufficient length for init data (wanted {end}, have {})", init_data.len()));
617        }
618        let buf = b64_tolerant_engine.decode(&init_data[start..end])
619            .context("decoding base64")?;
620        let bx = from_bytes(&buf)
621            .context("parsing the PSSH initialization data")?;
622        assert!(bx.len() == 1);
623        trace!("Got one box {}", bx[0].clone());
624        boxes.push(bx[0].clone());
625        start = end;
626    }
627    Ok(PsshBoxVec(boxes))
628}
629
630/// Parse one or more PSSH boxes from some initialization data encoded in hex format.
631pub fn from_hex(init_data: &str) -> Result<PsshBoxVec> {
632    let buf = hex::decode(init_data)
633        .context("decoding hex")?;
634    from_bytes(&buf)
635        .context("parsing the PSSH initialization_data")
636}
637
638/// Parse a single PSSH box.
639fn read_pssh_box(rdr: &mut Cursor<&[u8]>) -> Result<PsshBox> {
640    let size: u32 = rdr.read_u32::<BigEndian>()
641        .context("reading PSSH box size")?;
642    trace!("PSSH box of size {size} octets");
643    let mut box_header = [0u8; 4];
644    rdr.read_exact(&mut box_header)
645        .context("reading box header")?;
646    // the ISO BMFF box header
647    if !box_header.eq(b"pssh") {
648        return Err(anyhow!("expecting BMFF header"));
649    }
650    let version_and_flags: u32 = rdr.read_u32::<BigEndian>()
651        .context("reading PSSH version/flags")?;
652    let version: u8 = (version_and_flags >> 24).try_into().unwrap();
653    trace!("PSSH box version {version}");
654    if version > 1 {
655        return Err(anyhow!("unknown PSSH version {version}"));
656    }
657    let mut system_id_buf = [0u8; 16];
658    rdr.read_exact(&mut system_id_buf)
659        .context("reading system_id")?;
660    let system_id = DRMSystemId { id: system_id_buf };
661    let mut key_ids = Vec::new();
662    if version == 1 {
663        let mut kid_count = rdr.read_u32::<BigEndian>()
664            .context("reading KID count")?;
665        trace!("PSSH box has {kid_count} KIDs in box header");
666        while kid_count > 0 {
667            let mut key = [0u8; 16];
668            rdr.read_exact(&mut key)
669                .context("reading key_id")?;
670            key_ids.push(DRMKeyId { id: key });
671            kid_count -= 1;
672        }
673    }
674    let pssh_data_len = rdr.read_u32::<BigEndian>()
675        .context("reading PSSH data length")?;
676    trace!("PSSH box data length {pssh_data_len} octets");
677    let mut pssh_data = Vec::new();
678    rdr.take(pssh_data_len.into()).read_to_end(&mut pssh_data)
679        .context("extracting PSSH data")?;
680    match system_id {
681        WIDEVINE_SYSTEM_ID => {
682            let wv_pssh_data = WidevinePsshData::decode(Cursor::new(pssh_data))
683                .context("parsing Widevine PSSH data")?;
684            Ok(PsshBox {
685                version,
686                flags: version_and_flags & 0xF,
687                system_id,
688                key_ids,
689                pssh_data: PsshData::Widevine(wv_pssh_data),
690            })
691        },
692        PLAYREADY_SYSTEM_ID => {
693            let pr_pssh_data = playready::parse_pssh_data(&pssh_data)
694                .context("parsing PlayReady PSSH data")?;
695            Ok(PsshBox {
696                version,
697                flags: version_and_flags & 0xF,
698                system_id,
699                key_ids,
700                pssh_data: PsshData::PlayReady(pr_pssh_data),
701            })
702        },
703        IRDETO_SYSTEM_ID => {
704            let ir_pssh_data = irdeto::parse_pssh_data(&pssh_data)
705                .context("parsing Irdeto PSSH data")?;
706            Ok(PsshBox {
707                version,
708                flags: version_and_flags & 0xF,
709                system_id,
710                key_ids,
711                pssh_data: PsshData::Irdeto(ir_pssh_data),
712            })
713        },
714        MARLIN_SYSTEM_ID => {
715            Ok(PsshBox {
716                version,
717                flags: version_and_flags & 0xF,
718                system_id,
719                key_ids,
720                pssh_data: PsshData::Marlin(pssh_data),
721            })
722        },
723        NAGRA_SYSTEM_ID => {
724            let pd = nagra::parse_pssh_data(&pssh_data)
725                .context("parsing Nagra PSSH data")?;
726            Ok(PsshBox {
727                version,
728                flags: version_and_flags & 0xF,
729                system_id,
730                key_ids,
731                pssh_data: PsshData::Nagra(pd),
732            })
733        },
734        WISEPLAY_SYSTEM_ID => {
735            let cdrm_pssh_data = wiseplay::parse_pssh_data(&pssh_data)
736                .context("parsing WisePlay PSSH data")?;
737            Ok(PsshBox {
738                version,
739                flags: version_and_flags & 0xF,
740                system_id,
741                key_ids,
742                pssh_data: PsshData::WisePlay(cdrm_pssh_data),
743            })
744        },
745        COMMON_SYSTEM_ID => {
746            Ok(PsshBox {
747                version,
748                flags: version_and_flags & 0xF,
749                system_id,
750                key_ids,
751                pssh_data: PsshData::CommonEnc(pssh_data),
752            })
753        },
754        FAIRPLAYNFLX_SYSTEM_ID => {
755            Ok(PsshBox {
756                version,
757                flags: version_and_flags & 0xF,
758                system_id,
759                key_ids,
760                pssh_data: PsshData::FairPlay(pssh_data),
761            })
762        },
763        _ => Err(anyhow!("can't parse this system_id type: {:?}", system_id)),
764    }
765}
766
767/// Read one or more PSSH boxes from some initialization data provided as a slice of octets,
768/// returning an error if any non-PSSH data is found in the slice or if the parsing fails.
769pub fn from_bytes(init_data: &[u8]) -> Result<PsshBoxVec> {
770    let total_len = init_data.len();
771    let mut rdr = Cursor::new(init_data);
772    let mut boxes = PsshBoxVec::new();
773    while (rdr.position() as usize) < total_len - 1  {
774        let bx = read_pssh_box(&mut rdr)?;
775        boxes.add(bx.clone());
776        trace!("Read one box {bx} from bytes, remaining {} octets", total_len as u64 - rdr.position());
777        let pos = rdr.position() as usize;
778        if let Some(remaining) = &rdr.get_ref().get(pos..total_len) {
779            // skip over any octets that are NULL
780            if remaining.iter().all(|b| *b == 0) {
781                break;
782            }
783        }
784    }
785    Ok(boxes)
786}
787
788/// Read one or more PSSH boxes from a slice of octets, stopping (but not returning an error) when
789/// non-PSSH data is found in the slice. An error is returned if the parsing fails.
790pub fn from_buffer(init_data: &[u8]) -> Result<PsshBoxVec> {
791    let total_len = init_data.len();
792    let mut rdr = Cursor::new(init_data);
793    let mut boxes = PsshBoxVec::new();
794    while (rdr.position() as usize) < total_len - 1  {
795        if let Ok(bx) = read_pssh_box(&mut rdr) {
796            boxes.add(bx);
797        } else {
798            break;
799        }
800    }
801    Ok(boxes)
802}
803
804/// Locate the positions of PSSH boxes in a buffer, if any are present. Returns an iterator over
805/// start positions for PSSH boxes in the buffer.
806pub fn find_iter(buffer: &[u8]) -> impl Iterator<Item = usize> + '_ {
807    use bstr::ByteSlice;
808
809    buffer.find_iter(b"pssh")
810        .filter(|offset| {
811            if offset+24 > buffer.len() {
812                return false;
813            }
814            if offset+4 < 8 {
815                return false;
816            }
817            let start = offset - 4;
818            let mut rdr = Cursor::new(&buffer[start..]);
819            let size: u32 = rdr.read_u32::<BigEndian>().unwrap();
820            let end = start + size as usize;
821            if end > buffer.len() {
822                return false;
823            }
824            from_bytes(&buffer[start..end]).is_ok()
825        })
826        .map(|offset| offset - 4)
827}
828
829
830/// Extract PSSH boxes in a buffer, if any are present. Returns an iterator over PSSH boxes in the
831/// buffer.
832pub fn find_boxes_buffer(buffer: &[u8]) -> impl Iterator<Item = PsshBox> + '_ {
833    use bstr::ByteSlice;
834
835    let mut boxes = Vec::new();
836    for offset in buffer.find_iter(b"pssh") {
837        if offset+24 > buffer.len() || offset+4 < 8 {
838            break;
839        }
840        let start = offset - 4;
841        let mut rdr = Cursor::new(&buffer[start..]);
842        let size: u32 = rdr.read_u32::<BigEndian>().unwrap();
843        let end = start + size as usize;
844        if end > buffer.len() {
845            break;
846        }
847        if let Ok(pbv) = from_bytes(&buffer[start..end]) {
848            for pb in pbv {
849                boxes.push(pb);
850            }
851        }
852    }
853    boxes.into_iter()
854}
855
856
857/// Extract PSSH boxes from a stream of octets (an object that implements `Read`), if any are
858/// present. Returns an iterator whose elements are a `PsshBox` or an `io::Error`. The input is read
859/// in streaming mode (chunk by chunk), without storing the entire contents in memory. The search is
860/// undertaken lazily: successive chunks of octets are read only as needed for the iterator to
861/// provide the next item.
862pub fn find_boxes_stream<R>(reader: R) -> impl Iterator<Item = Result<PsshBox, io::Error>>
863where
864    R: Read,
865{
866    PsshBoxIterator::new(reader)
867}
868
869struct PsshBoxIterator<R> {
870    reader: R,
871    buffer: Vec<u8>,
872    buffer_pos: usize,
873    pending_boxes : Vec<PsshBox>,
874}
875
876impl<R> PsshBoxIterator<R> {
877    fn new(reader: R) -> Self {
878        PsshBoxIterator {
879            reader,
880            buffer: Vec::new(),
881            buffer_pos: 0,
882            pending_boxes: Vec::new(),
883        }
884    }
885}
886
887impl<R> Iterator for PsshBoxIterator<R>
888where
889    R: Read,
890{
891    type Item = Result<PsshBox, io::Error>;
892
893    fn next(&mut self) -> Option<Self::Item> {
894        use bstr::ByteSlice;
895
896        if let Some(bx) = self.pending_boxes.pop() {
897            return Some(Ok(bx));
898        }
899        let mut buf = [0; 1024 * 1024];
900        loop {
901            if self.buffer_pos > 0 {
902                self.buffer = self.buffer.split_off(self.buffer_pos);
903            }
904            let bytes_read = match self.reader.read(&mut buf) {
905                Ok(n) => n,
906                Err(e) => return Some(Err(e)),
907            };
908            if self.buffer_pos == 0 && bytes_read == 0 {
909                return None;
910            }
911            self.buffer.extend_from_slice(&buf[..bytes_read]);
912            self.buffer_pos = 0;
913            if let Some(offset) = self.buffer.find(b"pssh") {
914                trace!("Found pssh cookie at offset {offset}");
915                if offset + 24 > self.buffer.len() {
916                    self.buffer_pos = offset + 4;
917                    continue;
918                }
919                if offset < 4 {
920                    self.buffer_pos = 4;
921                    continue;
922                }
923                let start = offset - 4;
924                let buffer_len = self.buffer.len();
925                let mut rdr = Cursor::new(&self.buffer[start..buffer_len]);
926                let size: u32 = rdr.read_u32::<BigEndian>().unwrap();
927                let end = start + size as usize;
928                if end > self.buffer.len() {
929                    self.buffer_pos = offset + 1;
930                    continue;
931                }
932                if let Ok(pbv) = from_bytes(&self.buffer[start..end]) {
933                    self.buffer_pos = end;
934                    for pb in pbv {
935                        self.pending_boxes.push(pb);
936                    }
937                    if let Some(bx) = self.pending_boxes.pop() {
938                        return Some(Ok(bx));
939                    }
940                } else {
941                    self.buffer_pos = offset + 4;
942                }
943            } else {
944                // Try the last bit of the buffer in the next loop iteration, in case the b"pssh"
945                // cookie is at the buffer boundary.
946                if self.buffer.len() >= 3 {
947                    self.buffer_pos = self.buffer.len() - 3;
948                }
949            }
950        }
951    }
952}
953
954
955/// Multiline pretty printing of a PsshBox (verbose alternative to `to_string()` method).
956pub fn pprint(pssh: &PsshBox) {
957    println!("PSSH Box v{}", pssh.version);
958    println!("  SystemID: {}", pssh.system_id);
959    if pssh.version == 1 {
960        for key in &pssh.key_ids {
961            println!("  Key ID: {key}");
962        }
963    }
964    match &pssh.pssh_data {
965        PsshData::Widevine(wv) => println!("  {wv:?}"),
966        PsshData::PlayReady(pr) => println!("  {pr:?}"),
967        PsshData::Irdeto(pd) => {
968            println!("Irdeto XML: {}", pd.xml);
969        },
970        PsshData::Marlin(pd) => {
971            println!("  Marlin PSSH data ({} octets)", pd.len());
972            if !pd.is_empty() {
973                println!("== Hexdump of pssh data ==");
974                let mut hxbuf = Vec::new();
975                hxdmp::hexdump(pd, &mut hxbuf).unwrap();
976                println!("{}", String::from_utf8_lossy(&hxbuf));
977            }
978        },
979        PsshData::Nagra(pd) => println!("  {pd:?}"),
980        PsshData::WisePlay(pd) => {
981            println!("  WisePlay JSON: {}", pd.json);
982        },
983        PsshData::CommonEnc(pd) => {
984            println!("  Common PSSH data ({} octets)", pd.len());
985            if !pd.is_empty() {
986                println!("== Hexdump of pssh data ==");
987                let mut hxbuf = Vec::new();
988                hxdmp::hexdump(pd, &mut hxbuf).unwrap();
989                println!("{}", String::from_utf8_lossy(&hxbuf));
990            }
991        },
992        PsshData::FairPlay(pd) => {
993            println!("  FairPlay PSSH data ({} octets)", pd.len());
994            if !pd.is_empty() {
995                println!("== Hexdump of pssh data ==");
996                let mut hxbuf = Vec::new();
997                hxdmp::hexdump(pd, &mut hxbuf).unwrap();
998                println!("{}", String::from_utf8_lossy(&hxbuf));
999            }
1000        },
1001    }
1002}