pssh_box/
nagra.rs

1//! Definitions for PSSH data in the Nagra DRM system.
2
3use std::fmt;
4use serde::{Serialize, Deserialize};
5use base64::{engine, Engine};
6use anyhow::{anyhow, Context, Result};
7use serde_json::Value;
8use tracing::warn;
9use crate::ToBytes;
10
11// "Normal" base64 is not suitable for Nagra.
12const BASE64_URL_SAFE_FORGIVING:
13  engine::general_purpose::GeneralPurpose =
14  engine::general_purpose::GeneralPurpose::new(
15    &base64::alphabet::URL_SAFE,
16    engine::general_purpose::GeneralPurposeConfig::new()
17      .with_decode_allow_trailing_bits(true)
18      .with_decode_padding_mode(engine::DecodePaddingMode::Indifferent),
19  );
20
21
22#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct NagraPsshData {
24    pub content_id: String,
25    pub key_id: String,
26}
27
28impl fmt::Debug for NagraPsshData {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "NagraPsshData<content_id: {}, key_id: {}>", self.content_id, self.key_id)
31    }
32}
33
34impl ToBytes for NagraPsshData {
35    fn to_bytes(&self) -> Vec<u8> {
36        // make sure we serialize without any spaces
37        let json = format!("{{\"contentId\":\"{}\",\"keyId\":\"{}\"}}",
38                           self.content_id, self.key_id);
39        BASE64_URL_SAFE_FORGIVING.encode(json).into_bytes()
40    }
41}
42
43// The structure is similar to a JWT
44pub fn parse_pssh_data(buf: &[u8]) -> Result<NagraPsshData> {
45    let b64 = String::from_utf8(buf.to_vec())
46        .context("decoding UTF-8")?;
47    let json = BASE64_URL_SAFE_FORGIVING.decode(b64)
48        .context("decoding base64")?;
49    let parsed: Value = serde_json::from_slice(&json)
50        .context("parsing as JSON")?;
51    match parsed.as_object() {
52        Some(map) => {
53            if map.len() > 2 {
54                let keys: Vec<_> = map.keys().collect();
55                warn!("unknown key in Nagra PSSH data, {keys:?}");
56            }
57            let cid = map["contentId"].as_str()
58                .context("extracting contentId")?;
59            let kid = map["keyId"].as_str()
60                .context("extracting keyId")?;
61            Ok(NagraPsshData {
62                content_id: String::from(cid),
63                key_id: String::from(kid),
64            })
65        },
66        None => Err(anyhow!("parsing as JSON")),
67    }
68}