mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
fix(vaults): harden the KID window check and clear a stale flag on licence
This commit is contained in:
@@ -34,11 +34,11 @@ unshackle pushes keys from every source, not only a local CDM. A run against a r
|
|||||||
|
|
||||||
### Wrong keys from a poisoned vault
|
### Wrong keys from a poisoned vault
|
||||||
|
|
||||||
A public vault can return the right KID with a wrong content key. The decrypters accept a wrong content key and write noise. unshackle therefore trusts a content key from a vault only after FFmpeg decodes the decrypted track with strict error detection. For a fragmented MP4, unshackle reads from the encrypted file which fragments each KID encrypts, and FFmpeg decodes up to three windows of each KID: from its first, middle and last fragment on through the next fragments of that KID, for up to 10 seconds. A window never runs into a fragment of another KID or a clear fragment. A clear lead or a lead under another KID therefore cannot hide a wrong content key, and only the KID that fails is flagged. A KID that is not in this map, and every KID of any other file, gets one check of the whole file. FFmpeg decodes the first seconds and the last seconds. A failure there flags each of those KIDs. Content keys from a licence skip this check. A file this FFmpeg build cannot judge counts as a pass, never as a wrong content key: a codec it has no decoder for, or a header it cannot read. Without FFmpeg there is no check at all.
|
A public vault can return the right KID with a wrong content key. The decrypters accept a wrong content key and write noise. unshackle therefore trusts a content key from a vault only after FFmpeg decodes the decrypted track with strict error detection. For a fragmented MP4, unshackle reads from the encrypted file which fragments each KID encrypts, and FFmpeg decodes up to three windows of each KID: from its first, middle and last fragment on through the next fragments of that KID, for up to 4 seconds. A window never runs into a fragment of another KID or a clear fragment. A clear lead or a lead under another KID therefore cannot hide a wrong content key, and only the KID that fails is flagged. A KID that is not in this map, and every KID of any other file, gets one check of the whole file. FFmpeg decodes the first seconds and the last seconds. A failure there flags each of those KIDs. When a mapped KID fails, the KIDs that are not in the map get no verdict from that pass; the retry with the replaced key judges them. Content keys from a licence skip this check. A file this FFmpeg build cannot judge counts as a pass, never as a wrong content key: a codec it has no decoder for, or a header it cannot read. Without FFmpeg there is no check at all.
|
||||||
|
|
||||||
When the decode fails, unshackle:
|
When the decode fails, unshackle:
|
||||||
|
|
||||||
1. Flags the KID:KEY in the `bad_keys` table of every local SQLite vault, with the service and the name of the vault it came from. The pair also goes out of the service table, except in a vault marked `no_push`, where only the flag is written. A flagged pair cannot go back into a local vault, from a later run or from `kv copy`. An `API` vault that served the pair also gets a report; `HTTP` and `MySQL` vaults have no report route. A pair from a local vault goes to no remote vault, even when a remote vault holds the same pair.
|
1. Flags the KID:KEY in the `bad_keys` table of every local SQLite vault, with the service and the name of the vault it came from. The pair also goes out of the service table, except in a vault marked `no_push`, where only the flag is written. A flagged pair cannot go back into a local vault from another vault, a later run, or `kv copy`. A licence that returns the pair clears the flag and stores it. An `API` vault that served the pair also gets a report; `HTTP` and `MySQL` vaults have no report route. A pair from a local vault goes to no remote vault, even when a remote vault holds the same pair.
|
||||||
2. Restores the ciphertext from a hard link it made before decryption, then asks for the content key again. Every vault lookup skips a flagged pair, so the request reaches the remaining vaults and then the CDM. A remote answer that arrived after the winning one is reused instead of queried again.
|
2. Restores the ciphertext from a hard link it made before decryption, then asks for the content key again. Every vault lookup skips a flagged pair, so the request reaches the remaining vaults and then the CDM. A remote answer that arrived after the winning one is reused instead of queried again.
|
||||||
3. Removes the flag when the CDM returns the same content key, because then the decode check was wrong, not the content key. A content key from another vault is checked in its turn, and flagged as well if it also fails.
|
3. Removes the flag when the CDM returns the same content key, because then the decode check was wrong, not the content key. A content key from another vault is checked in its turn, and flagged as well if it also fails.
|
||||||
|
|
||||||
|
|||||||
@@ -90,17 +90,37 @@ def test_each_kid_maps_to_its_own_fragments(tmp_path: Path) -> None:
|
|||||||
assert kid_windows(path) == KidMap({A: [(0.0, 2.0)], B: [(2.0, 6.0), (4.0, 6.0)], C: [(6.0, 8.0)]}, True)
|
assert kid_windows(path) == KidMap({A: [(0.0, 2.0)], B: [(2.0, 6.0), (4.0, 6.0)], C: [(6.0, 8.0)]}, True)
|
||||||
|
|
||||||
|
|
||||||
def test_windows_run_through_a_kid_but_stop_at_another_or_after_ten_seconds() -> None:
|
def test_windows_run_through_a_kid_but_stop_at_another_or_after_the_longest() -> None:
|
||||||
long_run = [(t * 2.0, t * 2.0 + 2) for t in range(20)]
|
long_run = [(t * 2.0, t * 2.0 + 2) for t in range(20)]
|
||||||
assert pick_windows(long_run) == [(0.0, 10.0), (20.0, 30.0), (30.0, 40.0)]
|
assert pick_windows(long_run, longest=10.0) == [(0.0, 10.0), (20.0, 30.0), (30.0, 40.0)]
|
||||||
|
assert pick_windows(long_run) == [(0.0, 4.0), (20.0, 24.0), (36.0, 40.0)]
|
||||||
# a gap is another KID or a clear stretch: no window crosses it, and the last one ends the last run
|
# a gap is another KID or a clear stretch: no window crosses it, and the last one ends the last run
|
||||||
assert pick_windows([(0, 2), (2, 4), (10, 12), (12, 14), (14, 16)]) == [(0, 4), (10, 16)]
|
assert pick_windows([(0, 2), (2, 4), (10, 12), (12, 14), (14, 16)], longest=10.0) == [(0, 4), (10, 16)]
|
||||||
|
assert pick_windows([(0, 2), (2, 4), (10, 12), (12, 14), (14, 16)]) == [(0, 4), (10, 14.0), (12, 16)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_fragments_longer_than_the_longest_and_empty_spans() -> None:
|
||||||
|
assert pick_windows([(0.0, 60.0)]) == [(0.0, 4.0)]
|
||||||
|
assert pick_windows([(0, 15), (15, 30), (30, 45), (45, 60)]) == [(0, 4), (30, 34), (45, 49)]
|
||||||
|
assert pick_windows([(0.0, 5.0), (5.0, 5.0)]) == [(0.0, 4.0)]
|
||||||
|
assert pick_windows([(5.0, 5.0)]) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_box_bomb_and_oversized_sbgp(tmp_path: Path) -> None:
|
||||||
|
body = b""
|
||||||
|
for _ in range(5000):
|
||||||
|
body = box(b"trak", body)
|
||||||
|
(tmp_path / "bomb.mp4").write_bytes(box(b"moov", body))
|
||||||
|
assert kid_windows(tmp_path / "bomb.mp4") is None
|
||||||
|
huge = full(b"sbgp", b"seig" + struct.pack(">I", 0x00FFFFFF) + bytes(16))
|
||||||
|
(tmp_path / "sbgp.mp4").write_bytes(movie(encv(A)) + moof(1, 0, huge))
|
||||||
|
assert kid_windows(tmp_path / "sbgp.mp4") is None
|
||||||
|
|
||||||
|
|
||||||
def test_first_middle_and_last_fragment(tmp_path: Path) -> None:
|
def test_first_middle_and_last_fragment(tmp_path: Path) -> None:
|
||||||
path = tmp_path / "enc.mp4"
|
path = tmp_path / "enc.mp4"
|
||||||
path.write_bytes(movie(encv(A)) + b"".join(moof(1, t * 1000) for t in range(5)))
|
path.write_bytes(movie(encv(A)) + b"".join(moof(1, t * 1000) for t in range(5)))
|
||||||
assert kid_windows(path) == KidMap({A: [(0.0, 7.0), (2.0, 7.0)]}, True)
|
assert kid_windows(path) == KidMap({A: [(0.0, 4.0), (2.0, 6.0), (3.0, 7.0)]}, True)
|
||||||
|
|
||||||
|
|
||||||
def test_last_fragment_ends_after_its_sample_durations(tmp_path: Path) -> None:
|
def test_last_fragment_ends_after_its_sample_durations(tmp_path: Path) -> None:
|
||||||
@@ -145,7 +165,7 @@ def test_samples_past_the_sbgp_runs_take_the_sample_entry_kid(tmp_path: Path) ->
|
|||||||
def test_piff_tenc_and_tfxd(tmp_path: Path) -> None:
|
def test_piff_tenc_and_tfxd(tmp_path: Path) -> None:
|
||||||
path = tmp_path / "enc.ismv"
|
path = tmp_path / "enc.ismv"
|
||||||
path.write_bytes(movie(encv(A, piff=True)) + moof(1, 0, tfxd=True) + moof(1, 2000, tfxd=True))
|
path.write_bytes(movie(encv(A, piff=True)) + moof(1, 0, tfxd=True) + moof(1, 2000, tfxd=True))
|
||||||
assert kid_windows(path) == KidMap({A: [(0.0, 5.0), (2.0, 5.0)]}, True)
|
assert kid_windows(path) == KidMap({A: [(0.0, 4.0), (2.0, 5.0)]}, True)
|
||||||
|
|
||||||
|
|
||||||
def test_oversized_box(tmp_path: Path) -> None:
|
def test_oversized_box(tmp_path: Path) -> None:
|
||||||
|
|||||||
@@ -316,6 +316,19 @@ def test_flag_moves_the_row_and_blocks_it_coming_back(tmp_path: Path) -> None:
|
|||||||
assert local.add_key("SVC", KID, GOOD) # the KID itself is not blocked, only the pair
|
assert local.add_key("SVC", KID, GOOD) # the KID itself is not blocked, only the pair
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_licence_key_clears_a_flag_from_an_earlier_run(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""A false flag must not outlive the run: the next licence returns the pair and it is stored again."""
|
||||||
|
local = SQLite("local", tmp_path / "local.db")
|
||||||
|
local.flag_bad_key("SVC", KID, GOOD, "poisoned")
|
||||||
|
vaults = Vaults("SVC")
|
||||||
|
vaults.vaults = [local]
|
||||||
|
cmd = make_cmd(monkeypatch, vaults)
|
||||||
|
cmd.vault_cache_tally = None
|
||||||
|
cmd.cache_keys_to_vaults({KID: GOOD})
|
||||||
|
assert not local.is_bad_key(KID, GOOD)
|
||||||
|
assert local.get_key(KID, "SVC") == GOOD
|
||||||
|
|
||||||
|
|
||||||
def test_bulk_add_refuses_a_flagged_pair_in_any_case_or_kid_form(tmp_path: Path) -> None:
|
def test_bulk_add_refuses_a_flagged_pair_in_any_case_or_kid_form(tmp_path: Path) -> None:
|
||||||
local = SQLite("local", tmp_path / "local.db")
|
local = SQLite("local", tmp_path / "local.db")
|
||||||
local.flag_bad_key("SVC", KID, BAD, "poisoned")
|
local.flag_bad_key("SVC", KID, BAD, "poisoned")
|
||||||
@@ -404,6 +417,45 @@ def test_a_kid_absent_from_the_map_is_judged_by_the_fallback(tmp_path: Path, mon
|
|||||||
assert (None, 3, True) in calls # the fallback knows the track is video
|
assert (None, 3, True) in calls # the fallback knows the track is video
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_wrong_mapped_key_does_not_flag_a_kid_the_file_does_not_carry(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""The audio KID is in the licence and the vault but not in this video file: the whole-file
|
||||||
|
check fails on the wrong video key's noise, and that verdict must not reach the audio KID."""
|
||||||
|
audio = UUID(int=7)
|
||||||
|
local = SQLite("local", tmp_path / "local.db")
|
||||||
|
poisoned = Remote("poisoned", tmp_path / "poisoned.db")
|
||||||
|
poisoned.add_key("SVC", audio, GOOD)
|
||||||
|
poisoned.add_key("SVC", KID, BAD)
|
||||||
|
other = Remote("other", tmp_path / "other.db")
|
||||||
|
other.add_key("SVC", KID, GOOD)
|
||||||
|
vaults = Vaults("SVC")
|
||||||
|
vaults.vaults = [local, poisoned]
|
||||||
|
cmd = make_cmd(monkeypatch, vaults)
|
||||||
|
drm = FakeDRM({audio: vaults.get_key(audio)[0], KID: vaults.get_key(KID)[0]})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
dl_module.verify, "kid_windows", lambda path: dl_module.verify.KidMap({KID: [(4.0, 6.0)]}, True)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
dl_module,
|
||||||
|
"ffmpeg_decodes",
|
||||||
|
lambda path, start=None, seconds=3, video=None: drm.decrypted_with[-1][KID] == GOOD,
|
||||||
|
)
|
||||||
|
path = tmp_path / "video.mp4"
|
||||||
|
path.write_bytes(b"cipher")
|
||||||
|
|
||||||
|
def licence(drm: FakeDRM, track_kid: UUID) -> None:
|
||||||
|
vaults.vaults.append(other)
|
||||||
|
drm.content_keys[KID] = vaults.get_key(KID)[0]
|
||||||
|
|
||||||
|
cmd.decrypt_verified(drm, path, licence, KID)
|
||||||
|
cmd.wait_vault_writes()
|
||||||
|
assert drm.content_keys == {audio: GOOD, KID: GOOD}
|
||||||
|
assert local.is_bad_key(KID, BAD)
|
||||||
|
assert not local.is_bad_key(audio, GOOD)
|
||||||
|
assert local.get_key(audio, "SVC") == GOOD # judged on the retry and copied
|
||||||
|
|
||||||
|
|
||||||
def test_only_checked_kids_are_copied_to_other_vaults(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_only_checked_kids_are_copied_to_other_vaults(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Both the mapped KID and the absent one are proven before they reach another vault."""
|
"""Both the mapped KID and the absent one are proven before they reach another vault."""
|
||||||
lead = UUID(int=7)
|
lead = UUID(int=7)
|
||||||
|
|||||||
@@ -4048,12 +4048,13 @@ class dl:
|
|||||||
|
|
||||||
backup = path.with_name(path.name + ".enc")
|
backup = path.with_name(path.name + ".enc")
|
||||||
backup.unlink(missing_ok=True) # a killed run leaves one behind; its bytes may not match
|
backup.unlink(missing_ok=True) # a killed run leaves one behind; its bytes may not match
|
||||||
if vault_kids() and decrypt and binaries.FFMPEG:
|
checking = bool(vault_kids() and decrypt and binaries.FFMPEG)
|
||||||
|
if checking:
|
||||||
try:
|
try:
|
||||||
os.link(path, backup)
|
os.link(path, backup)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.log.debug(f"Cannot keep the ciphertext for a decrypt retry: {e!r}")
|
self.log.debug(f"Cannot keep the ciphertext for a decrypt retry: {e!r}")
|
||||||
kid_map = verify.kid_windows(path) if vault_kids() and decrypt and binaries.FFMPEG else None
|
kid_map = verify.kid_windows(path) if checking else None
|
||||||
windows = kid_map.windows if kid_map else {}
|
windows = kid_map.windows if kid_map else {}
|
||||||
video = kid_map.video if kid_map else None
|
video = kid_map.video if kid_map else None
|
||||||
|
|
||||||
@@ -4064,11 +4065,13 @@ class dl:
|
|||||||
"""The vault KIDs whose content key did not decode.
|
"""The vault KIDs whose content key did not decode.
|
||||||
|
|
||||||
A KID in the KID map fails when one of its windows fails. The KIDs that are not in the
|
A KID in the KID map fails when one of its windows fails. The KIDs that are not in the
|
||||||
map share the verdict of one start and end check of the whole file.
|
map share the verdict of one start and end check of the whole file, taken only when
|
||||||
|
every mapped KID passed: a wrong mapped key fails that check too and would flag them
|
||||||
|
for noise that is not theirs. The retry judges them again with the replaced key.
|
||||||
"""
|
"""
|
||||||
failed = {kid for kid in kids if kid in windows and not all(window_decodes(*w) for w in windows[kid])}
|
failed = {kid for kid in kids if kid in windows and not all(window_decodes(*w) for w in windows[kid])}
|
||||||
absent = {kid for kid in kids if kid not in windows}
|
absent = {kid for kid in kids if kid not in windows}
|
||||||
if absent and not ffmpeg_decodes(path, video=video):
|
if absent and not failed and not ffmpeg_decodes(path, video=video):
|
||||||
failed |= absent
|
failed |= absent
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Key check on {path.name}: {len(kids) - len(absent)} KID(s) by fragment window, "
|
f"Key check on {path.name}: {len(kids) - len(absent)} KID(s) by fragment window, "
|
||||||
@@ -4198,6 +4201,11 @@ class dl:
|
|||||||
self.flush_vault_writes([partial(self.cache_keys_to_vaults, keys)])
|
self.flush_vault_writes([partial(self.cache_keys_to_vaults, keys)])
|
||||||
|
|
||||||
def cache_keys_to_vaults(self, content_keys: dict[UUID, str]) -> None:
|
def cache_keys_to_vaults(self, content_keys: dict[UUID, str]) -> None:
|
||||||
|
"""Store licence keys in every vault. A licence key clears a flag on its pair first: the
|
||||||
|
flag came from a decode check that was wrong, and a local vault refuses a flagged pair."""
|
||||||
|
for kid, key in content_keys.items():
|
||||||
|
if self.vaults.is_flagged(kid, key):
|
||||||
|
self.vaults.unflag_bad_key(kid, key)
|
||||||
successful_caches = self.vaults.add_keys(content_keys)
|
successful_caches = self.vaults.add_keys(content_keys)
|
||||||
keys, caches = self.vault_cache_tally or (0, successful_caches)
|
keys, caches = self.vault_cache_tally or (0, successful_caches)
|
||||||
self.vault_cache_tally = (keys + len(content_keys), min(caches, successful_caches))
|
self.vault_cache_tally = (keys + len(content_keys), min(caches, successful_caches))
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ def sbgp_runs(data: bytes, body: int) -> Optional[list[tuple[int, int]]]:
|
|||||||
return None
|
return None
|
||||||
pos = body + (12 if version == 1 else 8)
|
pos = body + (12 if version == 1 else 8)
|
||||||
count = struct.unpack_from(">I", data, pos)[0]
|
count = struct.unpack_from(">I", data, pos)[0]
|
||||||
|
if count * 8 > len(data) - pos - 4:
|
||||||
|
raise ValueError("sbgp entry count is larger than the box")
|
||||||
runs = struct.unpack_from(f">{count * 2}I", data, pos + 4)
|
runs = struct.unpack_from(f">{count * 2}I", data, pos + 4)
|
||||||
return list(zip(runs[::2], runs[1::2]))
|
return list(zip(runs[::2], runs[1::2]))
|
||||||
|
|
||||||
@@ -202,7 +204,7 @@ def kid_windows(path: Path) -> Optional[KidMap]:
|
|||||||
moov_groups[track] = groups
|
moov_groups[track] = groups
|
||||||
elif kind == b"moof" and box:
|
elif kind == b"moof" and box:
|
||||||
fragments.extend(read_fragment(box, entries, defaults, moov_groups, clock))
|
fragments.extend(read_fragment(box, entries, defaults, moov_groups, clock))
|
||||||
except (OSError, struct.error, IndexError, ValueError, MemoryError, OverflowError):
|
except (OSError, struct.error, IndexError, ValueError, MemoryError, OverflowError, RecursionError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tracks: dict[int, list[Fragment]] = {}
|
tracks: dict[int, list[Fragment]] = {}
|
||||||
@@ -232,15 +234,19 @@ def kid_windows(path: Path) -> Optional[KidMap]:
|
|||||||
return KidMap({kid: pick_windows(fragment_spans) for kid, fragment_spans in spans.items()}, video)
|
return KidMap({kid: pick_windows(fragment_spans) for kid, fragment_spans in spans.items()}, video)
|
||||||
|
|
||||||
|
|
||||||
def pick_windows(spans: list[tuple[float, float]], longest: float = 10.0) -> list[tuple[float, float]]:
|
def pick_windows(spans: list[tuple[float, float]], longest: float = 4.0) -> list[tuple[float, float]]:
|
||||||
"""Pick up to three windows from one KID's fragment (start, end) spans: at its first, middle and last fragment.
|
"""Pick up to three windows from one KID's fragment (start, end) spans: at its first, middle and last fragment.
|
||||||
|
|
||||||
A window starts at a fragment and runs on through the next fragments of the same KID, up
|
A window starts at a fragment and runs on through the next fragments of the same KID, up
|
||||||
to ``longest`` seconds, but never into a fragment of another KID or a clear one. A short
|
to ``longest`` seconds, but never into a fragment of another KID or a clear one. A short
|
||||||
window can decode noise without an error: the decoder often needs several frames to find it.
|
window can decode noise without an error: the decoder often needs several frames to find it.
|
||||||
|
A wrong key fails on the first inter frame in every measured case, so ``longest`` buys
|
||||||
|
decoder frames, not certainty; every second of it is decoded in full for a right key.
|
||||||
The last window ends where the KID's last run of fragments ends.
|
The last window ends where the KID's last run of fragments ends.
|
||||||
"""
|
"""
|
||||||
spans = sorted(spans)
|
spans = sorted(span for span in spans if span[1] > span[0]) # a duplicated tfdt gives an empty span
|
||||||
|
if not spans:
|
||||||
|
return []
|
||||||
runs: list[list[float]] = [] # [start, end] of each run of back-to-back fragments
|
runs: list[list[float]] = [] # [start, end] of each run of back-to-back fragments
|
||||||
for start, end in spans:
|
for start, end in spans:
|
||||||
if runs and abs(start - runs[-1][1]) <= 1e-3:
|
if runs and abs(start - runs[-1][1]) <= 1e-3:
|
||||||
@@ -252,7 +258,7 @@ def pick_windows(spans: list[tuple[float, float]], longest: float = 10.0) -> lis
|
|||||||
return next(end for run_start, end in runs if run_start <= start < end)
|
return next(end for run_start, end in runs if run_start <= start < end)
|
||||||
|
|
||||||
last_from = max(runs[-1][0], runs[-1][1] - longest)
|
last_from = max(runs[-1][0], runs[-1][1] - longest)
|
||||||
last = next(i for i, (start, _) in enumerate(spans) if start >= last_from - 1e-3)
|
last = max(i for i, (start, _) in enumerate(spans) if start <= last_from + 1e-3)
|
||||||
return [(spans[i][0], min(run_end(spans[i][0]), spans[i][0] + longest)) for i in sorted({0, len(spans) // 2, last})]
|
return [(spans[i][0], min(run_end(spans[i][0]), spans[i][0] + longest)) for i in sorted({0, len(spans) // 2, last})]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ def ffmpeg_decodes(path: Path, start: Optional[float] = None, seconds: float = 3
|
|||||||
|
|
||||||
With ``start``, the check decodes ``seconds`` from that time. An FFmpeg copy cuts the
|
With ``start``, the check decodes ``seconds`` from that time. An FFmpeg copy cuts the
|
||||||
window first, so no sample after the window gets to the decoder. Without ``start``, the
|
window first, so no sample after the window gets to the decoder. Without ``start``, the
|
||||||
check decodes the first seconds and the last 10 seconds, because a title can open with
|
check decodes the first seconds and the last 4 seconds, because a title can open with
|
||||||
a clear or separately keyed lead.
|
a clear or separately keyed lead.
|
||||||
|
|
||||||
Every frame is decoded, because an HEVC keyframe decrypted with a wrong key can decode
|
Every frame is decoded, because an HEVC keyframe decrypted with a wrong key can decode
|
||||||
@@ -97,8 +97,9 @@ def ffmpeg_decodes(path: Path, start: Optional[float] = None, seconds: float = 3
|
|||||||
timeout=FFMPEG_CHECK_TIMEOUT,
|
timeout=FFMPEG_CHECK_TIMEOUT,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# -ignore_editlist: the windows come from tfdt, and an elst would shift FFmpeg's clock off them
|
||||||
cut = subprocess.Popen(
|
cut = subprocess.Popen(
|
||||||
[ffmpeg, "-nostdin", "-v", "error", *window, "-i", str(path)]
|
[ffmpeg, "-nostdin", "-v", "error", "-ignore_editlist", "1", *window, "-i", str(path)]
|
||||||
+ ["-map", "0:v?", "-map", "0:a?", "-c", "copy", "-f", "nut", "pipe:1"],
|
+ ["-map", "0:v?", "-map", "0:a?", "-c", "copy", "-f", "nut", "pipe:1"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
@@ -131,7 +132,7 @@ def ffmpeg_decodes(path: Path, start: Optional[float] = None, seconds: float = 3
|
|||||||
|
|
||||||
if start is not None:
|
if start is not None:
|
||||||
return decode(["-ss", f"{start:.6f}", "-t", f"{max(seconds - 0.5, seconds / 2):.6f}"], keyframes=False)
|
return decode(["-ss", f"{start:.6f}", "-t", f"{max(seconds - 0.5, seconds / 2):.6f}"], keyframes=False)
|
||||||
return decode(["-t", str(seconds)], keyframes=False) and decode(["-sseof", "-10", "-t", "10"], keyframes=False)
|
return decode(["-t", str(seconds)], keyframes=False) and decode(["-sseof", "-4", "-t", "4"], keyframes=False)
|
||||||
|
|
||||||
|
|
||||||
def ffprobe(uri: Union[bytes, Path]) -> dict:
|
def ffprobe(uri: Union[bytes, Path]) -> dict:
|
||||||
|
|||||||
@@ -113,9 +113,7 @@ class SQLite(Vault):
|
|||||||
self.create_bad_keys_table()
|
self.create_bad_keys_table()
|
||||||
bad = set(self.conn_factory.get().execute("SELECT lower(`kid`), lower(`key_`) FROM `bad_keys`").fetchall())
|
bad = set(self.conn_factory.get().execute("SELECT lower(`kid`), lower(`key_`) FROM `bad_keys`").fetchall())
|
||||||
kid_keys = {
|
kid_keys = {
|
||||||
kid: key_
|
kid: key_ for kid, key_ in kid_keys.items() if (kid.replace("-", "").lower(), key_.lower()) not in bad
|
||||||
for kid, key_ in kid_keys.items()
|
|
||||||
if ((kid.hex if isinstance(kid, UUID) else kid.replace("-", "").lower()), key_.lower()) not in bad
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if not kid_keys:
|
if not kid_keys:
|
||||||
|
|||||||
Reference in New Issue
Block a user