pssh_box/
playready.rs

1//! Definitions for PSSH data in the PlayReady DRM system.
2
3// PlayReady PSSH Data is a PlayReady Header Object, whose format is described at
4// https://docs.microsoft.com/en-us/playready/specifications/playready-header-specification
5//
6// and
7//
8// https://download.microsoft.com/download/2/3/8/238F67D9-1B8B-48D3-AB83-9C00112268B2/PlayReady%20Header%20Object%202015-08-13-FINAL-CL.PDF
9
10
11use std::fmt;
12use std::io::{Read, Cursor};
13use std::fmt::{Error, Write};
14use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
15use serde::{Serialize, Deserialize};
16use serde_with::{serde_as, skip_serializing_none};
17use serde_with::base64::Base64;
18use num_enum::TryFromPrimitive;
19use tracing::trace;
20use anyhow::{Result, Context, anyhow};
21use crate::ToBytes;
22
23
24struct Utf16Writer(Vec<u16>);
25
26impl Write for Utf16Writer {
27    fn write_str(&mut self, s: &str) -> Result<(), Error> {
28        self.0.extend(s.encode_utf16());
29        Ok(())
30    }
31
32    fn write_char(&mut self, c: char) -> Result<(), Error> {
33        self.0.extend(c.encode_utf16(&mut [0; 2]).iter());
34        Ok(())
35    }
36}
37
38pub fn to_utf16(xml: &str) -> Vec<u16> {
39    let mut writer = Utf16Writer(Vec::new());
40    write!(writer, "{xml}")
41        .expect("writing XML as UTF-16");
42    writer.0
43}
44
45fn serialize_xmlns<S>(os: &Option<String>, serializer: S) -> Result<S::Ok, S::Error>
46where S: serde::Serializer {
47    if let Some(s) = os {
48        serializer.serialize_str(s)
49    } else {
50        serializer.serialize_str("http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader")
51    }
52}
53
54#[serde_as]
55#[skip_serializing_none]
56#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(default)]
58pub struct PlayReadyKid {
59    #[serde(rename = "@value")]
60    pub value: Option<String>,
61    #[serde(rename = "@ALGID")]
62    pub algid: Option<String>,
63    #[serde_as(as = "Option<Base64>")]
64    #[serde(rename = "@CHECKSUM")]
65    pub checksum: Option<Vec<u8>>,
66    #[serde_as(as = "Base64")]
67    #[serde(rename = "$text")]
68    pub content: Vec<u8>,
69}
70
71// Note that some fields overlap with the PlayReadyKid type, depending on whether we have a version
72// 4.0.0.0 header or a 4.2.0.0 header.
73#[skip_serializing_none]
74#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(default)]
76pub struct ProtectInfo {
77    #[serde(rename = "KEYLEN")]
78    pub keylen: Option<u32>,
79    #[serde(rename = "ALGID")]
80    pub algid: Option<String>,
81    #[serde(rename = "KIDS")]
82    pub kids: Vec<PlayReadyKid>,
83}
84
85
86#[serde_as]
87#[skip_serializing_none]
88#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename = "WRMDATA")]
90#[serde(default)]
91pub struct WRMData {
92    #[serde(rename = "KID")]
93    pub kids: Vec<PlayReadyKid>,
94    #[serde(rename = "PROTECTINFO")]
95    pub protect_info: Option<ProtectInfo>,
96    #[serde_as(as = "Option<Base64>")]
97    #[serde(rename = "CHECKSUM")]
98    pub checksum: Option<Vec<u8>>,
99    /// URL for license acquisition WS
100    #[serde(rename = "LA_URL")]
101    pub la_url: Option<String>,
102    /// URL for non-silent license acquisition web page
103    #[serde(rename = "LUI_URL")]
104    pub lui_url: Option<String>,
105    /// base64-encoded guid
106    #[serde(rename = "DS_ID")]
107    pub ds_id: Option<String>,
108    // These are not parsed via quick-xml, because they often contain invalid XML.
109    #[serde(rename(serialize = "CUSTOMATTRIBUTES"))]
110    pub custom_attributes: Option<String>,
111    #[serde(rename = "DECRYPTORSETUP")]
112    pub decryptor_setup: Option<String>,
113}
114
115#[skip_serializing_none]
116#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename = "WRMHEADER")]
118#[serde(default)]
119pub struct WRMHeader {
120    #[serde(rename = "@xmlns", serialize_with="serialize_xmlns")]
121    pub xmlns: Option<String>,
122    #[serde(rename = "@version")]
123    pub version: String,
124    #[serde(rename = "DATA")]
125    pub data: WRMData,
126}
127
128impl ToBytes for WRMHeader {
129    fn to_bytes(&self) -> Vec<u8> {
130        let xml = quick_xml::se::to_string(self)
131            .expect("parsing WRMHeader XML");
132        let mut out = Vec::<u8>::new();
133        for u in to_utf16(&xml) {
134            let _ = out.write_u16::<LittleEndian>(u);
135        }
136        out
137    }
138}
139
140#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TryFromPrimitive)]
141#[repr(u16)]
142pub enum PlayReadyRecordType {
143    #[default]
144    RightsManagement = 1,
145    Reserved = 2,
146    EmbeddedLicenseStore = 3,
147}
148
149impl ToBytes for PlayReadyRecordType {
150    fn to_bytes(&self) -> Vec<u8> {
151        let mut buf = Vec::new();
152        let _ = buf.write_u16::<LittleEndian>(*self as u16);
153        buf
154    }
155}
156
157#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct PlayReadyRecord {
159    pub record_type: PlayReadyRecordType,
160    pub record_value: WRMHeader,
161}
162
163impl PlayReadyRecord {
164    pub fn new() -> PlayReadyRecord {
165        let xml = "<WRMHEADER><DATA></DATA></WRMHEADER>";
166        let mut rv: WRMHeader = quick_xml::de::from_str(xml).unwrap();
167        rv.xmlns = Some(String::from("http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader"));
168        rv.version = String::from("4.0.0.0");
169        PlayReadyRecord {
170            record_type: PlayReadyRecordType::RightsManagement,
171            record_value: rv,
172        }
173    }
174}
175
176impl ToBytes for PlayReadyRecord {
177    fn to_bytes(&self) -> Vec<u8> {
178        let mut buf = Vec::new();
179        buf.append(&mut self.record_type.to_bytes());
180        let mut val_bytes = self.record_value.to_bytes();
181        let _ = buf.write_u16::<LittleEndian>(val_bytes.len().try_into().unwrap());
182        buf.append(&mut val_bytes);
183        buf
184    }
185}
186
187fn parse_playready_record(rdr: &mut Cursor<&[u8]>) -> Result<PlayReadyRecord> {
188    let record_type = rdr.read_u16::<LittleEndian>()
189        .context("reading record_type field")?;
190    if record_type != 1 {
191        return Err(anyhow!("can't parse PlayReady record of type {record_type}"));
192    }
193    let record_length = rdr.read_u16::<LittleEndian>()
194        .context("reading record_length field")?;
195    let mut wrmh_u8 = Vec::new();
196    rdr.take(record_length.into()).read_to_end(&mut wrmh_u8)?;
197    let wrmh_u16 = wrmh_u8
198        .chunks(2)
199        .map(|e| u16::from_le_bytes(e.try_into().unwrap()))
200        .collect::<Vec<_>>();
201    let mut xml = String::from_utf16(&wrmh_u16)
202        .context("decoding UTF-16")?;
203    // Extract a possible <CUSTOMATTRIBUTES>...</CUSTOMATTRIBUTES> in the input, because it tends
204    // not to contain valid XML (undeclared namespaces, in particular) and makes the XML parsing
205    // fail. We insert it as a string in the parsed struct.
206    let mut custom_attributes: Option<String> = None;
207    if let Some(start) =  xml.find("<CUSTOMATTRIBUTES") {
208        if let Some(end) = xml.find("</CUSTOMATTRIBUTES>") {
209            if end < start {
210                return Err(anyhow!("invalid CUSTOMATTRIBUTES element"));
211            }
212            if let Some(subseq) = xml.get(start..end) {
213                let ca_tag_end = subseq.find('>')
214                    .context("finding end of CUSTOMATTRIBUTES element")?;
215                let inner_start = ca_tag_end + 1;
216                trace!("start = {}, inner_start = {}", start, inner_start);
217                if let Some(inner) = subseq.get(inner_start..) {
218                    custom_attributes = Some(String::from(inner));
219                }
220                xml.replace_range(start..end + 19, "");
221            }
222        }
223    }
224    let xd = &mut quick_xml::de::Deserializer::from_str(&xml);
225    let mut wrm_header: WRMHeader = serde_path_to_error::deserialize(xd)
226        .context("parsing PlayReady XML")?;
227    wrm_header.data.custom_attributes = custom_attributes;
228    Ok(PlayReadyRecord {
229        record_type: PlayReadyRecordType::try_from(record_type)?,
230        record_value: wrm_header,
231    })
232}
233
234#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct PlayReadyPsshData {
236    pub record: Vec<PlayReadyRecord>,
237}
238
239impl PlayReadyPsshData {
240    pub fn new() -> PlayReadyPsshData {
241        let empty_record = PlayReadyRecord::new();
242        let mut empty = PlayReadyPsshData::default();
243        empty.record.push(empty_record);
244        empty
245    }
246}
247
248impl fmt::Debug for PlayReadyPsshData {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        let mut items = Vec::new();
251        for r in &self.record {
252            if r.record_type == PlayReadyRecordType::RightsManagement {
253                let xml = quick_xml::se::to_string(&r.record_value)
254                    .map_err(|_| fmt::Error)?;
255                items.push(format!("RightsManagementRecord: {xml}"));
256            } else {
257                items.push(format!("{r:?}"));
258            }
259        }
260        write!(f, "PlayReadyPsshData<{}>", items.join(", "))
261    }
262}
263
264
265impl ToBytes for PlayReadyPsshData {
266    #[allow(unused_must_use)]
267    fn to_bytes(&self) -> Vec<u8> {
268        let mut buf = Vec::<u8>::new();
269        let mut records_buf = Vec::<u8>::new();
270        for r in &self.record {
271            trace!("Serializing playready, record of length {}", r.to_bytes().len());
272            records_buf.append(&mut r.to_bytes());
273        }
274        let total_length: u32 = 4 + 2 + records_buf.len() as u32;
275        buf.write_u32::<LittleEndian>(total_length).unwrap();
276        buf.write_u16::<LittleEndian>(self.record.len().try_into().unwrap()).unwrap();
277        buf.append(&mut records_buf);
278        buf
279    }
280}
281
282pub fn parse_pssh_data(buf: &[u8]) -> Result<PlayReadyPsshData> {
283    let mut rdr = Cursor::new(buf);
284    let blen = buf.len() as u32;
285    let length = rdr.read_u32::<LittleEndian>()
286        .context("reading pssh data length")?;
287    if length != blen {
288        return Err(anyhow!("header length {length} different from buffer length {blen}"));
289    }
290    let record_count = rdr.read_u16::<LittleEndian>()
291        .context("reading pssh data record count")?;
292    let mut records = Vec::new();
293    for _ in 1..=record_count {
294        records.push(parse_playready_record(&mut rdr)?);
295    }
296    Ok(PlayReadyPsshData {
297        record: records,
298    })
299}