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
+1 -1
View File
@@ -301,7 +301,7 @@ def test_import_takes_its_drm_type_from_the_export(tmp_path: Path, system: str,
doc["titles"][0]["drm"] = [{"system": system, "pssh": "AAAA"}]
export.write_text(json.dumps(doc), encoding="utf8")
assert ImportService(import_ctx(), "EXAMPLE", "movie-1", str(export))._server_cdm_type == expected
assert ImportService(import_ctx(), "EXAMPLE", "movie-1", str(export)).server_cdm_type == expected
class KeyDRM:
+4 -3
View File
@@ -18,6 +18,7 @@ import pytest
from pywidevine.pssh import PSSH
from rich.table import Table
from tests.remote.key_source import remote_keys
from unshackle.commands import dl as dl_module
from unshackle.commands.dl import dl
from unshackle.core.drm import Widevine
@@ -69,7 +70,7 @@ def make_cmd(monkeypatch: pytest.MonkeyPatch, vaults: Any) -> dl:
cmd.service = "EXAMPLE"
cmd.vaults = vaults
cmd.vault_cache_tally = 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
@@ -111,7 +112,7 @@ def test_the_error_names_the_kids_without_a_content_key() -> None:
def test_the_server_cdm_branch_lets_the_import_error_through(monkeypatch: pytest.MonkeyPatch) -> None:
drm = FakeDRM({})
track = SimpleNamespace(drm=[drm])
track = SimpleNamespace(id="vid", drm=[drm])
cmd = make_cmd(monkeypatch, EmptyVaults())
with pytest.raises(click.ClickException, match=KID.hex):
@@ -130,7 +131,7 @@ def test_the_retry_after_a_bad_vault_key_ends_in_the_import_error(
assert vaults.get_key(KID) == (BAD, poisoned)
cmd = make_cmd(monkeypatch, vaults)
drm = FakeDRM({KID: BAD, OTHER: OTHER_KEY})
track = SimpleNamespace(drm=[drm])
track = SimpleNamespace(id="vid", drm=[drm])
calls: list[str] = []
path = tmp_path / "video.mp4"
path.write_bytes(b"cipher")
+48 -2
View File
@@ -6,7 +6,7 @@ import logging
import threading
import time
from types import SimpleNamespace
from typing import Any
from typing import Any, Callable, Optional
from uuid import UUID
import pytest
@@ -14,10 +14,11 @@ from pywidevine.pssh import PSSH
from rich.table import Table
from rich.tree import Tree
from tests.core.test_all_drm import playready
from unshackle.commands.dl import dl
from unshackle.core.cdm.detect import cdm_type_stub
from unshackle.core.constants import DOWNLOAD_CANCELLED, DownloadCancelled
from unshackle.core.drm import Widevine
from unshackle.core.drm import ClearKeyCENC, PlayReady, Widevine
pytestmark = pytest.mark.unit
@@ -151,3 +152,48 @@ def test_a_key_label_is_added_once_with_its_marker_and_source() -> None:
f"[text2]{KID_A.hex}:{KEYS[KID_A]}* from cache",
f"[text2]{KID_B.hex}:{KEYS[KID_B]}",
]
@pytest.mark.parametrize(
"drm_class, build, cdm",
[
(Widevine, lambda: widevine(KID_A), "widevine"),
(PlayReady, lambda: playready(KID_A), "playready"),
(ClearKeyCENC, lambda: ClearKeyCENC(kids=[KID_A]), None),
],
ids=["widevine", "playready", "clearkey"],
)
def test_an_all_zero_licence_key_is_dropped(
cmd: dl, monkeypatch: pytest.MonkeyPatch, drm_class: type, build: Callable[[], Any], cdm: Optional[str]
) -> None:
def get_content_keys(self: Any, **_: Any) -> None:
self.content_keys.update({KID_A: KEYS[KID_A], KID_C: "0" * 32})
monkeypatch.setattr(drm_class, "get_content_keys", get_content_keys)
monkeypatch.setattr(dl, "LICENSE_KEY_CACHE", {})
monkeypatch.setattr(dl, "DRM_LOCKS", {})
drm = build()
if cdm:
cmd.cdm = cdm_type_stub(cdm)
prepare(cmd, drm, KID_A, new_table())
assert drm.content_keys == {KID_A: KEYS[KID_A]}
assert dl.LICENSE_KEY_CACHE == {KID_A: KEYS[KID_A]}
def test_vaults_only_stops_at_the_first_kid_no_vault_holds(cmd: dl, licences: list[str]) -> None:
asked: list[UUID] = []
class EmptyVaults(NoVaults):
def get_key(self, kid: UUID) -> tuple[None, None]:
asked.append(kid)
return None, None
cmd.vaults = EmptyVaults()
with pytest.raises(Widevine.Exceptions.CEKNotFound, match=KID_A.hex):
prepare(cmd, widevine(KID_A, KID_B), KID_A, new_table(), vaults_only=True)
assert asked == [KID_A]
assert licences == []
+5 -4
View File
@@ -8,6 +8,7 @@ from uuid import UUID
import pytest
from tests.remote.key_source import remote_keys
from unshackle.commands import dl as dl_module
from unshackle.commands.dl import dl
from unshackle.core.vault import RateLimited, vault_session
@@ -831,10 +832,11 @@ def test_prefer_vault_keys_queries_each_named_kid_once() -> None:
asked: list[UUID] = []
cmd = dl.__new__(dl)
cmd.vaults = SimpleNamespace(get_key=lambda kid: asked.append(kid) or (None, None))
cmd._remote_service = SimpleNamespace(server_vault_keys={})
video, audio = (SimpleNamespace(kids=[KID], content_keys={KID: GOOD, SIBLING: BAD}) for _ in range(2))
cmd.prefer_vault_keys(SimpleNamespace(tracks=[SimpleNamespace(drm=[video]), SimpleNamespace(drm=[audio])]))
cmd.prefer_vault_keys(
remote_keys(), SimpleNamespace(tracks=[SimpleNamespace(drm=[video]), SimpleNamespace(drm=[audio])])
)
assert asked == [KID]
@@ -845,13 +847,12 @@ def test_cache_resolved_keys_skips_a_key_a_local_vault_holds(tmp_path: Path) ->
cmd = dl.__new__(dl)
cmd.vaults = Vaults("SVC")
cmd.vaults.vaults = [local]
cmd._remote_service = SimpleNamespace(server_vault_keys={})
pushed: list[dict] = []
cmd.flush_vault_writes = lambda writes: pushed.extend(w.args[0] for w in writes)
dl.LICENSE_KEY_CACHE.clear()
drm = SimpleNamespace(kids=[KID], content_keys={KID: GOOD, SIBLING: BAD})
cmd.cache_resolved_keys(SimpleNamespace(tracks=[SimpleNamespace(drm=[drm])]))
cmd.cache_resolved_keys(remote_keys(), SimpleNamespace(tracks=[SimpleNamespace(drm=[drm])]))
assert pushed == [{KID: GOOD}]