7.2 Build the playback token
key = base64url_decode(playback_key) // 16 bytes
payload = {"exp": server_now + 300, "sid": session_id, "str": "<type>/<id>/<ext>"}
iv = 12 random bytes // new for every token
ct, tag = AES-128-GCM-encrypt(key, iv, utf8(json(payload))) // no AAD, 16-byte tag
p = base64url(iv || tag || ct) // note: tag BEFORE ciphertext
token = p + "." + base64url(HMAC-SHA256(key, ascii(p)))
- base64url without padding (
=). str:channel/<channel id>/tsorchannel/<channel id>/m3u8;vod/<movie id>/<container_extension>;saga/<episode id>/<container_extension>.- WebCrypto's
encryptreturnsct || tag: split the last 16 bytes off and reorder toiv || tag || ct. - Content outside the line's package is refused by the node (
403).
Test vectors. With these inputs your code must produce exactly these tokens:
| Vector 1 | Vector 2 | |
|---|---|---|
playback_key |
AAECAwQFBgcICQoLDA0ODw |
AAECAwQFBgcICQoLDA0ODw |
| key (hex) | 000102030405060708090a0b0c0d0e0f |
same |
| iv (hex) | a0a1a2a3a4a5a6a7a8a9aaab |
b0b1b2b3b4b5b6b7b8b9babb |
| payload | {"exp":1767225600,"sid":"wSN7wDGh2fH4o6Ai1LxUAg","str":"channel/123/ts"} |
{"exp":1767225600,"sid":"wSN7wDGh2fH4o6Ai1LxUAg","str":"vod/4321/mp4"} |
| tag (hex) | df94b875544f120892a13ab9d051d75f |
705e61aec7ca9d1ef50ac1c51db91198 |
Vector 1 token:
oKGio6Slpqeoqaqr35S4dVRPEgiSoTq50FHXX9GkXcMOqwk7vU6CMnQnhlBjwgF76nMflQdnItV1-svv8pEjeTuF2iwW7668-ppu8pB2Pz1Bo6zP4V7vpIsAaFLAA4D9HwWzvw.EbSRReGnF1PpE7iG426GxgG9jmEncKdcOj8bdAn0Wzk
Vector 2 token:
sLGys7S1tre4ubq7cF5hrsfKnR71CsHFHbkRmCXr1eW3tREOiRxsNOdPKdbpf3UEoIY13IOBVPeNZGaA880rY5YNtaMbP-JgJZVG5jyalBK4xDrNzXGF6i5D1oESAXdi_9Q.e_vR9ATz3Xp3Vdey0QZcCf19BNcExPx8dZf1j-noOEA
The JSON is serialized exactly as shown (no spaces, keys in that order). Any valid JSON works for the node; the fixed form only makes the vectors reproducible.
7.3 Clock
The node refuses a token whose exp has passed or is more than 600 s in the
future. TV clocks are often wrong, so never use the device clock directly:
offset = server_time - device_now // from login / refresh-session (or identity.timestamp)
server_now = device_now + offset
Update offset on every login and refresh-session.