refactor(drm): resolve content keys in one licence path behind ServerKeySource

Drop null keys from every licence, server and export source, not only Widevine. dl now finds remote sessions and imports through ServerKeySource instead of private fields.
This commit is contained in:
imSp4rky
2026-11-02 21:13:23 -07:00
parent d8a4950b99
commit c4677341c8
15 changed files with 463 additions and 491 deletions
@@ -11,7 +11,7 @@ from uuid import UUID
import pytest
from unshackle.core.remote_service import RemoteService
from tests.remote.key_source import remote_keys
pytestmark = pytest.mark.unit
@@ -46,14 +46,10 @@ class _Client:
def _service(server_type, answer):
svc = RemoteService.__new__(RemoteService)
svc = remote_keys(server_cdm_type=server_type)
svc._track_alias = {}
svc._title_alias = {}
svc._server_cdm = True
svc._server_cdm_type = server_type
svc.client_licensed = set()
svc._session_id = "sess"
svc.server_vault_keys = {}
svc.log = logging.getLogger("test-remote-license")
svc.client = _Client(answer)
return svc
@@ -86,7 +82,7 @@ def test_response_type_refreshes_the_server_type():
svc.proxy_license(b"", track, "widevine")
assert svc._server_cdm_type == "playready"
assert svc.server_cdm_type == "playready"
def test_proxy_mode_keeps_the_callers_type():
@@ -12,11 +12,12 @@ from uuid import UUID
import pytest
from tests.remote.key_source import remote_keys
from unshackle.core.api import handlers
from unshackle.core.api.errors import APIError
from unshackle.core.api.session_store import SessionEntry
from unshackle.core.drm import BlowfishStripe
from unshackle.core.remote_service import RemoteService, ServerVault
from unshackle.core.remote_service import RemoteService
pytestmark = pytest.mark.unit
@@ -25,13 +26,10 @@ CDM_KID = UUID("43de497c6d1945f49acc2951696ec628")
def remote_service() -> RemoteService:
svc = RemoteService.__new__(RemoteService)
svc = remote_keys(server_cdm=None)
svc._track_alias = {}
svc._title_alias = {}
svc.server_vault_keys = {}
svc.server_vault = ServerVault(svc)
svc._session_id = "sess"
svc.client_licensed = set()
svc.log = SimpleNamespace(warning=lambda *a, **k: None)
return svc
@@ -277,3 +275,15 @@ async def test_a_stripe_cipher_track_is_clear_and_never_licensed(monkeypatch):
assert json.loads(resp.body)["clear_tracks"] == ["aud"]
assert isinstance(track.drm[0], BlowfishStripe)
def test_the_server_licence_drops_a_null_key():
kid, null = UUID(int=1), UUID(int=2)
class Licensed:
content_keys: dict = {}
def get_content_keys(self, **kwargs):
self.content_keys = {kid: "ab" * 16, null: "0" * 32}
assert handlers.license_with_track_fallback(Licensed(), SimpleNamespace(drm=None)) == {kid.hex: "ab" * 16}
@@ -13,6 +13,7 @@ import pywidevine.pssh as wv_pssh_mod
from rich.table import Table
import unshackle.core.drm as drm_mod
from tests.remote.key_source import remote_keys
from unshackle.commands.dl import dl
from unshackle.core.api import handlers
from unshackle.core.api.errors import APIError, APIErrorCode
@@ -203,16 +204,11 @@ _WV.__name__ = "Widevine"
def remote(local_cdm, answer):
svc = RemoteService.__new__(RemoteService)
svc = remote_keys(server_cdm_type="playready", local_cdm=local_cdm)
svc._track_alias = {}
svc._title_alias = {}
svc._server_cdm = True
svc._server_cdm_type = "playready"
svc._session_id = "sess"
svc.server_vault_keys = {}
svc.client_licensed = set()
svc.log = logging.getLogger("test-server-cdm-cap")
svc.ctx = SimpleNamespace(obj=SimpleNamespace(cdm=local_cdm))
posts: list = []
svc.client = SimpleNamespace(post=lambda endpoint, payload, expect=(): posts.append(payload) or answer)
svc.drain_server_logs = lambda: None
@@ -557,7 +553,7 @@ def prepare(svc, licence):
def test_dl_licenses_a_client_licensed_track_with_the_local_device():
svc = SimpleNamespace(_server_cdm=True, client_licensed={"vid"}, local_cdm="local device")
svc = remote_keys(client_licensed={"vid"}, local_cdm="local device")
server_calls: list = []
@@ -566,7 +562,7 @@ def test_dl_licenses_a_client_licensed_track_with_the_local_device():
def test_dl_takes_the_local_path_when_the_server_hands_the_track_over():
svc = SimpleNamespace(_server_cdm=True, client_licensed=set(), local_cdm="local device")
svc = remote_keys(local_cdm="local device")
assert prepare(svc, lambda **kwargs: svc.client_licensed.add("vid")) == "local device"
@@ -587,7 +583,7 @@ class PlayReady:
def test_a_mid_download_handover_puts_the_keys_on_the_callers_drm(monkeypatch):
"""The caller decrypts with the DRM object it passed in, so the local licence must fill that one."""
monkeypatch.setattr(dl, "LICENSE_KEY_CACHE", {})
svc = SimpleNamespace(_server_cdm=True, client_licensed=set(), local_cdm="local device", server_vault_keys={})
svc = remote_keys(local_cdm="local device")
runner = dl.__new__(dl)
runner._remote_service = svc
runner.cdm = "server stub"
@@ -13,6 +13,7 @@ from uuid import UUID
import pytest
from rich.table import Table
from tests.remote.key_source import remote_keys
from unshackle.commands.dl import dl
pytestmark = pytest.mark.unit
@@ -68,7 +69,7 @@ def make_cmd(vaults):
cmd.vaults = vaults
cmd.vault_cache_tally = None
cmd.debug_logger = None
cmd._remote_service = SimpleNamespace(_server_cdm=True, _server_cdm_type="widevine")
cmd._remote_service = remote_keys()
dl.LICENSE_KEY_CACHE.clear()
dl.DRM_LOCKS.clear()
return cmd
@@ -78,7 +79,7 @@ def prepare(cmd, drm, licence, track_kid, **kwargs):
table = Table.grid()
table.add_row("")
cmd.prepare_drm(
track=SimpleNamespace(drm=[drm]),
track=SimpleNamespace(id="vid", drm=[drm]),
title=None,
drm=drm,
licence=licence,
@@ -128,7 +129,7 @@ def test_batch_resolved_keys_are_cached_before_download():
drm.content_keys = {NEW: "k_new"}
title = SimpleNamespace(tracks=[SimpleNamespace(drm=[drm]), SimpleNamespace(drm=None)])
cmd.cache_resolved_keys(title)
cmd.cache_resolved_keys(remote_keys(), title)
cmd.wait_vault_writes()
assert vaults.added == {NEW: "k_new"}
@@ -168,7 +169,7 @@ def test_server_vault_keys_are_held_back_until_verified():
drm2 = FakeWidevine([CACHED])
drm2.content_keys = {CACHED: "k_cached"}
dl.LICENSE_KEY_CACHE.clear()
cmd.cache_resolved_keys(SimpleNamespace(tracks=[SimpleNamespace(drm=[drm2])]))
cmd.cache_resolved_keys(remote, SimpleNamespace(tracks=[SimpleNamespace(drm=[drm2])]))
cmd.wait_vault_writes()
assert vaults.added == {NEW: "k_new"}
@@ -242,7 +243,7 @@ def test_no_cdm_and_no_server_cdm_names_the_missing_cdm(system):
from unshackle.core.drm import PlayReady, Widevine
cmd = make_cmd(FakeVaults())
cmd._remote_service = SimpleNamespace(_server_cdm=False)
cmd._remote_service = remote_keys(server_cdm=False)
cmd.cdm = None
cmd.cdm_for = lambda system, quality=None: None
drm_class = PlayReady if system == "playready" else Widevine
@@ -250,3 +251,32 @@ def test_no_cdm_and_no_server_cdm_names_the_missing_cdm(system):
drm.content_keys = {}
with pytest.raises(click.ClickException, match="No CDM configured for TEST"):
prepare(cmd, drm, lambda **kwargs: None, NEW)
NULL = UUID("0b3e2d1c4a5f46e7b8c9d0e1f2a3b4c5")
def test_a_null_key_from_the_server_licence_is_dropped():
vaults = FakeVaults()
drm = run_prepare(vaults, [NEW], {NEW: "k_new", NULL: "0" * 32}, track_kid=NEW)
assert drm.content_keys == {NEW: "k_new"}
assert vaults.added == {NEW: "k_new"}
assert NULL not in dl.LICENSE_KEY_CACHE
def test_a_null_key_from_the_batch_licence_is_dropped(monkeypatch):
vaults = FakeVaults()
cmd = make_cmd(vaults)
drm = FakeWidevine([NEW])
title = SimpleNamespace(tracks=[SimpleNamespace(drm=[drm])])
source = cmd._remote_service
monkeypatch.setattr(
source, "resolve_server_keys", lambda t: drm.content_keys.update({NEW: "k_new", NULL: "0" * 32})
)
cmd.take_server_keys(source, title, cdm_only=False)
cmd.wait_vault_writes()
assert drm.content_keys == {NEW: "k_new"}
assert vaults.added == {NEW: "k_new"}
assert NULL not in dl.LICENSE_KEY_CACHE