Skip to main content

playready/
revocation_list.rs

1//! Parsing and creating revocation lists.
2
3use crate::{binary_format, xml_utils};
4use base64::{Engine, prelude::BASE64_STANDARD};
5use binrw::{BinRead, BinWrite};
6use byteorder::{LittleEndian, ReadBytesExt};
7use std::{
8    fs::File,
9    io::{Cursor, Read},
10    path::Path,
11};
12
13/// List ID.
14pub type ListId = [u8; 16];
15
16/// Represents single revocation list info.
17#[derive(Debug, Clone, Default)]
18pub struct RevocationListInfo {
19    list_id: ListId,
20    version: u32,
21}
22
23impl RevocationListInfo {
24    /// Creates new revocation list info.
25    pub fn new(list_id: ListId, version: u32) -> Self {
26        Self { list_id, version }
27    }
28
29    /// Gets list ID.
30    pub fn list_id(&self) -> &ListId {
31        &self.list_id
32    }
33
34    /// Gets current version of list.
35    pub fn version(&self) -> u32 {
36        self.version
37    }
38}
39
40/// Represents collection of revocation list infos which can be sent in license challenge.
41#[derive(Debug, Clone, Default)]
42pub struct RevocationListInfos {
43    inner: Vec<RevocationListInfo>,
44}
45
46impl RevocationListInfos {
47    const PLAYREADY_RUNTIME_ID: ListId = [
48        0x8a, 0x8c, 0x9d, 0x4e, 0x52, 0xb6, 0xa7, 0x45, 0x97, 0x91, 0x69, 0x25, 0xa6, 0xb4, 0x79,
49        0x1f,
50    ];
51
52    const PLAYREADY_APPLICATION_ID: ListId = [
53        0x80, 0x2e, 0x08, 0x28, 0xa3, 0xc7, 0xb1, 0x40, 0x82, 0x56, 0x19, 0xe5, 0xb6, 0xd8, 0x9b,
54        0x27,
55    ];
56
57    const REV_INFO_V2_ID: ListId = [
58        0x11, 0xff, 0xd1, 0x52, 0x88, 0xd3, 0xdd, 0x4e, 0x82, 0xb7, 0x68, 0xea, 0x4c, 0x20, 0xa1,
59        0x6c,
60    ];
61
62    const WMDRMNET_ID: ListId = [
63        0x04, 0xe6, 0x75, 0xcd, 0x3d, 0x54, 0x9c, 0x4a, 0x9f, 0x09, 0xfe, 0x6d, 0x24, 0xe8, 0xbf,
64        0x90,
65    ];
66
67    /// Returns an initial revocation list infos (if no other are available).
68    pub fn initial() -> Self {
69        Self {
70            inner: vec![
71                RevocationListInfo::new(RevocationListInfos::PLAYREADY_RUNTIME_ID, 0),
72                RevocationListInfo::new(RevocationListInfos::PLAYREADY_APPLICATION_ID, 0),
73                RevocationListInfo::new(RevocationListInfos::REV_INFO_V2_ID, 0),
74                RevocationListInfo::new(RevocationListInfos::WMDRMNET_ID, 0),
75            ],
76        }
77    }
78
79    /// Returns an iterator to revocation list infos.
80    pub fn iter(&self) -> impl Iterator<Item = &RevocationListInfo> {
81        self.inner.iter()
82    }
83
84    /// Creates revocation list infos from revocation lists.
85    pub fn from_revocation_lists(rev_lists: &RevocationLists) -> Self {
86        let mut inner = Vec::with_capacity(rev_lists.inner.len());
87
88        for rev_list in &rev_lists.inner {
89            match rev_list.list_id {
90                Self::PLAYREADY_RUNTIME_ID | Self::PLAYREADY_APPLICATION_ID => {
91                    let Ok(list) = binary_format::revocation_list::BPrRLSigned::read(
92                        &mut Cursor::new(&rev_list.list_data),
93                    )
94                    .inspect_err(|e| log::error!("Failed to parse BPrRLSigned: {e:?}")) else {
95                        continue;
96                    };
97
98                    inner.push(RevocationListInfo::new(rev_list.list_id, list.data.version));
99                }
100                Self::REV_INFO_V2_ID => {
101                    let Ok(list) = binary_format::revocation_list::BRevInfoSigned::read(
102                        &mut Cursor::new(&rev_list.list_data),
103                    )
104                    .inspect_err(|e| log::error!("Failed to parse BRevInfoSigned: {e:?}")) else {
105                        continue;
106                    };
107
108                    inner.push(RevocationListInfo::new(
109                        Self::REV_INFO_V2_ID,
110                        list.data.sequence_number,
111                    ));
112                }
113                Self::WMDRMNET_ID => {
114                    let mut xml_text = vec![0u16; rev_list.list_data.len() / 2];
115
116                    if Cursor::new(&rev_list.list_data)
117                        .read_u16_into::<LittleEndian>(&mut xml_text)
118                        .inspect_err(|e| log::error!("Failed to read WMDRMNET string: {e:?}"))
119                        .is_err()
120                    {
121                        continue;
122                    }
123
124                    let Ok(mut xml_text) = String::from_utf16(&xml_text) else {
125                        continue;
126                    };
127
128                    xml_text.insert_str(0, "<DUMMY>"); // roxmltree expects only one root element so insert a dummy one
129                    xml_text.push_str("</DUMMY>");
130
131                    let Ok(Some(template)) = xml_utils::parse_wmdrmnet_list_data(&xml_text)
132                        .inspect_err(|e| log::error!("Failed to parse WMDRMNET list data: {e:?}"))
133                    else {
134                        continue;
135                    };
136
137                    let Ok(template) = BASE64_STANDARD
138                        .decode(template)
139                        .map_err(|e| log::error!("Failed to decode list data: {e:?}"))
140                    else {
141                        continue;
142                    };
143
144                    let Ok(list) = binary_format::revocation_list::WMDRMNETSigned::read(
145                        &mut Cursor::new(template),
146                    )
147                    .inspect_err(|e| log::error!("Failed to parse WMDRMNETSigned: {e:?}")) else {
148                        continue;
149                    };
150
151                    inner.push(RevocationListInfo::new(
152                        Self::WMDRMNET_ID,
153                        list.data.version,
154                    ));
155                }
156                _ => {}
157            }
158        }
159
160        Self { inner }
161    }
162
163    /// Constructs new [`RevocationListInfos`] by deserializing bytes in PRL format.
164    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::Error> {
165        let prl = binary_format::revocation_list::PrlRevListInfos::read(&mut Cursor::new(bytes))?;
166
167        let inner = prl
168            .rev_list_infos
169            .iter()
170            .map(|li| RevocationListInfo::new(li.list_id, li.version))
171            .collect();
172
173        Ok(Self { inner })
174    }
175
176    /// Constructs new [`RevocationListInfos`] by deserializing bytes from prl file.
177    pub fn from_prl(path: impl AsRef<Path>) -> Result<Self, crate::Error> {
178        let mut file = File::open(path)?;
179        let mut bytes = Vec::<u8>::new();
180        file.read_to_end(&mut bytes)?;
181
182        Self::from_bytes(&bytes)
183    }
184
185    /// Writes infos to the prl file.
186    pub fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), crate::Error> {
187        let mut file = File::create(path)?;
188
189        let rev_list_infos: Vec<_> = self
190            .inner
191            .iter()
192            .map(|li| binary_format::revocation_list::PrlRevListInfo {
193                list_id: li.list_id,
194                version: li.version,
195            })
196            .collect();
197
198        let rev_list_infos = binary_format::revocation_list::PrlRevListInfos {
199            list_count: u32::try_from(rev_list_infos.len()).unwrap(),
200            rev_list_infos,
201        };
202
203        rev_list_infos.write(&mut file)?;
204
205        Ok(())
206    }
207
208    /// Updates current revocation lists with the new ones specified in `other` argument.
209    ///
210    /// Returns `true` if at least one of the items has been updated.
211    pub fn update(&mut self, other: &Self) -> bool {
212        let mut updated = false;
213
214        for other_list in &other.inner {
215            if let Some(current_list) = self
216                .inner
217                .iter_mut()
218                .find(|r| r.list_id == other_list.list_id)
219            {
220                current_list.version = other_list.version;
221                updated = true;
222            } else {
223                self.inner.push(RevocationListInfo::new(
224                    other_list.list_id,
225                    other_list.version,
226                ));
227                updated = true;
228            }
229        }
230
231        updated
232    }
233}
234
235/// Represents single revocation list.
236#[derive(Debug, Clone, Default)]
237pub struct RevocationList {
238    list_id: ListId,
239    list_data: Vec<u8>,
240}
241
242/// Represents collection of revocation lists which can be retrieved from the challenge response.
243#[derive(Debug, Clone, Default)]
244pub struct RevocationLists {
245    inner: Vec<RevocationList>,
246}
247
248impl RevocationLists {
249    /// Constructs revocation lists from an XML string.
250    pub fn from_xml_str(text: &str) -> Result<Self, crate::Error> {
251        let parsed = xml_utils::parse_revocation_lists(text)?;
252        let mut inner = Vec::with_capacity(parsed.len());
253
254        for (list_id, list_data) in parsed {
255            let list_id_vec = BASE64_STANDARD.decode(list_id)?;
256            let list_data = BASE64_STANDARD.decode(list_data)?;
257
258            let mut list_id = ListId::default();
259            if list_id_vec.len() != list_id.len() {
260                log::error!("Wrong length of list id for {list_id_vec:?}");
261                continue;
262            }
263            list_id.copy_from_slice(&list_id_vec);
264
265            inner.push(RevocationList { list_id, list_data });
266        }
267
268        Ok(Self { inner })
269    }
270
271    /// Returns `true` if [`RevocationLists`] does not contain any entry.
272    pub fn is_empty(&self) -> bool {
273        self.inner.is_empty()
274    }
275
276    /// Returns revoked public key digests.
277    pub fn get_revoked_public_key_digests(&self) -> Result<RevokedPublicKeyDigests, crate::Error> {
278        let rev_list = self
279            .inner
280            .iter()
281            .find(|r| r.list_id == RevocationListInfos::PLAYREADY_RUNTIME_ID);
282
283        let Some(rev_list) = rev_list else {
284            return Ok(RevokedPublicKeyDigests(Vec::new()));
285        };
286
287        let rev_list = binary_format::revocation_list::BPrRLSigned::read(&mut Cursor::new(
288            &rev_list.list_data,
289        ))?;
290
291        Ok(RevokedPublicKeyDigests(rev_list.data.revocation_entries))
292    }
293}
294
295/// Represents public key digests that have been revoked.
296pub struct RevokedPublicKeyDigests(Vec<[u8; 32]>);
297
298impl RevokedPublicKeyDigests {
299    /// Returns `true` if `key` is present in digest set, false otherwise.
300    pub fn contains(&self, key: &[u8; 32]) -> bool {
301        self.0.contains(key)
302    }
303}
304
305#[cfg(test)]
306mod test {
307    use crate::{revocation_list::RevocationListInfos, revocation_list::RevocationLists};
308    use path_macro::path;
309    use std::{fs::File, io::Read};
310
311    fn read_rev_lists_from_file(file_name: &str) -> RevocationLists {
312        let path = path!(env!("CARGO_MANIFEST_DIR") / "testfiles" / file_name);
313        let mut file = File::open(path).unwrap();
314        let mut xml_str = String::new();
315        file.read_to_string(&mut xml_str).unwrap();
316
317        RevocationLists::from_xml_str(&xml_str).unwrap()
318    }
319
320    #[test]
321    fn test_update_from_initial_to_86() {
322        let mut infos = RevocationListInfos::initial();
323        let expected_versions = [0, 0, 0, 0];
324        for (info, expected_version) in infos.iter().zip(expected_versions) {
325            assert_eq!(info.version(), expected_version);
326        }
327
328        let rev_lists = read_rev_lists_from_file("RevInfo2v86_20260121.xml");
329        let infos_update = RevocationListInfos::from_revocation_lists(&rev_lists);
330        infos.update(&infos_update);
331        let expected_versions = [21, 11, 86, 12];
332
333        for (info, expected_version) in infos.iter().zip(expected_versions) {
334            assert_eq!(info.version(), expected_version);
335        }
336    }
337
338    #[test]
339    fn test_update_from_86_to_87() {
340        let rev_lists = read_rev_lists_from_file("RevInfo2v86_20260121.xml");
341        let mut infos = RevocationListInfos::from_revocation_lists(&rev_lists);
342        let expected_versions = [86, 21, 11, 12];
343        for (info, expected_version) in infos.iter().zip(expected_versions) {
344            assert_eq!(info.version(), expected_version);
345        }
346
347        let rev_lists = read_rev_lists_from_file("RevInfo2v87_20260316.xml");
348        let infos_update = RevocationListInfos::from_revocation_lists(&rev_lists);
349        infos.update(&infos_update);
350        let expected_versions = [87, 22, 11, 12];
351
352        for (info, expected_version) in infos.iter().zip(expected_versions) {
353            assert_eq!(info.version(), expected_version);
354        }
355    }
356}