Files
unshackle/unshackle/core/vault.py
T
imSp4rky 7c82282813 feat(vaults): flag and skip wrong content keys from poisoned vaults
Verify a vault-sourced key with an FFmpeg decode after decryption. On failure flag the KID:KEY locally with its source, restore the ciphertext, and retry through the remaining vaults and then the CDM.
2026-09-12 13:11:44 -06:00

68 lines
2.5 KiB
Python

from abc import ABCMeta, abstractmethod
from typing import Iterator, Optional, Union
from uuid import UUID
class Vault(metaclass=ABCMeta):
# a vault on this machine; Vaults.get_key tries these first, before the network ones
local: bool = False
def __init__(self, name: str, no_push: bool = False):
self.name = name
self.no_push = no_push
def __str__(self) -> str:
return f"{self.name} {type(self).__name__}"
@abstractmethod
def get_key(self, kid: Union[UUID, str], service: str) -> Optional[str]:
"""
Get the content key from the Vault by KID (Key ID) and Service.
It does not get the content key by PSSH, as the PSSH can be different depending on its
implementation, or even on how it was made. Some PSSH values can also be a CENC
Header rather than a PSSH MP4 Box, which makes the value even more confusingly different.
However, the KID never changes unless the video file itself has changed too, meaning the
content key for the presumed-matching KID would not work, further proving matching by KID
is superior.
"""
@abstractmethod
def get_keys(self, service: str) -> Iterator[tuple[str, str]]:
"""Get All Keys from Vault by Service."""
@abstractmethod
def add_key(self, service: str, kid: Union[UUID, str], key: str) -> bool:
"""Add KID:KEY to the Vault."""
@abstractmethod
def add_keys(self, service: str, kid_keys: dict[Union[UUID, str], str]) -> int:
"""
Add Multiple Content Keys with Key IDs for Service to the Vault.
The Vault ignores pre-existing Content Keys.
Raises PermissionError if the user has no permission to make the table.
"""
@abstractmethod
def get_services(self) -> Iterator[str]:
"""
Get a list of Service Tags from Vault.
Tags come back exactly as the Vault stores them, so passing one to
get_key/get_keys/add_key/add_keys reaches the namespace it came from.
"""
def is_bad_key(self, kid: Union[UUID, str], key: str) -> bool:
"""Return True if this Vault has flagged the KID:KEY as wrong."""
return False
def flag_bad_key(self, service: str, kid: Union[UUID, str], key: str, source: str) -> None:
"""Remember a KID:KEY that failed decryption and where it came from."""
def unflag_bad_key(self, kid: Union[UUID, str], key: str) -> None:
"""Forget a flagged KID:KEY, for example when a CDM licence confirmed it."""
__all__ = ("Vault",)