#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DrmSystem {
Widevine,
PlayReady,
FairPlay,
ClearKey,
}
impl DrmSystem {
#[must_use]
pub fn system_id(&self) -> &'static str {
match self {
Self::Widevine => "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",
Self::PlayReady => "9a04f079-9840-4286-ab92-e65be0885f95",
Self::FairPlay => "94ce86fb-07ff-4f43-adb8-93d2fa968ca2",
Self::ClearKey => "e2719d58-a985-b3c9-781a-b030af78d30e",
}
}
#[must_use]
pub fn display_name(&self) -> &'static str {
match self {
Self::Widevine => "Widevine",
Self::PlayReady => "PlayReady",
Self::FairPlay => "FairPlay",
Self::ClearKey => "ClearKey",
}
}
}
#[derive(Debug, Clone)]
pub struct DrmProtectionInfo {
pub system: DrmSystem,
pub pssh_box: Vec<u8>,
pub key_id: Vec<u8>,
pub la_url: Option<String>,
}
impl DrmProtectionInfo {
#[must_use]
pub fn new(system: DrmSystem, key_id: Vec<u8>) -> Self {
Self {
system,
pssh_box: Vec::new(),
key_id,
la_url: None,
}
}
#[must_use]
pub fn with_pssh(mut self, pssh: Vec<u8>) -> Self {
self.pssh_box = pssh;
self
}
#[must_use]
pub fn with_la_url(mut self, url: impl Into<String>) -> Self {
self.la_url = Some(url.into());
self
}
}
#[derive(Debug, Clone)]
pub struct ContentKey {
pub key_id: Vec<u8>,
pub key: Vec<u8>,
pub iv: Vec<u8>,
}
#[must_use]
pub fn generate_clear_key_pssh(key_ids: &[Vec<u8>]) -> Vec<u8> {
let ids: Vec<String> = key_ids.iter().map(|k| hex_encode(k)).collect();
let json = format!(
r#"{{"kids":[{}]}}"#,
ids.iter()
.map(|id| format!("\"{id}\""))
.collect::<Vec<_>>()
.join(",")
);
let payload = json.into_bytes();
let mut out = Vec::with_capacity(4 + payload.len());
let len = payload.len() as u32;
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&payload);
out
}
#[must_use]
pub fn drm_info_for_manifest(protections: &[DrmProtectionInfo]) -> String {
let mut items: Vec<String> = Vec::with_capacity(protections.len());
for p in protections {
let key_id_hex = hex_encode(&p.key_id);
let pssh_hex = hex_encode(&p.pssh_box);
let la = p
.la_url
.as_deref()
.map(|u| format!(r#","la_url":"{u}""#))
.unwrap_or_default();
items.push(format!(
r#" {{"system":"{name}","system_id":"{sid}","key_id":"{kid}","pssh":"{pssh}"{la}}}"#,
name = p.system.display_name(),
sid = p.system.system_id(),
kid = key_id_hex,
pssh = pssh_hex,
));
}
format!("[\n{}\n]", items.join(",\n"))
}
#[must_use]
pub fn generate_content_key() -> ContentKey {
const A: u64 = 6_364_136_223_846_793_005;
const C: u64 = 1_442_695_040_888_963_407;
let mut state: u64 = 0xDEAD_BEEF_CAFE_BABE;
let mut next_byte = || -> u8 {
state = state.wrapping_mul(A).wrapping_add(C);
(state >> 56) as u8
};
let key_id: Vec<u8> = (0..16).map(|_| next_byte()).collect();
let key: Vec<u8> = (0..16).map(|_| next_byte()).collect();
let iv: Vec<u8> = (0..16).map(|_| next_byte()).collect();
ContentKey { key_id, key, iv }
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_drm_system_ids_unique() {
let ids = [
DrmSystem::Widevine.system_id(),
DrmSystem::PlayReady.system_id(),
DrmSystem::FairPlay.system_id(),
DrmSystem::ClearKey.system_id(),
];
let unique: std::collections::HashSet<_> = ids.iter().collect();
assert_eq!(unique.len(), 4, "All system IDs should be unique");
}
#[test]
fn test_drm_system_widevine_id() {
assert_eq!(
DrmSystem::Widevine.system_id(),
"edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
);
}
#[test]
fn test_drm_system_display_names() {
assert_eq!(DrmSystem::Widevine.display_name(), "Widevine");
assert_eq!(DrmSystem::PlayReady.display_name(), "PlayReady");
assert_eq!(DrmSystem::FairPlay.display_name(), "FairPlay");
assert_eq!(DrmSystem::ClearKey.display_name(), "ClearKey");
}
#[test]
fn test_drm_protection_info_new() {
let key_id = vec![0u8; 16];
let info = DrmProtectionInfo::new(DrmSystem::Widevine, key_id.clone());
assert_eq!(info.system, DrmSystem::Widevine);
assert!(info.pssh_box.is_empty());
assert!(info.la_url.is_none());
}
#[test]
fn test_drm_protection_info_with_la_url() {
let info = DrmProtectionInfo::new(DrmSystem::PlayReady, vec![0u8; 16])
.with_la_url("https://license.example.com/pr");
assert_eq!(
info.la_url.as_deref(),
Some("https://license.example.com/pr")
);
}
#[test]
fn test_generate_clear_key_pssh_has_length_prefix() {
let key_ids = vec![vec![1u8; 16], vec![2u8; 16]];
let pssh = generate_clear_key_pssh(&key_ids);
assert!(
pssh.len() >= 4,
"should have at least the 4-byte length prefix"
);
let declared_len =
u32::from_le_bytes(pssh[..4].try_into().expect("should succeed in test")) as usize;
assert_eq!(declared_len + 4, pssh.len());
}
#[test]
fn test_generate_clear_key_pssh_contains_key_ids() {
let key_ids = vec![vec![0xABu8; 16]];
let pssh = generate_clear_key_pssh(&key_ids);
let payload = std::str::from_utf8(&pssh[4..]).expect("should succeed in test");
let expected_hex = "ab".repeat(16);
assert!(payload.contains(&expected_hex));
}
#[test]
fn test_drm_info_for_manifest_empty() {
let result = drm_info_for_manifest(&[]);
assert!(result.starts_with('['));
assert!(result.ends_with(']'));
}
#[test]
fn test_drm_info_for_manifest_contains_system_name() {
let info = DrmProtectionInfo::new(DrmSystem::Widevine, vec![0u8; 16]);
let result = drm_info_for_manifest(&[info]);
assert!(result.contains("Widevine"));
assert!(result.contains("edef8ba9"));
}
#[test]
fn test_drm_info_for_manifest_contains_la_url() {
let info = DrmProtectionInfo::new(DrmSystem::ClearKey, vec![0xFFu8; 16])
.with_la_url("https://lic.example.com");
let result = drm_info_for_manifest(&[info]);
assert!(result.contains("https://lic.example.com"));
}
#[test]
fn test_generate_content_key_lengths() {
let ck = generate_content_key();
assert_eq!(ck.key_id.len(), 16);
assert_eq!(ck.key.len(), 16);
assert_eq!(ck.iv.len(), 16);
}
#[test]
fn test_generate_content_key_deterministic() {
let ck1 = generate_content_key();
let ck2 = generate_content_key();
assert_eq!(ck1.key_id, ck2.key_id);
assert_eq!(ck1.key, ck2.key);
assert_eq!(ck1.iv, ck2.iv);
}
}