sdv2500/progettojava
0
1import argparse2import struct3import sys4from typing import Optional, Union5 6from Crypto.Cipher import AES7from collections import namedtuple8import array9 10CENCSampleAuxiliaryDataFormat = namedtuple("CENCSampleAuxiliaryDataFormat", ["is_encrypted", "iv", "sub_samples"])11 12 13class MP4Atom:14 """15 Represents an MP4 atom, which is a basic unit of data in an MP4 file.16 Each atom contains a header (size and type) and data.17 """18 19 __slots__ = ("atom_type", "size", "data")20 21 def __init__(self, atom_type: bytes, size: int, data: Union[memoryview, bytearray]):22 """23 Initializes an MP4Atom instance.24 25 Args:26 atom_type (bytes): The type of the atom.27 size (int): The size of the atom.28 data (Union[memoryview, bytearray]): The data contained in the atom.29 """30 self.atom_type = atom_type31 self.size = size32 self.data = data33 34 def __repr__(self):35 return f"<MP4Atom type={self.atom_type}, size={self.size}>"36 37 def pack(self):38 """39 Packs the atom into binary data.40 41 Returns:42 bytes: Packed binary data with size, type, and data.43 """44 return struct.pack(">I", self.size) + self.atom_type + self.data45 46 47class MP4Parser:48 """49 Parses MP4 data to extract atoms and their structure.50 """51 52 def __init__(self, data: memoryview):53 """54 Initializes an MP4Parser instance.55 56 Args:57 data (memoryview): The binary data of the MP4 file.58 """59 self.data = data60 self.position = 061 62 def read_atom(self) -> Optional[MP4Atom]:63 """64 Reads the next atom from the data.65 66 Returns:67 Optional[MP4Atom]: MP4Atom object or None if no more atoms are available.68 """69 pos = self.position70 if pos + 8 > len(self.data):71 return None72 73 size, atom_type = struct.unpack_from(">I4s", self.data, pos)74 pos += 875 76 if size == 1:77 if pos + 8 > len(self.data):78 return None79 size = struct.unpack_from(">Q", self.data, pos)[0]80 pos += 881 82 if size < 8 or pos + size - 8 > len(self.data):83 return None84 85 atom_data = self.data[pos : pos + size - 8]86 self.position = pos + size - 887 return MP4Atom(atom_type, size, atom_data)88 89 def list_atoms(self) -> list[MP4Atom]:90 """91 Lists all atoms in the data.92 93 Returns:94 list[MP4Atom]: List of MP4Atom objects.95 """96 atoms = []97 original_position = self.position98 self.position = 099 while self.position + 8 <= len(self.data):100 atom = self.read_atom()101 if not atom:102 break103 atoms.append(atom)104 self.position = original_position105 return atoms106 107 def _read_atom_at(self, pos: int, end: int) -> Optional[MP4Atom]:108 if pos + 8 > end:109 return None110 111 size, atom_type = struct.unpack_from(">I4s", self.data, pos)112 pos += 8113 114 if size == 1:115 if pos + 8 > end:116 return None117 size = struct.unpack_from(">Q", self.data, pos)[0]118 pos += 8119 120 if size < 8 or pos + size - 8 > end:121 return None122 123 atom_data = self.data[pos : pos + size - 8]124 return MP4Atom(atom_type, size, atom_data)125 126 def print_atoms_structure(self, indent: int = 0):127 """128 Prints the structure of all atoms in the data.129 130 Args:131 indent (int): The indentation level for printing.132 """133 pos = 0134 end = len(self.data)135 while pos + 8 <= end:136 atom = self._read_atom_at(pos, end)137 if not atom:138 break139 self.print_single_atom_structure(atom, pos, indent)140 pos += atom.size141 142 def print_single_atom_structure(self, atom: MP4Atom, parent_position: int, indent: int):143 """144 Prints the structure of a single atom.145 146 Args:147 atom (MP4Atom): The atom to print.148 parent_position (int): The position of the parent atom.149 indent (int): The indentation level for printing.150 """151 try:152 atom_type = atom.atom_type.decode("utf-8")153 except UnicodeDecodeError:154 atom_type = repr(atom.atom_type)155 print(" " * indent + f"Type: {atom_type}, Size: {atom.size}")156 157 child_pos = 0158 child_end = len(atom.data)159 while child_pos + 8 <= child_end:160 child_atom = self._read_atom_at(parent_position + 8 + child_pos, parent_position + 8 + child_end)161 if not child_atom:162 break163 self.print_single_atom_structure(child_atom, parent_position, indent + 2)164 child_pos += child_atom.size165 166 167class MP4Decrypter:168 """169 Class to handle the decryption of CENC encrypted MP4 segments.170 171 Attributes:172 key_map (dict[bytes, bytes]): Mapping of track IDs to decryption keys.173 current_key (Optional[bytes]): Current decryption key.174 trun_sample_sizes (array.array): Array of sample sizes from the 'trun' box.175 current_sample_info (list): List of sample information from the 'senc' box.176 encryption_overhead (int): Total size of encryption-related boxes.177 """178 179 def __init__(self, key_map: dict[bytes, bytes]):180 """181 Initializes the MP4Decrypter with a key map.182 183 Args:184 key_map (dict[bytes, bytes]): Mapping of track IDs to decryption keys.185 """186 self.key_map = key_map187 self.current_key = None188 self.trun_sample_sizes = array.array("I")189 self.current_sample_info = []190 self.encryption_overhead = 0191 192 def decrypt_segment(self, combined_segment: bytes) -> bytes:193 """194 Decrypts a combined MP4 segment.195 196 Args:197 combined_segment (bytes): Combined initialization and media segment.198 199 Returns:200 bytes: Decrypted segment content.201 """202 data = memoryview(combined_segment)203 parser = MP4Parser(data)204 atoms = parser.list_atoms()205 206 atom_process_order = [b"moov", b"moof", b"sidx", b"mdat"]207 208 processed_atoms = {}209 for atom_type in atom_process_order:210 if atom := next((a for a in atoms if a.atom_type == atom_type), None):211 processed_atoms[atom_type] = self._process_atom(atom_type, atom)212 213 result = bytearray()214 for atom in atoms:215 if atom.atom_type in processed_atoms:216 processed_atom = processed_atoms[atom.atom_type]217 result.extend(processed_atom.pack())218 else:219 result.extend(atom.pack())220 221 return bytes(result)222 223 def _process_atom(self, atom_type: bytes, atom: MP4Atom) -> MP4Atom:224 """225 Processes an MP4 atom based on its type.226 227 Args:228 atom_type (bytes): Type of the atom.229 atom (MP4Atom): The atom to process.230 231 Returns:232 MP4Atom: Processed atom.233 """234 if atom_type == b"moov":235 return self._process_moov(atom)236 elif atom_type == b"moof":237 return self._process_moof(atom)238 elif atom_type == b"sidx":239 return self._process_sidx(atom)240 elif atom_type == b"mdat":241 return self._decrypt_mdat(atom)242 else:243 return atom244 245 def _process_moov(self, moov: MP4Atom) -> MP4Atom:246 """247 Processes the 'moov' (Movie) atom, which contains metadata about the entire presentation.248 This includes information about tracks, media data, and other movie-level metadata.249 250 Args:251 moov (MP4Atom): The 'moov' atom to process.252 253 Returns:254 MP4Atom: Processed 'moov' atom with updated track information.255 """256 parser = MP4Parser(moov.data)257 new_moov_data = bytearray()258 259 for atom in iter(parser.read_atom, None):260 if atom.atom_type == b"trak":261 new_trak = self._process_trak(atom)262 new_moov_data.extend(new_trak.pack())263 elif atom.atom_type != b"pssh":264 # Skip PSSH boxes as they are not needed in the decrypted output265 new_moov_data.extend(atom.pack())266 267 return MP4Atom(b"moov", len(new_moov_data) + 8, new_moov_data)268 269 def _process_moof(self, moof: MP4Atom) -> MP4Atom:270 """271 Processes the 'moov' (Movie) atom, which contains metadata about the entire presentation.272 This includes information about tracks, media data, and other movie-level metadata.273 274 Args:275 moov (MP4Atom): The 'moov' atom to process.276 277 Returns:278 MP4Atom: Processed 'moov' atom with updated track information.279 """280 parser = MP4Parser(moof.data)281 new_moof_data = bytearray()282 283 for atom in iter(parser.read_atom, None):284 if atom.atom_type == b"traf":285 new_traf = self._process_traf(atom)286 new_moof_data.extend(new_traf.pack())287 else:288 new_moof_data.extend(atom.pack())289 290 return MP4Atom(b"moof", len(new_moof_data) + 8, new_moof_data)291 292 def _process_traf(self, traf: MP4Atom) -> MP4Atom:293 """294 Processes the 'traf' (Track Fragment) atom, which contains information about a track fragment.295 This includes sample information, sample encryption data, and other track-level metadata.296 297 Args:298 traf (MP4Atom): The 'traf' atom to process.299 300 Returns:301 MP4Atom: Processed 'traf' atom with updated sample information.302 """303 parser = MP4Parser(traf.data)304 new_traf_data = bytearray()305 tfhd = None306 sample_count = 0307 sample_info = []308 309 atoms = parser.list_atoms()310 311 # calculate encryption_overhead earlier to avoid dependency on trun312 self.encryption_overhead = sum(a.size for a in atoms if a.atom_type in {b"senc", b"saiz", b"saio"})313 314 for atom in atoms:315 if atom.atom_type == b"tfhd":316 tfhd = atom317 new_traf_data.extend(atom.pack())318 elif atom.atom_type == b"trun":319 sample_count = self._process_trun(atom)320 new_trun = self._modify_trun(atom)321 new_traf_data.extend(new_trun.pack())322 elif atom.atom_type == b"senc":323 # Parse senc but don't include it in the new decrypted traf data and similarly don't include saiz and saio324 sample_info = self._parse_senc(atom, sample_count)325 elif atom.atom_type not in {b"saiz", b"saio"}:326 new_traf_data.extend(atom.pack())327 328 if tfhd:329 tfhd_track_id = struct.unpack_from(">I", tfhd.data, 4)[0]330 self.current_key = self._get_key_for_track(tfhd_track_id)331 self.current_sample_info = sample_info332 333 return MP4Atom(b"traf", len(new_traf_data) + 8, new_traf_data)334 335 def _decrypt_mdat(self, mdat: MP4Atom) -> MP4Atom:336 """337 Decrypts the 'mdat' (Media Data) atom, which contains the actual media data (audio, video, etc.).338 The decryption is performed using the current decryption key and sample information.339 340 Args:341 mdat (MP4Atom): The 'mdat' atom to decrypt.342 343 Returns:344 MP4Atom: Decrypted 'mdat' atom with decrypted media data.345 """346 if not self.current_key or not self.current_sample_info:347 return mdat # Return original mdat if we don't have decryption info348 349 decrypted_samples = bytearray()350 mdat_data = mdat.data351 position = 0352 353 for i, info in enumerate(self.current_sample_info):354 if position >= len(mdat_data):355 break # No more data to process356 357 sample_size = self.trun_sample_sizes[i] if i < len(self.trun_sample_sizes) else len(mdat_data) - position358 sample = mdat_data[position : position + sample_size]359 position += sample_size360 decrypted_sample = self._process_sample(sample, info, self.current_key)361 decrypted_samples.extend(decrypted_sample)362 363 return MP4Atom(b"mdat", len(decrypted_samples) + 8, decrypted_samples)364 365 def _parse_senc(self, senc: MP4Atom, sample_count: int) -> list[CENCSampleAuxiliaryDataFormat]:366 """367 Parses the 'senc' (Sample Encryption) atom, which contains encryption information for samples.368 This includes initialization vectors (IVs) and sub-sample encryption data.369 370 Args:371 senc (MP4Atom): The 'senc' atom to parse.372 sample_count (int): The number of samples.373 374 Returns:375 list[CENCSampleAuxiliaryDataFormat]: List of sample auxiliary data formats with encryption information.376 """377 data = memoryview(senc.data)378 version_flags = struct.unpack_from(">I", data, 0)[0]379 version, flags = version_flags >> 24, version_flags & 0xFFFFFF380 position = 4381 382 if version == 0:383 sample_count = struct.unpack_from(">I", data, position)[0]384 position += 4385 386 sample_info = []387 for _ in range(sample_count):388 if position + 8 > len(data):389 break390 391 iv = data[position : position + 8].tobytes()392 position += 8393 394 sub_samples = []395 if flags & 0x000002 and position + 2 <= len(data): # Check if subsample information is present396 subsample_count = struct.unpack_from(">H", data, position)[0]397 position += 2398 399 for _ in range(subsample_count):400 if position + 6 <= len(data):401 clear_bytes, encrypted_bytes = struct.unpack_from(">HI", data, position)402 position += 6403 sub_samples.append((clear_bytes, encrypted_bytes))404 else:405 break406 407 sample_info.append(CENCSampleAuxiliaryDataFormat(True, iv, sub_samples))408 409 return sample_info410 411 def _get_key_for_track(self, track_id: int) -> bytes:412 """413 Retrieves the decryption key for a given track ID from the key map.414 415 Args:416 track_id (int): The track ID.417 418 Returns:419 bytes: The decryption key for the specified track ID.420 """421 if len(self.key_map) == 1:422 return next(iter(self.key_map.values()))423 key = self.key_map.get(track_id.pack(4, "big"))424 if not key:425 raise ValueError(f"No key found for track ID {track_id}")426 return key427 428 @staticmethod429 def _process_sample(430 sample: memoryview, sample_info: CENCSampleAuxiliaryDataFormat, key: bytes431 ) -> Union[memoryview, bytearray, bytes]:432 """433 Processes and decrypts a sample using the provided sample information and decryption key.434 This includes handling sub-sample encryption if present.435 436 Args:437 sample (memoryview): The sample data.438 sample_info (CENCSampleAuxiliaryDataFormat): The sample auxiliary data format with encryption information.439 key (bytes): The decryption key.440 441 Returns:442 Union[memoryview, bytearray, bytes]: The decrypted sample.443 """444 if not sample_info.is_encrypted:445 return sample446 447 # pad IV to 16 bytes448 iv = sample_info.iv + b"\x00" * (16 - len(sample_info.iv))449 cipher = AES.new(key, AES.MODE_CTR, initial_value=iv, nonce=b"")450 451 if not sample_info.sub_samples:452 # If there are no sub_samples, decrypt the entire sample453 return cipher.decrypt(sample)454 455 result = bytearray()456 offset = 0457 for clear_bytes, encrypted_bytes in sample_info.sub_samples:458 result.extend(sample[offset : offset + clear_bytes])459 offset += clear_bytes460 result.extend(cipher.decrypt(sample[offset : offset + encrypted_bytes]))461 offset += encrypted_bytes462 463 # If there's any remaining data, treat it as encrypted464 if offset < len(sample):465 result.extend(cipher.decrypt(sample[offset:]))466 467 return result468 469 def _process_trun(self, trun: MP4Atom) -> int:470 """471 Processes the 'trun' (Track Fragment Run) atom, which contains information about the samples in a track fragment.472 This includes sample sizes, durations, flags, and composition time offsets.473 474 Args:475 trun (MP4Atom): The 'trun' atom to process.476 477 Returns:478 int: The number of samples in the 'trun' atom.479 """480 trun_flags, sample_count = struct.unpack_from(">II", trun.data, 0)481 data_offset = 8482 483 if trun_flags & 0x000001:484 data_offset += 4485 if trun_flags & 0x000004:486 data_offset += 4487 488 self.trun_sample_sizes = array.array("I")489 490 for _ in range(sample_count):491 if trun_flags & 0x000100: # sample-duration-present flag492 data_offset += 4493 if trun_flags & 0x000200: # sample-size-present flag494 sample_size = struct.unpack_from(">I", trun.data, data_offset)[0]495 self.trun_sample_sizes.append(sample_size)496 data_offset += 4497 else:498 self.trun_sample_sizes.append(0) # Using 0 instead of None for uniformity in the array499 if trun_flags & 0x000400: # sample-flags-present flag500 data_offset += 4501 if trun_flags & 0x000800: # sample-composition-time-offsets-present flag502 data_offset += 4503 504 return sample_count505 506 def _modify_trun(self, trun: MP4Atom) -> MP4Atom:507 """508 Modifies the 'trun' (Track Fragment Run) atom to update the data offset.509 This is necessary to account for the encryption overhead.510 511 Args:512 trun (MP4Atom): The 'trun' atom to modify.513 514 Returns:515 MP4Atom: Modified 'trun' atom with updated data offset.516 """517 trun_data = bytearray(trun.data)518 current_flags = struct.unpack_from(">I", trun_data, 0)[0] & 0xFFFFFF519 520 # If the data-offset-present flag is set, update the data offset to account for encryption overhead521 if current_flags & 0x000001:522 current_data_offset = struct.unpack_from(">i", trun_data, 8)[0]523 struct.pack_into(">i", trun_data, 8, current_data_offset - self.encryption_overhead)524 525 return MP4Atom(b"trun", len(trun_data) + 8, trun_data)526 527 def _process_sidx(self, sidx: MP4Atom) -> MP4Atom:528 """529 Processes the 'sidx' (Segment Index) atom, which contains indexing information for media segments.530 This includes references to media segments and their durations.531 532 Args:533 sidx (MP4Atom): The 'sidx' atom to process.534 535 Returns:536 MP4Atom: Processed 'sidx' atom with updated segment references.537 """538 sidx_data = bytearray(sidx.data)539 540 current_size = struct.unpack_from(">I", sidx_data, 32)[0]541 reference_type = current_size >> 31542 current_referenced_size = current_size & 0x7FFFFFFF543 544 # Remove encryption overhead from referenced size545 new_referenced_size = current_referenced_size - self.encryption_overhead546 new_size = (reference_type << 31) | new_referenced_size547 struct.pack_into(">I", sidx_data, 32, new_size)548 549 return MP4Atom(b"sidx", len(sidx_data) + 8, sidx_data)550 551 def _process_trak(self, trak: MP4Atom) -> MP4Atom:552 """553 Processes the 'trak' (Track) atom, which contains information about a single track in the movie.554 This includes track header, media information, and other track-level metadata.555 556 Args:557 trak (MP4Atom): The 'trak' atom to process.558 559 Returns:560 MP4Atom: Processed 'trak' atom with updated track information.561 """562 parser = MP4Parser(trak.data)563 new_trak_data = bytearray()564 565 for atom in iter(parser.read_atom, None):566 if atom.atom_type == b"mdia":567 new_mdia = self._process_mdia(atom)568 new_trak_data.extend(new_mdia.pack())569 else:570 new_trak_data.extend(atom.pack())571 572 return MP4Atom(b"trak", len(new_trak_data) + 8, new_trak_data)573 574 def _process_mdia(self, mdia: MP4Atom) -> MP4Atom:575 """576 Processes the 'mdia' (Media) atom, which contains media information for a track.577 This includes media header, handler reference, and media information container.578 579 Args:580 mdia (MP4Atom): The 'mdia' atom to process.581 582 Returns:583 MP4Atom: Processed 'mdia' atom with updated media information.584 """585 parser = MP4Parser(mdia.data)586 new_mdia_data = bytearray()587 588 for atom in iter(parser.read_atom, None):589 if atom.atom_type == b"minf":590 new_minf = self._process_minf(atom)591 new_mdia_data.extend(new_minf.pack())592 else:593 new_mdia_data.extend(atom.pack())594 595 return MP4Atom(b"mdia", len(new_mdia_data) + 8, new_mdia_data)596 597 def _process_minf(self, minf: MP4Atom) -> MP4Atom:598 """599 Processes the 'minf' (Media Information) atom, which contains information about the media data in a track.600 This includes data information, sample table, and other media-level metadata.601 602 Args:603 minf (MP4Atom): The 'minf' atom to process.604 605 Returns:606 MP4Atom: Processed 'minf' atom with updated media information.607 """608 parser = MP4Parser(minf.data)609 new_minf_data = bytearray()610 611 for atom in iter(parser.read_atom, None):612 if atom.atom_type == b"stbl":613 new_stbl = self._process_stbl(atom)614 new_minf_data.extend(new_stbl.pack())615 else:616 new_minf_data.extend(atom.pack())617 618 return MP4Atom(b"minf", len(new_minf_data) + 8, new_minf_data)619 620 def _process_stbl(self, stbl: MP4Atom) -> MP4Atom:621 """622 Processes the 'stbl' (Sample Table) atom, which contains information about the samples in a track.623 This includes sample descriptions, sample sizes, sample times, and other sample-level metadata.624 625 Args:626 stbl (MP4Atom): The 'stbl' atom to process.627 628 Returns:629 MP4Atom: Processed 'stbl' atom with updated sample information.630 """631 parser = MP4Parser(stbl.data)632 new_stbl_data = bytearray()633 634 for atom in iter(parser.read_atom, None):635 if atom.atom_type == b"stsd":636 new_stsd = self._process_stsd(atom)637 new_stbl_data.extend(new_stsd.pack())638 else:639 new_stbl_data.extend(atom.pack())640 641 return MP4Atom(b"stbl", len(new_stbl_data) + 8, new_stbl_data)642 643 def _process_stsd(self, stsd: MP4Atom) -> MP4Atom:644 """645 Processes the 'stsd' (Sample Description) atom, which contains descriptions of the sample entries in a track.646 This includes codec information, sample entry details, and other sample description metadata.647 648 Args:649 stsd (MP4Atom): The 'stsd' atom to process.650 651 Returns:652 MP4Atom: Processed 'stsd' atom with updated sample descriptions.653 """654 parser = MP4Parser(stsd.data)655 entry_count = struct.unpack_from(">I", parser.data, 4)[0]656 new_stsd_data = bytearray(stsd.data[:8])657 658 parser.position = 8 # Move past version_flags and entry_count659 660 for _ in range(entry_count):661 sample_entry = parser.read_atom()662 if not sample_entry:663 break664 665 processed_entry = self._process_sample_entry(sample_entry)666 new_stsd_data.extend(processed_entry.pack())667 668 return MP4Atom(b"stsd", len(new_stsd_data) + 8, new_stsd_data)669 670 def _process_sample_entry(self, entry: MP4Atom) -> MP4Atom:671 """672 Processes a sample entry atom, which contains information about a specific type of sample.673 This includes codec-specific information and other sample entry details.674 675 Args:676 entry (MP4Atom): The sample entry atom to process.677 678 Returns:679 MP4Atom: Processed sample entry atom with updated information.680 """681 # Determine the size of fixed fields based on sample entry type682 if entry.atom_type in {b"mp4a", b"enca"}:683 fixed_size = 28 # 8 bytes for size, type and reserved, 20 bytes for fixed fields in Audio Sample Entry.684 elif entry.atom_type in {b"mp4v", b"encv", b"avc1", b"hev1", b"hvc1"}:685 fixed_size = 78 # 8 bytes for size, type and reserved, 70 bytes for fixed fields in Video Sample Entry.686 else:687 fixed_size = 16 # 8 bytes for size, type and reserved, 8 bytes for fixed fields in other Sample Entries.688 689 new_entry_data = bytearray(entry.data[:fixed_size])690 parser = MP4Parser(entry.data[fixed_size:])691 codec_format = None692 693 for atom in iter(parser.read_atom, None):694 if atom.atom_type in {b"sinf", b"schi", b"tenc", b"schm"}:695 if atom.atom_type == b"sinf":696 codec_format = self._extract_codec_format(atom)697 continue # Skip encryption-related atoms698 new_entry_data.extend(atom.pack())699 700 # Replace the atom type with the extracted codec format701 new_type = codec_format if codec_format else entry.atom_type702 return MP4Atom(new_type, len(new_entry_data) + 8, new_entry_data)703 704 def _extract_codec_format(self, sinf: MP4Atom) -> Optional[bytes]:705 """706 Extracts the codec format from the 'sinf' (Protection Scheme Information) atom.707 This includes information about the original format of the protected content.708 709 Args:710 sinf (MP4Atom): The 'sinf' atom to extract from.711 712 Returns:713 Optional[bytes]: The codec format or None if not found.714 """715 parser = MP4Parser(sinf.data)716 for atom in iter(parser.read_atom, None):717 if atom.atom_type == b"frma":718 return atom.data719 return None720 721 722def decrypt_segment(init_segment: bytes, segment_content: bytes, key_id: str, key: str) -> bytes:723 """724 Decrypts a CENC encrypted MP4 segment.725 726 Args:727 init_segment (bytes): Initialization segment data.728 segment_content (bytes): Encrypted segment content.729 key_id (str): Key ID in hexadecimal format.730 key (str): Key in hexadecimal format.731 """732 key_map = {bytes.fromhex(key_id): bytes.fromhex(key)}733 decrypter = MP4Decrypter(key_map)734 decrypted_content = decrypter.decrypt_segment(init_segment + segment_content)735 return decrypted_content736 737 738def cli():739 """740 Command line interface for decrypting a CENC encrypted MP4 segment.741 """742 init_segment = b""743 744 if args.init and args.segment:745 with open(args.init, "rb") as f:746 init_segment = f.read()747 with open(args.segment, "rb") as f:748 segment_content = f.read()749 elif args.combined_segment:750 with open(args.combined_segment, "rb") as f:751 segment_content = f.read()752 else:753 print("Usage: python mp4decrypt.py --help")754 sys.exit(1)755 756 try:757 decrypted_segment = decrypt_segment(init_segment, segment_content, args.key_id, args.key)758 print(f"Decrypted content size is {len(decrypted_segment)} bytes")759 with open(args.output, "wb") as f:760 f.write(decrypted_segment)761 print(f"Decrypted segment written to {args.output}")762 except Exception as e:763 print(f"Error: {e}")764 sys.exit(1)765 766 767if __name__ == "__main__":768 arg_parser = argparse.ArgumentParser(description="Decrypts a MP4 init and media segment using CENC encryption.")769 arg_parser.add_argument("--init", help="Path to the init segment file", required=False)770 arg_parser.add_argument("--segment", help="Path to the media segment file", required=False)771 arg_parser.add_argument(772 "--combined_segment", help="Path to the combined init and media segment file", required=False773 )774 arg_parser.add_argument("--key_id", help="Key ID in hexadecimal format", required=True)775 arg_parser.add_argument("--key", help="Key in hexadecimal format", required=True)776 arg_parser.add_argument("--output", help="Path to the output file", required=True)777 args = arg_parser.parse_args()778 cli()