mirror of
				https://github.com/devine-dl/pywidevine.git
				synced 2025-11-04 03:44:50 +00:00 
			
		
		
		
	Add Widevine Device (.WVD) Class
This commit is contained in:
		
							parent
							
								
									35ccd2f393
								
							
						
					
					
						commit
						5c9d4cda73
					
				
							
								
								
									
										142
									
								
								pywidevine/device.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								pywidevine/device.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,142 @@
 | 
				
			|||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import base64
 | 
				
			||||||
 | 
					from enum import Enum
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from typing import Any, Optional, Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from construct import BitStruct, Bytes, Const
 | 
				
			||||||
 | 
					from construct import Enum as CEnum
 | 
				
			||||||
 | 
					from construct import Flag, Int8ub, Int16ub
 | 
				
			||||||
 | 
					from construct import Optional as COptional
 | 
				
			||||||
 | 
					from construct import Padded, Padding, Struct, this
 | 
				
			||||||
 | 
					from Crypto.PublicKey import RSA
 | 
				
			||||||
 | 
					from google.protobuf.message import DecodeError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pywidevine.license_protocol_pb2 import ClientIdentification, FileHashes, SignedDrmCertificate, DrmCertificate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _Types(Enum):
 | 
				
			||||||
 | 
					    CHROME = 1
 | 
				
			||||||
 | 
					    ANDROID = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Device:
 | 
				
			||||||
 | 
					    # needed so bin_format can enumerate the types
 | 
				
			||||||
 | 
					    Types = _Types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bin_format = Struct(
 | 
				
			||||||
 | 
					        "signature" / Const(b"WVD"),
 | 
				
			||||||
 | 
					        "version" / Const(Int8ub, 2),
 | 
				
			||||||
 | 
					        "type_" / CEnum(
 | 
				
			||||||
 | 
					            Int8ub,
 | 
				
			||||||
 | 
					            **{t.name: t.value for t in _Types}
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        "security_level" / Int8ub,
 | 
				
			||||||
 | 
					        "flags" / Padded(1, COptional(BitStruct(
 | 
				
			||||||
 | 
					            Padding(7),
 | 
				
			||||||
 | 
					            "send_key_control_nonce" / Flag  # deprecated, do not use
 | 
				
			||||||
 | 
					        ))),
 | 
				
			||||||
 | 
					        "private_key_len" / Int16ub,
 | 
				
			||||||
 | 
					        "private_key" / Bytes(this.private_key_len),
 | 
				
			||||||
 | 
					        "client_id_len" / Int16ub,
 | 
				
			||||||
 | 
					        "client_id" / Bytes(this.client_id_len)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # == Bin Format Revisions == #
 | 
				
			||||||
 | 
					    # Version 2: Removed vmp and vmp_len as it should already be within the Client ID
 | 
				
			||||||
 | 
					    # Version 1: Removed system_id as it can be retrieved from the Client ID's DRM Certificate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        *_: Any,
 | 
				
			||||||
 | 
					        type_: Types,
 | 
				
			||||||
 | 
					        security_level: int,
 | 
				
			||||||
 | 
					        flags: Optional[dict],
 | 
				
			||||||
 | 
					        private_key: Optional[bytes],
 | 
				
			||||||
 | 
					        client_id: Optional[bytes],
 | 
				
			||||||
 | 
					        **__: Any
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This is the device key data that is needed for the CDM (Content Decryption Module).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Parameters:
 | 
				
			||||||
 | 
					            type_: Device Type
 | 
				
			||||||
 | 
					            security_level: Security level from 1 (the highest ranking) to 3 (the lowest ranking)
 | 
				
			||||||
 | 
					            flags: Extra flags
 | 
				
			||||||
 | 
					            private_key: Device Private Key
 | 
				
			||||||
 | 
					            client_id: Device Client Identification Blob
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        # *_,*__ is to ignore unwanted args, like signature and version from the struct
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not client_id:
 | 
				
			||||||
 | 
					            raise ValueError("Client ID is required, the WVD does not contain one or is malformed.")
 | 
				
			||||||
 | 
					        if not private_key:
 | 
				
			||||||
 | 
					            raise ValueError("Private Key is required, the WVD does not contain one or is malformed.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.type = self.Types[type_] if isinstance(type_, str) else type_
 | 
				
			||||||
 | 
					        self.security_level = security_level
 | 
				
			||||||
 | 
					        self.flags = flags
 | 
				
			||||||
 | 
					        self.private_key = RSA.importKey(private_key)
 | 
				
			||||||
 | 
					        self.client_id = ClientIdentification()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.client_id.ParseFromString(client_id)
 | 
				
			||||||
 | 
					        except DecodeError:
 | 
				
			||||||
 | 
					            raise ValueError("Failed to parse client_id as a ClientIdentification")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.vmp = FileHashes()
 | 
				
			||||||
 | 
					        if self.client_id.vmp_data:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                self.vmp.ParseFromString(self.client_id.vmp_data)
 | 
				
			||||||
 | 
					            except DecodeError:
 | 
				
			||||||
 | 
					                raise ValueError("Failed to parse Client ID's VMP data as a FileHashes")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        signed_drm_certificate = SignedDrmCertificate()
 | 
				
			||||||
 | 
					        signed_drm_certificate.ParseFromString(self.client_id.token)
 | 
				
			||||||
 | 
					        drm_certificate = DrmCertificate()
 | 
				
			||||||
 | 
					        drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
 | 
				
			||||||
 | 
					        self.system_id = drm_certificate.system_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __repr__(self) -> str:
 | 
				
			||||||
 | 
					        return "{name}({items})".format(
 | 
				
			||||||
 | 
					            name=self.__class__.__name__,
 | 
				
			||||||
 | 
					            items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()])
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def loads(cls, data: Union[bytes, str]) -> Device:
 | 
				
			||||||
 | 
					        if isinstance(data, str):
 | 
				
			||||||
 | 
					            data = base64.b64decode(data)
 | 
				
			||||||
 | 
					        if not isinstance(data, bytes):
 | 
				
			||||||
 | 
					            raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}")
 | 
				
			||||||
 | 
					        return cls(**cls.bin_format.parse(data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def load(cls, path: Union[Path, str]) -> Device:
 | 
				
			||||||
 | 
					        if not isinstance(path, (Path, str)):
 | 
				
			||||||
 | 
					            raise ValueError(f"Expecting Path object or path string, got {path!r}")
 | 
				
			||||||
 | 
					        with Path(path).open(mode="rb") as f:
 | 
				
			||||||
 | 
					            return cls(**cls.bin_format.parse_stream(f))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def dumps(self) -> bytes:
 | 
				
			||||||
 | 
					        private_key = self.private_key.export_key("DER") if self.private_key else None
 | 
				
			||||||
 | 
					        return self.bin_format.build(dict(
 | 
				
			||||||
 | 
					            version=2,
 | 
				
			||||||
 | 
					            type=self.type.value,
 | 
				
			||||||
 | 
					            security_level=self.security_level,
 | 
				
			||||||
 | 
					            flags=self.flags,
 | 
				
			||||||
 | 
					            private_key_len=len(private_key) if private_key else 0,
 | 
				
			||||||
 | 
					            private_key=private_key,
 | 
				
			||||||
 | 
					            client_id_len=len(self.client_id.SerializeToString()) if self.client_id else 0,
 | 
				
			||||||
 | 
					            client_id=self.client_id.SerializeToString() if self.client_id else None
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def dump(self, path: Union[Path, str]) -> None:
 | 
				
			||||||
 | 
					        if not isinstance(path, (Path, str)):
 | 
				
			||||||
 | 
					            raise ValueError(f"Expecting Path object or path string, got {path!r}")
 | 
				
			||||||
 | 
					        path = Path(path)
 | 
				
			||||||
 | 
					        path.parent.mkdir(parents=True, exist_ok=True)
 | 
				
			||||||
 | 
					        path.write_bytes(self.dumps())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					__ALL__ = (Device,)
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user