Skip to main content

kino_core/
drm.rs

1//! DRM - Digital Rights Management module
2//!
3//! Provides support for:
4//! - Widevine (Chrome, Android, Chromecast)
5//! - FairPlay (Safari, iOS, tvOS)
6//! - PlayReady (Edge, Windows)
7//! - ClearKey (Open standard for testing)
8//!
9//! # Architecture
10//!
11//! ```text
12//! ┌─────────────────────────────────────────────────────┐
13//! │                    DRM Manager                       │
14//! ├─────────────────────────────────────────────────────┤
15//! │                                                     │
16//! │  ┌──────────┐  ┌──────────┐  ┌──────────┐         │
17//! │  │ Widevine │  │ FairPlay │  │ PlayReady│         │
18//! │  │  Client  │  │  Client  │  │  Client  │         │
19//! │  └────┬─────┘  └────┬─────┘  └────┬─────┘         │
20//! │       │             │             │                │
21//! │       └─────────────┼─────────────┘                │
22//! │                     │                              │
23//! │              ┌──────┴──────┐                       │
24//! │              │ License     │                       │
25//! │              │ Server      │                       │
26//! │              │ Protocol    │                       │
27//! │              └─────────────┘                       │
28//! └─────────────────────────────────────────────────────┘
29//! ```
30
31use crate::error::{Error, Result};
32use crate::types::DrmSystem;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use url::Url;
36
37/// PSSH (Protection System Specific Header) box data
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PsshBox {
40    /// DRM system ID (UUID)
41    pub system_id: String,
42    /// Key IDs contained in this PSSH
43    pub key_ids: Vec<String>,
44    /// Raw PSSH data (base64 encoded)
45    pub data: String,
46}
47
48impl PsshBox {
49    /// Create a new PSSH box
50    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    /// Parse system ID to DRM type
59    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    /// Get raw data as bytes
70    pub fn data_bytes(&self) -> Result<Vec<u8>> {
71        base64_decode(&self.data)
72    }
73}
74
75/// DRM configuration for a content item
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DrmConfig {
78    /// License server URL for Widevine
79    pub widevine_license_url: Option<Url>,
80    /// License server URL for PlayReady
81    pub playready_license_url: Option<Url>,
82    /// Certificate URL for FairPlay
83    pub fairplay_certificate_url: Option<Url>,
84    /// License server URL for FairPlay
85    pub fairplay_license_url: Option<Url>,
86    /// Custom headers for license requests
87    pub license_headers: HashMap<String, String>,
88    /// Content ID for FairPlay
89    pub fairplay_content_id: Option<String>,
90    /// ClearKey keys (key_id -> key mapping)
91    pub clearkey_keys: HashMap<String, String>,
92    /// Whether to persist licenses
93    pub persist_license: bool,
94    /// License duration in seconds (0 = forever)
95    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    /// Create a Widevine-only configuration
116    pub fn widevine(license_url: Url) -> Self {
117        Self {
118            widevine_license_url: Some(license_url),
119            ..Default::default()
120        }
121    }
122
123    /// Create a FairPlay configuration
124    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    /// Create a ClearKey configuration
133    pub fn clearkey(keys: HashMap<String, String>) -> Self {
134        Self {
135            clearkey_keys: keys,
136            ..Default::default()
137        }
138    }
139
140    /// Add a custom header for license requests
141    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    /// Check if any DRM is configured
147    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    /// Get supported DRM systems
155    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/// License request/response for a DRM system
174#[derive(Debug, Clone)]
175pub struct LicenseRequest {
176    /// DRM system type
177    pub system: DrmSystem,
178    /// Request body (challenge)
179    pub challenge: Vec<u8>,
180    /// License server URL
181    pub license_url: Url,
182    /// Request headers
183    pub headers: HashMap<String, String>,
184}
185
186/// License response from server
187#[derive(Debug, Clone)]
188pub struct LicenseResponse {
189    /// DRM system type
190    pub system: DrmSystem,
191    /// License data
192    pub license: Vec<u8>,
193    /// Expiration time (Unix timestamp, 0 = no expiration)
194    pub expiration: u64,
195}
196
197/// DRM session state
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum DrmSessionState {
200    /// Session not started
201    Idle,
202    /// Waiting for license server certificate
203    AwaitingCertificate,
204    /// Certificate received, generating challenge
205    GeneratingChallenge,
206    /// License request sent
207    AwaitingLicense,
208    /// License received and loaded
209    Ready,
210    /// Session expired
211    Expired,
212    /// Error occurred
213    Error,
214}
215
216/// DRM session information
217#[derive(Debug, Clone)]
218pub struct DrmSession {
219    /// Session ID
220    pub id: String,
221    /// DRM system
222    pub system: DrmSystem,
223    /// Current state
224    pub state: DrmSessionState,
225    /// Key IDs in this session
226    pub key_ids: Vec<String>,
227    /// Expiration time (Unix timestamp)
228    pub expiration: u64,
229    /// Error message if state is Error
230    pub error: Option<String>,
231}
232
233impl DrmSession {
234    /// Create a new DRM session
235    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    /// Check if session is ready for decryption
247    pub fn is_ready(&self) -> bool {
248        self.state == DrmSessionState::Ready
249    }
250
251    /// Check if session is expired
252    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
264/// DRM Manager - Handles license acquisition and session management
265pub struct DrmManager {
266    config: DrmConfig,
267    sessions: HashMap<String, DrmSession>,
268    pssh_boxes: Vec<PsshBox>,
269}
270
271impl DrmManager {
272    /// Create a new DRM manager
273    pub fn new(config: DrmConfig) -> Self {
274        Self {
275            config,
276            sessions: HashMap::new(),
277            pssh_boxes: Vec::new(),
278        }
279    }
280
281    /// Set PSSH boxes from manifest or init segment
282    pub fn set_pssh_boxes(&mut self, boxes: Vec<PsshBox>) {
283        self.pssh_boxes = boxes;
284    }
285
286    /// Get PSSH box for a specific DRM system
287    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    /// Create a license request for Widevine
293    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    /// Create a license request for FairPlay
306    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    /// Get ClearKey license (no server needed)
319    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        // Build ClearKey license JSON
325        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    /// Create or get a session for a DRM system
348    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    /// Update session with license response
356    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    /// Get all active sessions
367    pub fn sessions(&self) -> impl Iterator<Item = &DrmSession> {
368        self.sessions.values()
369    }
370
371    /// Get session by ID
372    pub fn get_session(&self, id: &str) -> Option<&DrmSession> {
373        self.sessions.get(id)
374    }
375
376    /// Close a session
377    pub fn close_session(&mut self, id: &str) {
378        self.sessions.remove(id);
379    }
380
381    /// Close all sessions
382    pub fn close_all_sessions(&mut self) {
383        self.sessions.clear();
384    }
385
386    /// Check if DRM is required for playback
387    pub fn is_drm_required(&self) -> bool {
388        !self.pssh_boxes.is_empty()
389    }
390
391    /// Get best available DRM system
392    pub fn select_drm_system(&self) -> Option<DrmSystem> {
393        let supported = self.config.supported_systems();
394
395        // Check what PSSH boxes we have
396        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        // Fall back to ClearKey if configured (doesn't need PSSH)
403        if !self.config.clearkey_keys.is_empty() {
404            return Some(DrmSystem::ClearKey);
405        }
406
407        None
408    }
409}
410
411// Base64 encoding/decoding helpers (avoiding external dependency for core lib)
412fn 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        // Output bytes based on how many input characters we had
468        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}