PlayReadyのPSSH Dataを生成

MicrosoftのPlayReady DRMのデータを生成する処理を書いてみます。

https://docs.microsoft.com/en-us/playready/specifications/specifications

## 前置き

動画コンテンツのDRMをするにあたって、MP4の基本的な知識やブロック暗号アルゴリズム、DASH CPIXの仕様をおさえておく必要があります。

MP4の仕様は ISO/IECの14496-12に定義 
https://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf

流れとしては
1. PSSH Boxを生成
2. エンコーダがMux時にmp4のinitialセグメントにPSSH Boxを埋め込みつつ暗号化
3. 再生時にMSE(Media Source Extension)がPSSHを取り出し
4. ライセンスサーバと通信して複合

という感じになります。PSSH Boxはヘッダ部とData部で構成されますが、
この記事ではPlay ReadyにおけるPSSHのDataの生成方法を解説するので、この記事単体でDRMを実現することはできません。PSSH Box全体の生成を含めたDRM処理の解説は後ほど載せたいと思います。

## PlayReadyの仕様をみてみる

https://docs.microsoft.com/en-us/playready/specifications/playready-header-specification
現時点の最終変更日は2017年11月。

### PlayReady Object

PlayReady Objectの仕様

|フィールド名|フィールドタイプ|サイズ|説明|
|---|---|---|---|---|
|Length| DWORD |32bit|PlayReady Objectのサイズ|
|PRO Length Count|WORD|16bit|PRO Recordsの数|
|PRO Records|Byte Array|可変|下記に記載した形式のオブジェクトの配列|

PRO Recordsの形式

|フィールド名|フィールドタイプ|サイズ|説明|
|---|---|---|---|---|
|Record Type|WORD|16bit|Record Valueのデータのタイプ|
|Record Length Count|WORD|16bit|Record Valueのサイズ|
|Record Value|Byte Array|可変|Record Typeによって変わる。下記にまとめた。|
(WORD・DWORDはおそらくアセンブラの概念の物を指していて、WORDが2Byte、DWORDが4Byteということだと思います。)

Record ValueとRecord Typeの対応表

|Value Type|説明|
|---|---|
|0x0001|後述するPlayReady Header(PRH)|
|0x0002|Reserved|
|0x0003|Embedded License Store (ELS).|

PlayReady Header(PRH)は再生時にクライアントがライセンスを取得する際に使われる情報を格納するXMLのデータ構造です。バージョン4.0.0.0から4.3.0.0まであり、バージョンが古いほど古いSDKに対応しています。より広い or 古いデバイスでのDRMをサポートしたい場合は古いバージョンでPRHを生成すると良いでしょう。

詳しいPRHの仕様は、少し量が多いのでv4.0.0.0のみ簡単に載せます。

```
<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0">
  <DATA>
    <PROTECTINFO>
      <ALGID>AESCTR</ALGID>
      <KEYLEN>16</KEYLEN>
    </PROTECTINFO>
    <KID>キーID</KID>
    <CHECKSUM>後述するchecksum</CHECKSUM>
    <LA_URL>ライセンス取得URL</LA_URL>
    <CUSTOMATTRIBUTES>
      ...カスタムデータ...
    </CUSTOMATTRIBUTES>
  </DATA>
</WRMHEADER>
```

### PlayReady ObjectがMP4のどこに配置されるのか
<img src="https://docs.microsoft.com/en-us/playready/images/pro_with_prh.png" />
PSSHはmp4のメタデータが記録されているmoovボックスに配置され、PlayReady ObjectはPSSHのData部に格納されているためこのように配置されます。
画像は https://docs.microsoft.com/en-us/playready/specifications/playready-header-specification から引用。

## Checksum生成アルゴリズム

https://docs.microsoft.com/en-us/playready/specifications/playready-header-specification#5-key-checksum-algorithm

PlayReadyでは暗号化と複合で同じキーでないと処理できないことを保証するためにチェックサムを付与することができます。生成したチェックサムは後述したPlayReady Headerに格納され、ALGID要素に指定されたモードによって格納する値が変わります。

AESCBCモードの場合Checksumは不必要。AESCTRとCOCKTAILモードの時に必要なようです。

## ContentKey生成のアルゴリズム

https://docs.microsoft.com/en-us/playready/specifications/playready-key-seed

PlayReadyでは、
`コンテンツキー(KID) = 生成処理(KeyID, KeySeed)`
のように、KeyIDとKeySeedからコンテンツキーを生成します。
公式サイトに掲載されている生成アルゴリズムを下記に載せています。
コードをみると、KeyIDのオーダーはGUIDとして扱うようです。 (GUIDについてはこちら https://qiita.com/daisukeoda/items/5f9d7545a6a418d8ec29 )

```c#
byte[] GeneratePlayReadyContentKey(byte[] keySeed, Guid keyId)
{
    const int DRM_AES_KEYSIZE_128 = 16;
    byte[] contentKey = new byte[DRM_AES_KEYSIZE_128];
    //
    //  Truncate the key seed to 30 bytes, key seed must be at least 30 bytes long.
    //
    byte[] truncatedKeySeed = new byte[30];
    Array.Copy(keySeed, truncatedKeySeed, truncatedKeySeed.Length);
    //
    //  Get the keyId as a byte array
    //
    byte[] keyIdAsBytes = keyId.ToByteArray();
    //
    //  Create sha_A_Output buffer.  It is the SHA of the truncatedKeySeed and the keyIdAsBytes
    //
    SHA256Managed sha_A = new SHA256Managed();
    sha_A.TransformBlock(truncatedKeySeed, 0, truncatedKeySeed.Length, truncatedKeySeed, 0);
    sha_A.TransformFinalBlock(keyIdAsBytes, 0, keyIdAsBytes.Length);
    byte[] sha_A_Output = sha_A.Hash;
    //
    //  Create sha_B_Output buffer.  It is the SHA of the truncatedKeySeed, the keyIdAsBytes, and
    //  the truncatedKeySeed again.
    //
    SHA256Managed sha_B = new SHA256Managed();
    sha_B.TransformBlock(truncatedKeySeed, 0, truncatedKeySeed.Length, truncatedKeySeed, 0);
    sha_B.TransformBlock(keyIdAsBytes, 0, keyIdAsBytes.Length, keyIdAsBytes, 0);
    sha_B.TransformFinalBlock(truncatedKeySeed, 0, truncatedKeySeed.Length);
    byte[] sha_B_Output = sha_B.Hash;
    //
    //  Create sha_C_Output buffer.  It is the SHA of the truncatedKeySeed, the keyIdAsBytes,
    //  the truncatedKeySeed again, and the keyIdAsBytes again.
    //
    SHA256Managed sha_C = new SHA256Managed();
    sha_C.TransformBlock(truncatedKeySeed, 0, truncatedKeySeed.Length, truncatedKeySeed, 0);
    sha_C.TransformBlock(keyIdAsBytes, 0, keyIdAsBytes.Length, keyIdAsBytes, 0);
    sha_C.TransformBlock(truncatedKeySeed, 0, truncatedKeySeed.Length, truncatedKeySeed, 0);
    sha_C.TransformFinalBlock(keyIdAsBytes, 0, keyIdAsBytes.Length);
    byte[] sha_C_Output = sha_C.Hash;
    for (int i = 0; i < DRM_AES_KEYSIZE_128; i++)
    {
        contentKey[i] = Convert.ToByte(sha_A_Output[i] ^ sha_A_Output[i + DRM_AES_KEYSIZE_128]
                                       ^ sha_B_Output[i] ^ sha_B_Output[i + DRM_AES_KEYSIZE_128]
                                       ^ sha_C_Output[i] ^ sha_C_Output[i + DRM_AES_KEYSIZE_128]);
    }

    return contentKey;
}
```

## 実装

### コンテンツキー生成処理

公式サンプルをpython実装に置き換えただけですが。

```python
import base64
import hashlib
import uuid

CONTENT_KEY_SEED = "00000000000000111111111111111111"

def gen_content_key(key_id):
  key_id = uuid.UUID(key_id).bytes_le

  seed_bytes = b""
  for x in range(len(CONTENT_KEY_SEED)):
    if x % 2 == 1:
        continue
    if x + 2 > len(CONTENT_KEY_SEED):
        break
    l = x + 2
    seed_bytes += int(CONTENT_KEY_SEED[x:l], 16).to_bytes(1, "big")

  # sha a
  # SHA of the truncatedKeySeed and the keyIdAsBytes
  sha = hashlib.sha256()
  sha.update(seed_bytes)
  sha.update(key_id)
  shaA = [c for c in sha.digest()]

  # sha b
  # SHA of the truncatedKeySeed, the keyIdAsBytes, and
  # the truncatedKeySeed again.
  sha = hashlib.sha256()
  sha.update(seed_bytes)
  sha.update(key_id)
  sha.update(seed_bytes)
  shaB = [c for c in sha.digest()]

  # sha c
  # SHA of the truncatedKeySeed, the keyIdAsBytes,
  # the truncatedKeySeed again, and the keyIdAsBytes again.
  sha = hashlib.sha256()
  sha.update(seed_bytes)
  sha.update(key_id)
  sha.update(seed_bytes)
  sha.update(key_id)
  shaC = [c for c in sha.digest()]

  AES_KEYSIZE_128 = 16
  content_key = b""
  for i in range(AES_KEYSIZE_128):
    xorA = shaA[i] ^ shaA[i + AES_KEYSIZE_128]
    xorB = shaB[i] ^ shaB[i + AES_KEYSIZE_128]
    xorC = shaC[i] ^ shaC[i + AES_KEYSIZE_128]
    content_key += (xorA ^ xorB ^ xorC).to_bytes(1, byteorder='big')

  return content_key
```

### Checksum生成処理

```python
from Crypto.Cipher import AES
import uuid
import base64

def compute_check_sum(kid, content_key):
  # kidは16, 24, 32のいずれかでなければいけない 
  if isinstance(kid, str):
    kid = uuid.UUID(kid).bytes_le
  elif isinstance(kid, uuid.UUID):
    kid = kid.bytes_le

  crypto = AES.new(base64.b16decode(content_key), AES.MODE_ECB)
  return crypto.encrypt(kid)[:8]
```

### PlayReady Header生成処理

```python
import base64
import uuid

checksum = "さっき生成したチェックサム"

def gen_wrm_header(kid, content_key, la_url):
    # 厳密には複数kidの入力に対応する必要がある
    # あと今回はCUSTOMATTRIBUTEは生成してません
    le_kid = uuid.UUID(kid).bytes_le
    b64_KID = base64.b64encode(le_kid).decode('utf-8')
    b64_checksum = base64.b64encode(checksum).decode('utf-8')
    wrh = '<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO>'
    wrh += f"<KID>{b64_KID}</KID>"
    wrh += f"<CHECKSUM>{b64_checksum}</CHECKSUM>"
    wrh += f"<LA_URL>{la_url}</LA_URL>"
    wrh += "</DATA></WRMHEADER>"
    return wrh
```

### PlayReady Object生成処理

```python


wrh = "さっき生成したPlayReady Header"

def gen_playready_object(wrh):
    wrh = wrh.encode('utf-16le')
    wrh_length = len(wrh)
    overall_block_len = 4
    record_count_len = 2
    record_type_len = 2
    wrh_size_len = 2
    # PRO全体の長さ
    pro_size = wrh_length + overall_block_len + record_count_len + record_type_len + wrh_size_len

    return ((pro_size).to_bytes(overall_block_len, "little") + # PRO全体のながさ
      (1).to_bytes(record_count_len, "little") + # record数
      (1).to_bytes(record_type_len, "little") + # recordタイプ
      (wrh_length).to_bytes(wrh_size_len, "little") + # wrmheaderのながさ
      wrh # wrmheader本体
    )
```

## まとめ

PlayReadyなDRMでの最低限必要な処理を解説してみました。実装については、unified streaming社のDRMライブラリである[pycpix](https://github.com/unifiedstreaming/pycpix/)を参考にしてみました。

以下のリポジトリにサンプルコードの全体像があるのでご参考に。
https://github.com/OdaDaisuke/playready-drm

後日、DRMして実際に再生するところまで解説予定です。