1use crate::{ContentKey, Scheme};
17use aes::Aes128;
18use aes::cipher::{BlockCipherEncrypt, KeyInit};
19
20const COMMON_SYSTEM_ID: [u8; 16] = [
22 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b,
23];
24const WIDEVINE_SYSTEM_ID: [u8; 16] = [
26 0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed,
27];
28const PLAYREADY_SYSTEM_ID: [u8; 16] = [
30 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
31];
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ProtectionSystem {
36 Common,
38 Widevine,
40 PlayReady,
42}
43
44impl ProtectionSystem {
45 pub fn parse(name: &str) -> Option<Self> {
47 match name.trim().to_ascii_lowercase().as_str() {
48 "common" | "commonsystem" => Some(Self::Common),
49 "widevine" => Some(Self::Widevine),
50 "playready" => Some(Self::PlayReady),
51 _ => None,
52 }
53 }
54
55 pub fn system_id(self) -> [u8; 16] {
57 match self {
58 Self::Common => COMMON_SYSTEM_ID,
59 Self::Widevine => WIDEVINE_SYSTEM_ID,
60 Self::PlayReady => PLAYREADY_SYSTEM_ID,
61 }
62 }
63
64 pub fn pssh_box(self, key: &ContentKey, scheme: Scheme) -> Vec<u8> {
67 match self {
68 Self::Common => assemble(self.system_id(), 1, &[key.kid], &[]),
70 Self::Widevine => assemble(self.system_id(), 0, &[], &widevine_data(key, scheme)),
71 Self::PlayReady => assemble(self.system_id(), 0, &[], &playready_data(key, scheme)),
72 }
73 }
74}
75
76fn assemble(system_id: [u8; 16], version: u8, kids: &[[u8; 16]], data: &[u8]) -> Vec<u8> {
78 let mut body = Vec::new();
79 body.extend_from_slice(b"pssh");
80 body.push(version);
81 body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&system_id);
83 if version >= 1 {
84 body.extend_from_slice(&(kids.len() as u32).to_be_bytes());
85 for kid in kids {
86 body.extend_from_slice(kid);
87 }
88 }
89 body.extend_from_slice(&(data.len() as u32).to_be_bytes());
90 body.extend_from_slice(data);
91
92 let mut out = ((body.len() + 4) as u32).to_be_bytes().to_vec();
93 out.extend_from_slice(&body);
94 out
95}
96
97fn widevine_data(key: &ContentKey, scheme: Scheme) -> Vec<u8> {
100 let mut data = Vec::new();
101 data.push(0x12); data.push(0x10); data.extend_from_slice(&key.kid);
104 data.push(0x48); put_varint(u32::from_be_bytes(scheme.scheme_type()), &mut data);
106 data
107}
108
109fn put_varint(mut value: u32, out: &mut Vec<u8>) {
111 loop {
112 let byte = (value & 0x7f) as u8;
113 value >>= 7;
114 if value == 0 {
115 out.push(byte);
116 return;
117 }
118 out.push(byte | 0x80);
119 }
120}
121
122fn playready_data(key: &ContentKey, scheme: Scheme) -> Vec<u8> {
124 let kid = playready_guid(&key.kid);
125 let kid_b64 = base64(&kid);
126 let checksum_b64 = base64(&playready_checksum(&key.key, &kid));
127 let algid = if scheme.is_cbc() { "AESCBC" } else { "AESCTR" };
129
130 let xml = format!(
131 "<WRMHEADER xmlns=\"http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader\" \
132 version=\"4.0.0.0\"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>{algid}</ALGID>\
133 </PROTECTINFO><KID>{kid_b64}</KID><CHECKSUM>{checksum_b64}</CHECKSUM></DATA></WRMHEADER>"
134 );
135 let header: Vec<u8> = xml.encode_utf16().flat_map(u16::to_le_bytes).collect();
136
137 let record_len = header.len() as u16;
139 let total_len = 4 + 2 + 2 + 2 + header.len();
141 let mut obj = Vec::with_capacity(total_len);
142 obj.extend_from_slice(&(total_len as u32).to_le_bytes());
143 obj.extend_from_slice(&1u16.to_le_bytes()); obj.extend_from_slice(&1u16.to_le_bytes()); obj.extend_from_slice(&record_len.to_le_bytes());
146 obj.extend_from_slice(&header);
147 obj
148}
149
150fn playready_guid(kid: &[u8; 16]) -> [u8; 16] {
153 let mut g = *kid;
154 g.swap(0, 3);
155 g.swap(1, 2);
156 g.swap(4, 5);
157 g.swap(6, 7);
158 g
159}
160
161fn playready_checksum(key: &[u8; 16], guid_kid: &[u8; 16]) -> [u8; 8] {
164 let cipher = Aes128::new_from_slice(key).expect("AES-128 key is 16 bytes");
165 let mut block = (*guid_kid).into();
166 cipher.encrypt_block(&mut block);
167 block[..8].try_into().unwrap()
168}
169
170fn base64(input: &[u8]) -> String {
172 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
173 let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
174 for chunk in input.chunks(3) {
175 let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
176 let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
177 out.push(ALPHABET[(n >> 18 & 0x3f) as usize] as char);
178 out.push(ALPHABET[(n >> 12 & 0x3f) as usize] as char);
179 out.push(if chunk.len() > 1 { ALPHABET[(n >> 6 & 0x3f) as usize] as char } else { '=' });
180 out.push(if chunk.len() > 2 { ALPHABET[(n & 0x3f) as usize] as char } else { '=' });
181 }
182 out
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 const KID: [u8; 16] = [
191 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
192 0xff,
193 ];
194 const KEY: [u8; 16] = [
195 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
196 0xff,
197 ];
198
199 fn key() -> ContentKey {
200 ContentKey { kid: KID, key: KEY }
201 }
202
203 fn hex(bytes: &[u8]) -> String {
204 bytes.iter().map(|b| format!("{b:02x}")).collect()
205 }
206
207 #[test]
208 fn base64_matches_known_vectors() {
209 assert_eq!(base64(b""), "");
210 assert_eq!(base64(b"f"), "Zg==");
211 assert_eq!(base64(b"fo"), "Zm8=");
212 assert_eq!(base64(b"foo"), "Zm9v");
213 assert_eq!(base64(b"foobar"), "Zm9vYmFy");
214 }
215
216 #[test]
217 fn common_pssh_matches_shaka() {
218 let got = ProtectionSystem::Common.pssh_box(&key(), Scheme::Cenc);
219 assert_eq!(
220 hex(&got),
221 "0000003470737368010000001077efecc0b24d02ace33c1e52e2fb4b\
222 0000000111223344556677889900aabbccddeeff00000000"
223 );
224 }
225
226 #[test]
227 fn widevine_pssh_matches_shaka() {
228 let got = ProtectionSystem::Widevine.pssh_box(&key(), Scheme::Cenc);
229 assert_eq!(
230 hex(&got),
231 "000000387073736800000000edef8ba979d64acea3c827dcd51d21ed\
232 00000018121011223344556677889900aabbccddeeff48e3dc959b06"
233 );
234 }
235
236 #[test]
237 fn playready_pssh_matches_shaka() {
238 let got = ProtectionSystem::PlayReady.pssh_box(&key(), Scheme::Cenc);
239 let expected = concat!(
242 "0000022670737368000000009a04f07998404286ab92e65be0885f9500000206060200000100010",
243 "0fc013c00570052004d00480045004100440045005200200078006d006c006e0073003d002200680",
244 "07400740070003a002f002f0073006300680065006d00610073002e006d006900630072006f0073",
245 "006f00660074002e0063006f006d002f00440052004d002f00320030003000370",
246 "02f00300033002f0050006c0061007900520065006100640079004800650061006",
247 "4006500720022002000760065007200730069006f006e003d00220034002e0030",
248 "002e0030002e00300022003e003c0044004100540041003e003c00500052004f005",
249 "40045004300540049004e0046004f003e003c004b00450059004c0045004e003e0",
250 "0310036003c002f004b00450059004c0045004e003e003c0041004c00470049004",
251 "4003e004100450053004300540052003c002f0041004c004700490044003e003c0",
252 "02f00500052004f00540045004300540049004e0046004f003e003c004b0049004",
253 "4003e00520044004d006900450057005a0056006900480065005a0041004b00710",
254 "037007a004e00330075002f0077003d003d003c002f004b00490044003e003c004",
255 "3004800450043004b00530055004d003e00350041006100550053004600700056",
256 "004800640030003d003c002f0043004800450043004b00530055004d003e003c00",
257 "2f0044004100540041003e003c002f00570052004d004800450041004400450052003e00",
258 );
259 assert_eq!(hex(&got), expected);
260 }
261
262 #[test]
263 fn widevine_protection_scheme_tracks_fourcc() {
264 let cenc = ProtectionSystem::Widevine.pssh_box(&key(), Scheme::Cenc);
268 let cbcs = ProtectionSystem::Widevine.pssh_box(&key(), Scheme::Cbcs);
269 assert_ne!(cenc, cbcs);
270 let kid_prefix = hex(&[&[0x12, 0x10][..], &KID].concat());
271 assert!(hex(&cenc).contains(&kid_prefix) && hex(&cbcs).contains(&kid_prefix));
272 }
273
274 #[test]
275 fn parse_is_case_insensitive() {
276 assert_eq!(ProtectionSystem::parse("Widevine"), Some(ProtectionSystem::Widevine));
277 assert_eq!(ProtectionSystem::parse("PLAYREADY"), Some(ProtectionSystem::PlayReady));
278 assert_eq!(ProtectionSystem::parse("commonsystem"), Some(ProtectionSystem::Common));
279 assert_eq!(ProtectionSystem::parse("nope"), None);
280 }
281}