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