1use crate::error::{Error, Result};
32use crate::types::DrmSystem;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use url::Url;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PsshBox {
40 pub system_id: String,
42 pub key_ids: Vec<String>,
44 pub data: String,
46}
47
48impl PsshBox {
49 pub fn new(system_id: &str, data: &[u8]) -> Self {
51 Self {
52 system_id: system_id.to_string(),
53 key_ids: Vec::new(),
54 data: base64_encode(data),
55 }
56 }
57
58 pub fn drm_system(&self) -> Option<DrmSystem> {
60 match self.system_id.to_lowercase().as_str() {
61 "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" => Some(DrmSystem::Widevine),
62 "94ce86fb-07ff-4f43-adb8-93d2fa968ca2" => Some(DrmSystem::FairPlay),
63 "9a04f079-9840-4286-ab92-e65be0885f95" => Some(DrmSystem::PlayReady),
64 "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b" => Some(DrmSystem::ClearKey),
65 _ => None,
66 }
67 }
68
69 pub fn data_bytes(&self) -> Result<Vec<u8>> {
71 base64_decode(&self.data)
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DrmConfig {
78 pub widevine_license_url: Option<Url>,
80 pub playready_license_url: Option<Url>,
82 pub fairplay_certificate_url: Option<Url>,
84 pub fairplay_license_url: Option<Url>,
86 pub license_headers: HashMap<String, String>,
88 pub fairplay_content_id: Option<String>,
90 pub clearkey_keys: HashMap<String, String>,
92 pub persist_license: bool,
94 pub license_duration: u64,
96}
97
98impl Default for DrmConfig {
99 fn default() -> Self {
100 Self {
101 widevine_license_url: None,
102 playready_license_url: None,
103 fairplay_certificate_url: None,
104 fairplay_license_url: None,
105 license_headers: HashMap::new(),
106 fairplay_content_id: None,
107 clearkey_keys: HashMap::new(),
108 persist_license: false,
109 license_duration: 0,
110 }
111 }
112}
113
114impl DrmConfig {
115 pub fn widevine(license_url: Url) -> Self {
117 Self {
118 widevine_license_url: Some(license_url),
119 ..Default::default()
120 }
121 }
122
123 pub fn fairplay(license_url: Url, certificate_url: Url) -> Self {
125 Self {
126 fairplay_license_url: Some(license_url),
127 fairplay_certificate_url: Some(certificate_url),
128 ..Default::default()
129 }
130 }
131
132 pub fn clearkey(keys: HashMap<String, String>) -> Self {
134 Self {
135 clearkey_keys: keys,
136 ..Default::default()
137 }
138 }
139
140 pub fn with_header(mut self, key: &str, value: &str) -> Self {
142 self.license_headers.insert(key.to_string(), value.to_string());
143 self
144 }
145
146 pub fn is_configured(&self) -> bool {
148 self.widevine_license_url.is_some()
149 || self.playready_license_url.is_some()
150 || self.fairplay_license_url.is_some()
151 || !self.clearkey_keys.is_empty()
152 }
153
154 pub fn supported_systems(&self) -> Vec<DrmSystem> {
156 let mut systems = Vec::new();
157 if self.widevine_license_url.is_some() {
158 systems.push(DrmSystem::Widevine);
159 }
160 if self.playready_license_url.is_some() {
161 systems.push(DrmSystem::PlayReady);
162 }
163 if self.fairplay_license_url.is_some() {
164 systems.push(DrmSystem::FairPlay);
165 }
166 if !self.clearkey_keys.is_empty() {
167 systems.push(DrmSystem::ClearKey);
168 }
169 systems
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct LicenseRequest {
176 pub system: DrmSystem,
178 pub challenge: Vec<u8>,
180 pub license_url: Url,
182 pub headers: HashMap<String, String>,
184}
185
186#[derive(Debug, Clone)]
188pub struct LicenseResponse {
189 pub system: DrmSystem,
191 pub license: Vec<u8>,
193 pub expiration: u64,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum DrmSessionState {
200 Idle,
202 AwaitingCertificate,
204 GeneratingChallenge,
206 AwaitingLicense,
208 Ready,
210 Expired,
212 Error,
214}
215
216#[derive(Debug, Clone)]
218pub struct DrmSession {
219 pub id: String,
221 pub system: DrmSystem,
223 pub state: DrmSessionState,
225 pub key_ids: Vec<String>,
227 pub expiration: u64,
229 pub error: Option<String>,
231}
232
233impl DrmSession {
234 pub fn new(system: DrmSystem) -> Self {
236 Self {
237 id: uuid::Uuid::new_v4().to_string(),
238 system,
239 state: DrmSessionState::Idle,
240 key_ids: Vec::new(),
241 expiration: 0,
242 error: None,
243 }
244 }
245
246 pub fn is_ready(&self) -> bool {
248 self.state == DrmSessionState::Ready
249 }
250
251 pub fn is_expired(&self) -> bool {
253 if self.expiration == 0 {
254 return false;
255 }
256 let now = std::time::SystemTime::now()
257 .duration_since(std::time::UNIX_EPOCH)
258 .map(|d| d.as_secs())
259 .unwrap_or(0);
260 now >= self.expiration
261 }
262}
263
264pub struct DrmManager {
266 config: DrmConfig,
267 sessions: HashMap<String, DrmSession>,
268 pssh_boxes: Vec<PsshBox>,
269}
270
271impl DrmManager {
272 pub fn new(config: DrmConfig) -> Self {
274 Self {
275 config,
276 sessions: HashMap::new(),
277 pssh_boxes: Vec::new(),
278 }
279 }
280
281 pub fn set_pssh_boxes(&mut self, boxes: Vec<PsshBox>) {
283 self.pssh_boxes = boxes;
284 }
285
286 pub fn get_pssh(&self, system: DrmSystem) -> Option<&PsshBox> {
288 let target_id = system.system_id().to_lowercase();
289 self.pssh_boxes.iter().find(|p| p.system_id.to_lowercase() == target_id)
290 }
291
292 pub fn create_widevine_request(&self, challenge: Vec<u8>) -> Result<LicenseRequest> {
294 let license_url = self.config.widevine_license_url.clone()
295 .ok_or_else(|| Error::drm("Widevine license URL not configured"))?;
296
297 Ok(LicenseRequest {
298 system: DrmSystem::Widevine,
299 challenge,
300 license_url,
301 headers: self.config.license_headers.clone(),
302 })
303 }
304
305 pub fn create_fairplay_request(&self, spc: Vec<u8>) -> Result<LicenseRequest> {
307 let license_url = self.config.fairplay_license_url.clone()
308 .ok_or_else(|| Error::drm("FairPlay license URL not configured"))?;
309
310 Ok(LicenseRequest {
311 system: DrmSystem::FairPlay,
312 challenge: spc,
313 license_url,
314 headers: self.config.license_headers.clone(),
315 })
316 }
317
318 pub fn get_clearkey_license(&self) -> Result<LicenseResponse> {
320 if self.config.clearkey_keys.is_empty() {
321 return Err(Error::drm("No ClearKey keys configured"));
322 }
323
324 let keys: Vec<serde_json::Value> = self.config.clearkey_keys.iter()
326 .map(|(kid, key)| {
327 serde_json::json!({
328 "kty": "oct",
329 "kid": kid,
330 "k": key,
331 })
332 })
333 .collect();
334
335 let license_json = serde_json::json!({
336 "keys": keys,
337 "type": "temporary",
338 });
339
340 Ok(LicenseResponse {
341 system: DrmSystem::ClearKey,
342 license: license_json.to_string().into_bytes(),
343 expiration: 0,
344 })
345 }
346
347 pub fn create_session(&mut self, system: DrmSystem) -> &DrmSession {
349 let session = DrmSession::new(system);
350 let id = session.id.clone();
351 self.sessions.insert(id.clone(), session);
352 self.sessions.get(&id).unwrap()
353 }
354
355 pub fn process_license(&mut self, session_id: &str, response: LicenseResponse) -> Result<()> {
357 let session = self.sessions.get_mut(session_id)
358 .ok_or_else(|| Error::drm("Session not found"))?;
359
360 session.state = DrmSessionState::Ready;
361 session.expiration = response.expiration;
362
363 Ok(())
364 }
365
366 pub fn sessions(&self) -> impl Iterator<Item = &DrmSession> {
368 self.sessions.values()
369 }
370
371 pub fn get_session(&self, id: &str) -> Option<&DrmSession> {
373 self.sessions.get(id)
374 }
375
376 pub fn close_session(&mut self, id: &str) {
378 self.sessions.remove(id);
379 }
380
381 pub fn close_all_sessions(&mut self) {
383 self.sessions.clear();
384 }
385
386 pub fn is_drm_required(&self) -> bool {
388 !self.pssh_boxes.is_empty()
389 }
390
391 pub fn select_drm_system(&self) -> Option<DrmSystem> {
393 let supported = self.config.supported_systems();
394
395 for system in &[DrmSystem::Widevine, DrmSystem::FairPlay, DrmSystem::PlayReady, DrmSystem::ClearKey] {
397 if supported.contains(system) && self.get_pssh(*system).is_some() {
398 return Some(*system);
399 }
400 }
401
402 if !self.config.clearkey_keys.is_empty() {
404 return Some(DrmSystem::ClearKey);
405 }
406
407 None
408 }
409}
410
411fn base64_encode(data: &[u8]) -> String {
413 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
414
415 let mut result = String::new();
416 for chunk in data.chunks(3) {
417 let b = match chunk.len() {
418 3 => [chunk[0], chunk[1], chunk[2], 0],
419 2 => [chunk[0], chunk[1], 0, 0],
420 1 => [chunk[0], 0, 0, 0],
421 _ => continue,
422 };
423
424 let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | (b[2] as u32);
425
426 result.push(ALPHABET[((n >> 18) & 0x3F) as usize] as char);
427 result.push(ALPHABET[((n >> 12) & 0x3F) as usize] as char);
428 result.push(if chunk.len() > 1 { ALPHABET[((n >> 6) & 0x3F) as usize] as char } else { '=' });
429 result.push(if chunk.len() > 2 { ALPHABET[(n & 0x3F) as usize] as char } else { '=' });
430 }
431 result
432}
433
434fn base64_decode(data: &str) -> Result<Vec<u8>> {
435 const DECODE_TABLE: &[i8; 128] = &[
436 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
437 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
438 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
439 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
440 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
441 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
442 -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
443 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
444 ];
445
446 let input: Vec<u8> = data.bytes()
447 .filter(|b| *b != b'=' && *b != b'\n' && *b != b'\r')
448 .collect();
449
450 let mut result = Vec::with_capacity(input.len() * 3 / 4);
451
452 for chunk in input.chunks(4) {
453 let mut n: u32 = 0;
454 let chunk_len = chunk.len();
455
456 for (i, &b) in chunk.iter().enumerate() {
457 if b as usize >= 128 {
458 return Err(Error::drm("Invalid base64 character"));
459 }
460 let val = DECODE_TABLE[b as usize];
461 if val < 0 {
462 return Err(Error::drm("Invalid base64 character"));
463 }
464 n |= (val as u32) << (18 - i * 6);
465 }
466
467 result.push((n >> 16) as u8);
469 if chunk_len > 2 {
470 result.push((n >> 8) as u8);
471 }
472 if chunk_len > 3 {
473 result.push(n as u8);
474 }
475 }
476
477 Ok(result)
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_drm_config() {
486 let config = DrmConfig::default();
487 assert!(!config.is_configured());
488
489 let config = DrmConfig::widevine(Url::parse("https://license.example.com").unwrap());
490 assert!(config.is_configured());
491 assert!(config.supported_systems().contains(&DrmSystem::Widevine));
492 }
493
494 #[test]
495 fn test_pssh_box() {
496 let pssh = PsshBox::new(DrmSystem::Widevine.system_id(), b"test data");
497 assert_eq!(pssh.drm_system(), Some(DrmSystem::Widevine));
498 }
499
500 #[test]
501 fn test_base64_roundtrip() {
502 let original = b"Hello, DRM!";
503 let encoded = base64_encode(original);
504 let decoded = base64_decode(&encoded).unwrap();
505 assert_eq!(original.to_vec(), decoded);
506 }
507
508 #[test]
509 fn test_clearkey_license() {
510 let mut keys = HashMap::new();
511 keys.insert("abc123".to_string(), "key456".to_string());
512
513 let config = DrmConfig::clearkey(keys);
514 let manager = DrmManager::new(config);
515
516 let license = manager.get_clearkey_license().unwrap();
517 assert_eq!(license.system, DrmSystem::ClearKey);
518 }
519}