mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 12:05:07 +00:00
fix(tracks): flag every encrypted sample entry type after decrypt
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""Self-check for ``has_encrypted_sample_entry``: a finalized MP4 whose moov still carries
|
||||
an encv/enca sample entry means the decrypt tool skipped/failed the track (frma was never
|
||||
an encrypted (enc*/drm*) sample entry means the decrypt tool skipped/failed the track (frma was never
|
||||
applied), so the detector must flag it before the file reaches the muxer. Detection is
|
||||
scoped to moov, so stray 4CC byte runs in mdat must never trip (or mask) it.
|
||||
|
||||
@@ -11,6 +11,8 @@ from __future__ import annotations
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from unshackle.core.tracks.track import has_encrypted_sample_entry
|
||||
|
||||
|
||||
@@ -80,6 +82,12 @@ def test_text_containing_enca_in_moov_not_detected(tmp_path: Path) -> None:
|
||||
assert has_encrypted_sample_entry(f) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", [b"encs", b"enct", b"drmi"])
|
||||
def test_other_encrypted_entry_kinds_detected(tmp_path: Path, kind: bytes) -> None:
|
||||
f = write(tmp_path, "enc.mp4", FTYP, moov(stsd([sample_entry(kind, box(b"sinf", box(b"frma", b"stpp")))])))
|
||||
assert has_encrypted_sample_entry(f) is True
|
||||
|
||||
|
||||
def test_missing_or_bogus_file(tmp_path: Path) -> None:
|
||||
assert has_encrypted_sample_entry(tmp_path / "nope.mp4") is False
|
||||
bogus = write(tmp_path, "bogus.bin", b"\x00\x01\x02\x03not-an-mp4")
|
||||
|
||||
@@ -11,6 +11,7 @@ PIFF_TENC = bytes.fromhex("8974dbce7be74c5184f97148f9882554")
|
||||
PIFF_TFXD = bytes.fromhex("6d1d9b0542d544e680e2141daff757b2")
|
||||
PIFF_SENC = bytes.fromhex("a2394f525a9b4f14a2446c427c648df4")
|
||||
TENC = {b"tenc", b"uuid" + PIFF_TENC}
|
||||
ENCRYPTED_ENTRY = (b"enc", b"drm")
|
||||
SENC = {b"senc", b"uuid" + PIFF_SENC}
|
||||
# the fixed sample entry fields before the child boxes: visual; sound v0 (and ISO v1), QuickTime sound v1 and v2
|
||||
ENTRY_FIELDS = {b"encv": (78,), b"enca": (28, 44, 64)}
|
||||
@@ -130,7 +131,15 @@ def sample_entries(data: bytes, body: int, end: int) -> tuple[list[Optional[UUID
|
||||
whether any entry is encrypted."""
|
||||
found = list(children(data, body + 8, end))
|
||||
kids = [entry_kid(data, kind, entry, entry_end) for kind, entry, entry_end in found]
|
||||
return kids, any(kind[:3] in (b"enc", b"drm") for kind, _, _ in found)
|
||||
return kids, any(kind[:3] in ENCRYPTED_ENTRY for kind, _, _ in found)
|
||||
|
||||
|
||||
def has_encrypted_entry(moov: bytes) -> bool:
|
||||
"""True if a moov box holds a sample entry of an encrypted type (``enc*`` or ``drm*``)."""
|
||||
return any(
|
||||
kind == b"stsd" and any(entry[:3] in ENCRYPTED_ENTRY for entry, _, _ in children(moov, body + 8, end))
|
||||
for kind, body, end in walk(moov)
|
||||
)
|
||||
|
||||
|
||||
def senc_protects_samples(buf: bytes, body: int, end: int) -> bool:
|
||||
|
||||
@@ -30,7 +30,7 @@ from unshackle.core.config import config
|
||||
from unshackle.core.constants import DOWNLOAD_ALL_DRM, DOWNLOAD_CANCELLED, DOWNLOAD_LICENCE_ONLY, DownloadCancelled
|
||||
from unshackle.core.downloaders import requests
|
||||
from unshackle.core.drm import DRM_T, ClearKeyCENC, PlayReady, Widevine
|
||||
from unshackle.core.drm.verify import decrypt_track, senc_protects_samples
|
||||
from unshackle.core.drm.verify import decrypt_track, has_encrypted_entry, senc_protects_samples
|
||||
from unshackle.core.events import events
|
||||
from unshackle.core.session import MAX_BACKOFF, RnetSession, utf8_text
|
||||
from unshackle.core.temp import restore_segments, stash_segments
|
||||
@@ -244,28 +244,18 @@ def has_dts_uhd_sample_entry(path: Path) -> bool:
|
||||
|
||||
|
||||
def has_encrypted_sample_entry(path: Path) -> bool:
|
||||
"""True if the MP4's moov still carries an encrypted sample entry (encv/enca).
|
||||
"""True if the MP4's moov still carries an encrypted sample entry (``enc*`` or ``drm*``).
|
||||
|
||||
A faithful decrypt rewrites encv/enca back to the real codec 4CC through frma, so a
|
||||
survivor means the decrypt tool skipped/failed the track. This is a 4CC scan
|
||||
scoped to the moov box (where sample entries live), not a structural parse.
|
||||
The scoping avoids chance byte collisions in mdat. Any read error -> False.
|
||||
A faithful decrypt rewrites the entry back to the real codec 4CC through frma, so a
|
||||
survivor means the decrypt tool skipped/failed the track. Only the moov is read, and its
|
||||
stsd boxes are parsed, so byte runs in mdat or in metadata cannot match. Any read or
|
||||
parse error -> False.
|
||||
"""
|
||||
try:
|
||||
moov = read_top_level_box(path, b"moov")
|
||||
except OSError:
|
||||
return moov is not None and has_encrypted_entry(moov)
|
||||
except (OSError, struct.error, RecursionError):
|
||||
return False
|
||||
if not moov:
|
||||
return False
|
||||
for tok in (b"encv", b"enca"):
|
||||
i = moov.find(tok, 4)
|
||||
while i != -1:
|
||||
# require a plausible sample-entry box size right before the 4CC
|
||||
size = int.from_bytes(moov[i - 4 : i], "big")
|
||||
if 16 <= size <= len(moov) - (i - 4):
|
||||
return True
|
||||
i = moov.find(tok, i + 1)
|
||||
return False
|
||||
|
||||
|
||||
def moof_still_encrypted(moof: bytes) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user