Skip to main content

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