├── README.md ├── applemusic.py ├── binaries ├── aria2c.exe ├── ffmpeg.exe ├── mkvmerge.exe ├── mp4decrypt.exe └── packager.exe ├── config └── __init__.py ├── cookies └── cookies.txt ├── pywidevine └── L3 │ ├── __init__.py │ ├── cdm │ ├── __init__.py │ ├── cdm.py │ ├── deviceconfig.py │ ├── formats │ │ ├── __init__.py │ │ ├── wv_proto2.proto │ │ ├── wv_proto2_pb2.py │ │ ├── wv_proto3.proto │ │ └── wv_proto3_pb2.py │ ├── key.py │ ├── session.py │ └── vmp.py │ ├── decrypt │ ├── __init__.py │ ├── wvdecrypt.py │ ├── wvdecryptconfig.py │ └── wvdecryptcustom.py │ └── getPSSH.py └── yt-dlp.exe /README.md: -------------------------------------------------------------------------------- 1 | ## AppleMusic-Downloader 2 | Download videos and audios from AppleMusic! 3 | 4 | ## How to use 5 | 6 | * python applemusic.py https://music.apple.com/id/album/lyodra/1575729416?l=id&ls 7 | 8 | ![AM-DL](https://i.ibb.co/RBggYDp/unknown-5.png) 9 | 10 | I thank the people who did this incredible job. 11 | I recommend you to follow the users, they have good projects in their accounts: 12 | * [ReiDoBregaBR](https://github.com/ReiDoBrega) Design the script with a new layout 13 | * [Puyodead1](https://github.com/Puyodead1) Original base script 14 | 15 | ## If you're wondering why I just posted it publicly, it's because this was leaked to a lot of people when it was posted in the discord group anyway 16 | (Anyway this was an open project kek) 17 | 18 | I know it has nothing to do with what I said above but fuck WVLeaks 19 | 20 | -------------------------------------------------------------------------------- /applemusic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | import sys 6 | import time 7 | import json 8 | import m3u8 9 | import base64 10 | import shutil 11 | import urllib3 12 | import requests 13 | import argparse 14 | import music_tag 15 | import webbrowser 16 | import subprocess 17 | import unicodedata 18 | import pathvalidate 19 | 20 | import config as toolcfg 21 | 22 | from unidecode import unidecode 23 | from urllib.parse import unquote 24 | from subprocess import CalledProcessError 25 | from coloredlogs import ColoredFormatter, logging 26 | from pywidevine.L3.decrypt.wvdecrypt import WvDecrypt 27 | from pywidevine.L3.decrypt.wvdecryptconfig import WvDecryptConfig 28 | from pywidevine.L3.cdm.formats.widevine_pssh_data_pb2 import WidevinePsshData 29 | from colorama import init 30 | 31 | REGEX = re.compile(r"//music\.apple\.com/.*/(?P.*)/(?P.*)/(?P\d*)[\?i=]*(?P\d*)?$") 32 | 33 | 34 | init(autoreset=True) 35 | 36 | BANNER = """ 37 | ___ __ __ ___ _ ____ _ 38 | / | ____ ____ / /__ / |/ /_ _______(_)____ / __ \(_)___ ____ ___ _____ 39 | / /| | / __ \/ __ \/ / _ \ / /|_/ / / / / ___/ / ___/ / /_/ / / __ \/ __ \/ _ \/ ___/ 40 | / ___ |/ /_/ / /_/ / / __/ / / / / /_/ (__ ) / /__ / _, _/ / /_/ / /_/ / __/ / 41 | /_/ |_/ .___/ .___/_/\___/ /_/ /_/\__,_/____/_/\___/ /_/ |_/_/ .___/ .___/\___/_/ 42 | /_/ /_/ /_/ /_/ 43 | 44 | > REMAKE By ReiDoBregaBR 45 | > SOURCE By Puyodead1 46 | > VERSION 2.0.0 47 | """ 48 | class VideoTrack: 49 | 50 | def __init__(self, type_, url, uri, aid, audio_only): 51 | self.type = type_ 52 | self.url = url 53 | self.uri = uri 54 | self.aid = aid 55 | self.audio_only = audio_only 56 | 57 | def get_type(self): 58 | return 'video' 59 | 60 | def get_filename(self, unformatted_filename): 61 | return unformatted_filename.format(filename=self.filename, track_type='video', track_no='0') 62 | 63 | class AudioTrack: 64 | 65 | def __init__(self, type_, url, uri, aid, audio_only): 66 | self.type = type_ 67 | self.url = url 68 | self.uri = uri 69 | self.aid = aid 70 | self.audio_only = audio_only 71 | 72 | def get_type(self): 73 | return 'audio' 74 | 75 | def get_filename(self, unformatted_filename): 76 | return unformatted_filename.format(filename=self.filename, track_type='audio', track_no='0') 77 | 78 | def checkIfInt(str): 79 | try: 80 | x = int(str) 81 | return x 82 | except ValueError: 83 | return 84 | 85 | def checkIfBoolean(str): 86 | try: 87 | x = int(str) 88 | if x in [0,1]: 89 | if x == 0: 90 | return False 91 | elif x == 1: 92 | return True 93 | else: 94 | return 95 | except ValueError: 96 | aslower = str.lower() 97 | if aslower in ['yes','on','true']: 98 | return True 99 | elif aslower in ['no','off','false']: 100 | return False 101 | else: 102 | return 103 | 104 | class Bunch(object): 105 | def __init__(self, adict, bdict): 106 | def handledict(anydict): 107 | newdict = {} 108 | for idx, name in enumerate(list(anydict.keys())): 109 | newname = name.replace('-','_') 110 | newdict[newname] = list(anydict.values())[idx] 111 | return newdict 112 | newadict = handledict(adict) 113 | for item in newadict: 114 | if item in ['skip_cleanup','debug','keys']: 115 | bool = checkIfBoolean(newadict[item]) 116 | if bool is not None: 117 | newadict[item] = bool 118 | else: 119 | print(f'ERROR: Config param {item!r} has to be boolean value') 120 | sys.exit(2) 121 | if item in ['title', 'track','track_start']: 122 | int = checkIfInt(newadict[item]) 123 | if int is not None: 124 | newadict[item] = int 125 | else: 126 | print(f'ERROR: Config param {item!r} has to be int value') 127 | sys.exit(2) 128 | newbdict = handledict(bdict) 129 | self.__dict__.update(newadict) 130 | #~ print(self.__dict__) 131 | for item in newbdict: 132 | if item not in self.__dict__: 133 | self.__dict__[item] = newbdict[item] 134 | elif newbdict[item] != None and newbdict[item] != False: 135 | self.__dict__[item] = newbdict[item] 136 | 137 | class AppleMusicClient: 138 | 139 | def __init__(self): 140 | self.log = logging.getLogger(__name__) 141 | self.args = args 142 | self.args.url = self._title(args.url) 143 | self.session = self.client_session() 144 | self.audio_only = True if self.content_type != 'music-video' else False 145 | self.contents_to_be_ripped = [] 146 | 147 | def _title(self, title): 148 | matches = re.search(REGEX, title) 149 | if not matches: 150 | try: 151 | self.url_track_id = None 152 | self.content_type = 'album' 153 | if 'playlist' in title: 154 | self.content_type = 'playlist' 155 | elif '?i=' in title: 156 | self.content_type = 'track' 157 | args.url = title.split('/')[-1] 158 | except: 159 | self.log.fatal("[-] Invalid URL..") 160 | exit(1) 161 | return args.url 162 | 163 | self.content_type = matches.group(1) 164 | self.url_name = matches.group(2) 165 | self.url_main_id = matches.group(3) 166 | self.url_track_id = matches.group(4) 167 | 168 | try: 169 | if self.content_type == "album" and self.url_track_id: 170 | args.url = self.url_track_id 171 | elif self.content_type == "album": 172 | args.url = self.url_main_id 173 | elif self.content_type == "music-video": 174 | args.url = self.url_main_id 175 | else: 176 | self.log.fatal("[-] Invalid URL: Only Songs, Albums, and Music Videos are supported") 177 | except Exception as e: 178 | self.log.exception(f"[-] Error: {e}") 179 | 180 | return args.url 181 | 182 | def cookies_(self): 183 | cookies = {} 184 | APPLE_MUSIC_COOKIES = os.path.join(toolcfg.folder.cookies, 'cookies.txt') 185 | if os.path.exists(APPLE_MUSIC_COOKIES) and os.path.isfile(APPLE_MUSIC_COOKIES): 186 | with open(APPLE_MUSIC_COOKIES, "r") as f: 187 | for l in f: 188 | if not re.match(r"^#", l) and not re.match(r"^\n", l): 189 | line_fields = l.strip().replace('"', '"').split('\t') 190 | cookies[line_fields[5]] = line_fields[6] 191 | else: 192 | self.log.info(f"[+] Vistit {toolcfg.endpoints.home} And Export Cookies From Your Account and Put in Folder..") 193 | self.log.info("[+] After Add The Cookies, Back To Window And Press Enter") 194 | time.sleep(5) 195 | webbrowser.open(toolcfg.endpoints.home) 196 | input("continue ?") 197 | return self.cookies_() 198 | return cookies 199 | 200 | def client_session(self): 201 | self.session = requests.Session() 202 | self.session.verify = False 203 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 204 | self.session.headers.update({ 205 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0"}) 206 | self.session.cookies.update(self.cookies_()) 207 | 208 | token_file = os.path.join(toolcfg.folder.cookies, 'token.txt') 209 | if not os.path.exists(token_file) or not os.path.isfile(token_file): 210 | token = re.search(r'"token":"(.+?)"', unquote(self.session.get(toolcfg.endpoints.home).content.decode())) 211 | if token: 212 | token = token.group(1) 213 | with open(token_file, "w") as f: 214 | f.write(token) 215 | else: 216 | with open(token_file, "r") as f: 217 | token = f.read().strip() 218 | 219 | if not token: 220 | self.log.fatal("[-] Couldn't Find Token From The Homepage, Cookies May Be Invalid.") 221 | exit(1) 222 | 223 | self.session.headers.update({ 224 | "Accept": "application/json", 225 | "Accept-Language": "en-US,en;q=0.5", 226 | "Accept-Encoding": "gzip, deflate, br", 227 | "authorization": f"Bearer {token}", 228 | "content-type": "application/json", 229 | "x-apple-music-user-token": self.session.cookies.get_dict()["media-user-token"], 230 | "x-apple-renewal": "true", 231 | "DNT": "1", 232 | "Connection": "keep-alive", 233 | "Sec-Fetch-Dest": "empty", 234 | "Sec-Fetch-Mode": "cors", 235 | "Sec-Fetch-Site": "same-site" 236 | }) 237 | return self.session 238 | 239 | # Album Music/Single Track 240 | def fetch_music_metadata(self, track_id): 241 | 242 | data = self.fetch_metadata(track_id) 243 | 244 | music = data["songList"][0] 245 | 246 | song_assets = next((x for x in music["assets"] if x["flavor"] == "28:ctrp256"), None) 247 | if not song_assets: 248 | raise Exception("Failed to find 28:ctrp256 asset") 249 | metadata = song_assets["metadata"] 250 | metadata['playback'] = song_assets["URL"] 251 | metadata['artworkURL'] = song_assets["artworkURL"] 252 | metadata['license'] = music["hls-key-server-url"] 253 | 254 | output_name = toolcfg.filenames.musics_template.format( 255 | track=metadata['trackNumber'], 256 | artist=metadata['artistName'], 257 | name=metadata['itemName'] 258 | ) 259 | return metadata, output_name 260 | 261 | # Album Music Check 262 | def fetch_metadata(self, track_id=None): 263 | data = { 264 | 'salableAdamId': args.url if not track_id else track_id 265 | } 266 | reqs = self.session.post(toolcfg.endpoints.playback, data=json.dumps(data)) 267 | if reqs.status_code != 200: 268 | self.log.fatal(f"[{reqs.status_code}] {reqs.reason}: {reqs.content}") 269 | return None 270 | reqs_js = reqs.json() 271 | return reqs_js 272 | 273 | def fetch_info(self, type: str, tid=None): 274 | tid = args.url if not tid else tid 275 | reqs = self.session.get(toolcfg.endpoints.amp.format( 276 | type=type, id=tid, region=self.args.region)) 277 | if reqs.status_code != 200: 278 | self.log.fatal(f"[{reqs.status_code}] {reqs.reason}: {reqs.content}") 279 | return None 280 | return reqs.json() 281 | 282 | def music_tag(self, data: dict): 283 | f = music_tag.load_file(self.oufn) 284 | for key, value in data.items(): 285 | f[key] = value 286 | f.save() 287 | shutil.move(self.oufn, self.oufn.replace( 288 | toolcfg.folder.output, self.album_folder)) 289 | 290 | def insert_metadata(self, metadata): 291 | self.log.info("+ Adding metadata") 292 | 293 | data = { 294 | "album": metadata["playlistName"], 295 | "albumartist": metadata["artistName"], 296 | "artist": metadata["artistName"], 297 | "comment": metadata["copyright"], 298 | "compilation": metadata["compilation"], 299 | "composer": metadata["composerName"], 300 | "discnumber": metadata["discNumber"], 301 | "genre": metadata["genre"], 302 | "totaldiscs": metadata["discCount"], 303 | "totaltracks": metadata["trackCount"], 304 | "tracknumber": metadata["trackNumber"], 305 | "tracktitle": metadata["itemName"], 306 | "year": metadata["year"], 307 | } 308 | # sometimes don't found 309 | try: 310 | data['isrc'] = metadata["xid"] 311 | except KeyError: 312 | pass 313 | 314 | reqs = requests.get(metadata["artworkURL"]) 315 | if reqs.ok: 316 | data["artwork"] = reqs.content 317 | else: 318 | self.log.warning("- Failed to Get Artwork") 319 | 320 | try: 321 | self.music_tag(data) 322 | except Exception as e: 323 | self.log.warning(f"- Failed to Tag File: {e}") 324 | 325 | def extract_playlist_data(self, metadata): 326 | playlist = m3u8.load(metadata['playback']) 327 | if self.content_type != 'music-video': 328 | fn = playlist.segments[0].uri 329 | track_url = playlist.base_uri + fn 330 | key_id = playlist.keys[0].uri 331 | return track_url, key_id 332 | 333 | # return only audio track url and key id 334 | track_url = [ 335 | x for x in playlist.media if x.type == "AUDIO"][-1].uri 336 | key_id = next(x for x in m3u8.load(track_url).keys if x.keyformat == 337 | "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed").uri 338 | if not key_id: 339 | self.log.fatal("- Failed To Find Audio Key ID With Widevine SystemID") 340 | exit(1) 341 | 342 | # for video track -> self only 343 | self.vt_url = playlist.playlists[-1].uri 344 | self.vt_kid = next(x for x in m3u8.load(self.vt_url).keys if x.keyformat == 345 | "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed").uri 346 | if not self.vt_kid: 347 | self.log.fatal("- Failed To Find Video Key ID With Widevine SystemID") 348 | exit(1) 349 | return track_url, key_id 350 | 351 | def do_ffmpeg_fix(self, track): 352 | ffmpeg_command = [ 353 | toolcfg.binaries.ffmpeg, 354 | '-y', 355 | '-hide_banner', 356 | '-loglevel', 'error', 357 | '-i', track, 358 | '-map_metadata', '-1', 359 | '-fflags', 'bitexact', 360 | '-c', 'copy', 361 | self.oufn, 362 | ] 363 | subprocess.run(ffmpeg_command, check=True) 364 | 365 | def download_track(self, tracks: VideoTrack): # KEKE DW 366 | 367 | aria2c_input = '' 368 | 369 | for i, track in enumerate(tracks): 370 | self.log.info("+ Downloading {} Track".format(track.get_type().title())) 371 | # Music Only 372 | if self.content_type != 'music-video': 373 | track_fname = toolcfg.filenames.encrypted_filename_audio.format( 374 | filename=self.filename, track_type=track.type, track_no='0') 375 | (dname, fname) = os.path.split(track_fname) 376 | 377 | aria2c_input += f'{track.url}\n' 378 | aria2c_input += f'\tdir={dname}\n' 379 | aria2c_input += f'\tout={fname}\n' 380 | 381 | aria2c_infile = os.path.join(toolcfg.folder.temp, 'aria2c_infile.txt') 382 | track_fname = os.path.join(track_fname) 383 | with open(aria2c_infile, 'w') as fd: 384 | fd.write(aria2c_input) 385 | 386 | aria2c_opts = [ 387 | toolcfg.binaries.aria2c, 388 | '--allow-overwrite', 'false', 389 | '--quiet', 'true', # false 390 | '--continue', 'true', 391 | '--summary-interval=0', 392 | '--async-dns=false', 393 | '--disable-ipv6=true', 394 | '--retry-wait=5', 395 | '-x', '16', 396 | '-s', '16', 397 | '-i', aria2c_infile, 398 | ] 399 | 400 | try: 401 | subprocess.run(aria2c_opts, check=True) 402 | except CalledProcessError as e: 403 | if e.returncode == 13: 404 | self.log.info('- File Already Downloaded, not Overwriting') 405 | else: 406 | if e.returncode == 9: 407 | self.log.error('- The Human Controlling the Downloader is Stupid... VERY VERY STUPID LOL...') 408 | else: 409 | self.log.error(f'I Think Aria2c is Having Some Trouble... it Returned Exit Code {e.returncode}') 410 | sys.exit(1) 411 | os.remove(aria2c_infile) 412 | else: 413 | # Music Video 414 | if track.get_type() == 'video': 415 | fname = toolcfg.filenames.encrypted_filename_video.format( 416 | filename=self.filename, track_type=track.get_type(), track_no='0') 417 | elif track.get_type() == 'audio': 418 | fname = toolcfg.filenames.encrypted_filename_audio.format( 419 | filename=self.filename, track_type=track.get_type(), track_no='0') 420 | self.download_alternative(track.url, fname) 421 | 422 | def download_alternative(self, url: str, output_name: str): 423 | subprocess.Popen(["yt-dlp", "--allow-unplayable", "-o", output_name, url]).wait() 424 | 425 | def do_service_certificate(self, track: VideoTrack): # KEKE DW 426 | license_response = self.session.post( 427 | url=self.license_url, 428 | data=json.dumps({ 429 | 'adamId': track.aid, 430 | 'challenge': "CAQ=", 431 | 'isLibrary': False, 432 | 'key-system': 'com.widevine.alpha', 433 | 'uri': track.uri, 434 | 'user-initiated': True 435 | })) 436 | if license_response.status_code != 200: 437 | self.log.fatal( 438 | f"[{license_response.status_code}] {license_response.reason}: {license_response.content}") 439 | sys.exit(1) 440 | license_response_json = license_response.json() 441 | if not "license" in license_response_json: 442 | self.log.fatal("Invalid license response") 443 | self.log.fatal(license_response_json) 444 | sys.exit(1) 445 | return license_response_json["license"] 446 | 447 | def get_license(self, challenge, track: VideoTrack): # KEKE DW 448 | license_response = self.session.post( 449 | url=self.license_url, 450 | data=json.dumps({ 451 | 'challenge': challenge, 452 | 'key-system': 'com.widevine.alpha', 453 | 'uri': track.uri, 454 | 'adamId': track.aid, 455 | 'isLibrary': False, 456 | 'user-initiated': True 457 | })) 458 | if license_response.status_code != 200: 459 | self.log.fatal( 460 | f"[{license_response.status_code}] {license_response.reason}: {license_response.content}") 461 | sys.exit(1) 462 | license_response_json = license_response.json() 463 | if not "license" in license_response_json: 464 | self.log.fatal("- Invalid license response") 465 | self.log.fatal(license_response_json) 466 | sys.exit(1) 467 | license_b64 = license_response_json["license"] 468 | return license_b64 469 | 470 | def do_decrypt(self, config: WvDecryptConfig, track: VideoTrack): # KEKE DW 471 | self.log.info("+ Fetching Service Certificate...") 472 | cert_data_b64 = self.do_service_certificate(track) 473 | if not cert_data_b64: 474 | raise Exception("Failed to get service certificate") 475 | self.log.info("+ Requesting license...") 476 | 477 | 478 | if self.content_type != 'music-video': 479 | wvpsshdata = WidevinePsshData() 480 | wvpsshdata.algorithm = 1 481 | wvpsshdata.key_id.append(base64.b64decode(config.init_data_b64.split(",")[1])) 482 | config.init_data_b64 = base64.b64encode(wvpsshdata.SerializeToString()).decode("utf8") 483 | self.log.debug(f'init_data_b64: {config.init_data_b64}') 484 | else: 485 | config.init_data_b64 = config.init_data_b64.split(",")[1] 486 | 487 | wvdecrypt = WvDecrypt(config) 488 | chal = base64.b64encode(wvdecrypt.get_challenge()).decode('utf-8') 489 | license_b64 = self.get_license(chal, track) 490 | if not license_b64: 491 | print('NO license') 492 | return False 493 | wvdecrypt.update_license(license_b64) 494 | key = wvdecrypt.start_process() 495 | return True, key 496 | 497 | def do_merge(self, ats, vfn, output): 498 | self.log.info("+ Muxing Video + Audio Using MKVMerge") 499 | mkvmerge_command = [toolcfg.binaries.mkvmerge, 500 | "--output", 501 | output, 502 | "--no-date", 503 | "--language", 504 | "0:und", 505 | "(", 506 | vfn, 507 | ")", 508 | "--language", 509 | "0:und", 510 | "(", 511 | ats, 512 | ")"] 513 | subprocess.run(mkvmerge_command) 514 | 515 | def fetch_titles(self): 516 | self.log.info('+ Starting Apple Music Ripper') 517 | # album music/ single track/ playlists 518 | if self.content_type in ('album', 'playlist'): 519 | # No Single Track, Download Full Album 520 | if not self.url_track_id: 521 | album_info = self.fetch_info( 522 | ("albums" if self.content_type == 'album' else 'playlists')) 523 | if not album_info: 524 | raise Exception("Failed to get album info") 525 | 526 | self.album_name = normalize(album_info["data"][0]["attributes"]["name"]) 527 | 528 | tracks = album_info["data"][0]["relationships"]["tracks"]["data"] 529 | 530 | if args.track: 531 | try: 532 | if ',' in args.track: 533 | tracks_list = [int(x) for x in args.track.split(',')] 534 | elif '-' in args.track: 535 | (start, end) = args.track.split('-') 536 | tracks_list = list(range(int(start), int(end) + 1)) 537 | else: 538 | tracks_list = [int(args.track)] 539 | except ValueError: 540 | print('ERROR: track must be either a single number (ex: 1), ' 541 | 'a range (ex: 2-5) or a comma-separated list of values (ex: 1,3,4)') 542 | sys.exit(1) 543 | try: 544 | eplist = [] 545 | for num, ep in enumerate(tracks_list, 1): 546 | eplist.append(tracks[int(ep) - 1]) 547 | tracks_list = eplist 548 | except IndexError: 549 | self.log.error( 550 | 'The requested track ({}) was not found.'.format(args.track)) 551 | sys.exit(1) 552 | else: 553 | tracks_list = tracks 554 | 555 | if args.track_start: 556 | tracks_list = tracks_list[(int(args.track_start) - 1):] 557 | else: 558 | # Single Track 559 | tracks_list = [] 560 | tracks_single = { 561 | 'id': self.url_track_id 562 | } 563 | tracks_list.append(tracks_single) 564 | 565 | for track in tracks_list: 566 | metadata, output_name = self.fetch_music_metadata(track["id"]) 567 | output_name = normalize(output_name) 568 | self.contents_to_be_ripped.append((metadata, output_name)) 569 | # music video 570 | elif self.content_type == 'music-video': 571 | video_info = self.fetch_info("music-videos", tid=self.url_main_id) 572 | if not video_info: 573 | raise Exception("Failed to Get Video Info") 574 | 575 | # hls here 576 | data = self.fetch_metadata(self.url_main_id) 577 | try: 578 | video_ls = data["songList"][0] 579 | except KeyError: 580 | self.log.error(data['customerMessage']) 581 | sys.exit(1) 582 | 583 | metadata = video_info["data"][0]["attributes"] 584 | metadata['playback'] = video_ls["hls-playlist-url"] 585 | metadata['license'] = video_ls["hls-key-server-url"] 586 | metadata['itemId'] = self.url_main_id 587 | 588 | output_name = toolcfg.filenames.videos_template.format( 589 | artist=metadata['artistName'], 590 | name=metadata['name'] 591 | ) 592 | output_name = normalize(output_name) 593 | self.contents_to_be_ripped.append((metadata, output_name)) 594 | 595 | return self.contents_to_be_ripped 596 | 597 | def run(self, metadata, filename): 598 | 599 | self.filename = filename 600 | self.log.info(f'+ Downloading {filename}..') 601 | track_url, track_uri = self.extract_playlist_data(metadata) 602 | self.license_url = metadata['license'] 603 | # Mount Object AudioTrack + "VideoTrack" if Music Video.. 604 | ats = AudioTrack(type_='audio', url=track_url, uri=track_uri, aid=metadata['itemId'], audio_only=self.audio_only) 605 | enc_tracks = [ats] 606 | if self.content_type == 'music-video': 607 | # make video track 608 | vts = VideoTrack(type_='video', url=self.vt_url, uri=self.vt_kid, aid=metadata['itemId'], audio_only=False) 609 | enc_tracks += [vts] 610 | 611 | if not self.args.keys: 612 | # Download Tracks 613 | self.download_track(enc_tracks) 614 | for track in enc_tracks: 615 | # Decrypt 616 | self.log.info("+ Decrypting {} Track".format(track.type.title())) 617 | wvdecrypt_config = WvDecryptConfig(self.args, self.content_type ,self.filename, track.get_type(), track.audio_only, 618 | '0', self.args.keys, track_uri, cert_data_b64=None) 619 | success, key = self.do_decrypt(wvdecrypt_config, track) 620 | if self.args.keys: 621 | return 622 | else: 623 | self.log.info("+ All Decrypting Complete") 624 | if self.content_type != 'music-video': 625 | self.album_folder = os.path.join(toolcfg.folder.output, self.album_name) 626 | if not os.path.exists(self.album_folder): 627 | os.mkdir(self.album_folder) 628 | 629 | ffmpeg_atrack = wvdecrypt_config.get_filename(toolcfg.filenames.decrypted_filename_audio_ff) 630 | self.oufn = wvdecrypt_config.get_filename(toolcfg.filenames.muxed_audio_filename) 631 | self.do_ffmpeg_fix(ffmpeg_atrack) 632 | # Music Tag 633 | self.insert_metadata(metadata) 634 | else: 635 | ats = toolcfg.filenames.decrypted_filename_audio.format( 636 | filename=self.filename, track_type='audio', track_no='0') 637 | vts = toolcfg.filenames.decrypted_filename_video.format( 638 | filename=self.filename, track_type='video', track_no='0') 639 | out = wvdecrypt_config.get_filename(toolcfg.filenames.muxed_video_filename) 640 | self.do_merge(ats, vts, out) 641 | 642 | if self.args.skip_cleanup: 643 | self.log.info('+ Skipping Clean') 644 | return True 645 | self.log.info("+ Cleaning Temporary Files") 646 | file_list = [f for f in os.listdir(toolcfg.folder.temp)] 647 | for f in file_list: 648 | if f.startswith("{}".format(self.filename)): 649 | os.remove(os.path.join(toolcfg.folder.temp,f)) 650 | return True 651 | 652 | 653 | def unwanted_char(asin): 654 | nfkd_form = unicodedata.normalize('NFKD', asin) 655 | only_ascii = nfkd_form.encode('ASCII', 'ignore') 656 | return only_ascii.decode("utf-8") 657 | 658 | def normalize(outputfile): 659 | outputfile = unwanted_char(outputfile) 660 | outputfile = pathvalidate.sanitize_filename(outputfile) 661 | outputfile = unidecode(outputfile) 662 | outputfile = re.sub(r'[]!"#$%\'()*+,:;<=>?@\\^_-`|~[]', '', outputfile) 663 | outputfile = re.sub(r'\.{2,}', '.', outputfile) 664 | 665 | return outputfile 666 | 667 | if __name__ == "__main__": 668 | 669 | print(BANNER) 670 | 671 | parser = argparse.ArgumentParser( 672 | description='Apple Music Ripper') 673 | 674 | parser.add_argument('url', nargs='?', help='apple music title url') 675 | parser.add_argument('-t', '--track', help='rip only specified track from album') 676 | parser.add_argument('-ts', '--track_start', help="rip starting at the track number provided") 677 | parser.add_argument('-r', '--region', default='us', choices=['us', 'eu', 'br'], help='Apple Music Region') 678 | parser.add_argument("-d", "--debug", action="store_true", help="Enable debug logging") 679 | # don't use with music video, bad fps lol 680 | parser.add_argument("-m4", "--mp4decrypt", dest="mp4_decrypt", action="store_true", 681 | help="Use mp4decrypt instead of shaka-packager to decrypt files") 682 | parser.add_argument('-k', '--skip-cleanup', action='store_true', help='skip cleanup step') 683 | parser.add_argument("--keys", action="store_true", help="show keys and exit") 684 | args_parsed = parser.parse_args() 685 | 686 | config_dict = {} 687 | args = Bunch(config_dict, vars(args_parsed)) 688 | 689 | DEBUG_LEVELKEY_NUM = 21 690 | logging.addLevelName(DEBUG_LEVELKEY_NUM, "LOGKEY") 691 | 692 | def logkey(self, message, *args, **kws): 693 | # Yes, logger takes its '*args' as 'args'. 694 | if self.isEnabledFor(DEBUG_LEVELKEY_NUM): 695 | self._log(DEBUG_LEVELKEY_NUM, message, args, **kws) 696 | 697 | logging.Logger.logkey = logkey 698 | 699 | logger = logging.getLogger() 700 | 701 | ch = logging.StreamHandler(sys.stdout) 702 | ch.setLevel(logging.DEBUG) 703 | formatter = ColoredFormatter( 704 | '[%(asctime)s] %(levelname)s: %(message)s', datefmt='%I:%M:%S') 705 | ch.setFormatter(formatter) 706 | logger.addHandler(ch) 707 | 708 | if args.keys: 709 | logger.setLevel(21) 710 | else: 711 | logger.setLevel(logging.INFO) 712 | 713 | if args.debug: 714 | logger.setLevel(logging.DEBUG) 715 | 716 | client = AppleMusicClient() 717 | titles = client.fetch_titles() 718 | for metadata, filename in titles: 719 | if args.keys: 720 | logger.logkey('{}'.format(filename)) 721 | client.run(metadata, filename) -------------------------------------------------------------------------------- /binaries/aria2c.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/binaries/aria2c.exe -------------------------------------------------------------------------------- /binaries/ffmpeg.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/binaries/ffmpeg.exe -------------------------------------------------------------------------------- /binaries/mkvmerge.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/binaries/mkvmerge.exe -------------------------------------------------------------------------------- /binaries/mp4decrypt.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/binaries/mp4decrypt.exe -------------------------------------------------------------------------------- /binaries/packager.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/binaries/packager.exe -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shutil import which 4 | from os.path import join 5 | from os import pathsep, environ 6 | 7 | class Endpoints: 8 | def __init__(self): 9 | self.home = "https://music.apple.com" 10 | self.amp = "https://amp-api.music.apple.com/v1/catalog/{region}/{type}/{id}" 11 | self.playback = "https://play.music.apple.com/WebObjects/MZPlay.woa/wa/webPlayback" 12 | # self.playlist = "https://amp-api.music.apple.com/v1/catalog/{region}/albums/{playlist_id}?l=en-us&platform=web&omit[resource]=autos&include=tracks,artists&include[songs]=artists,composers&extend[url]=f" 13 | 14 | class Folders: 15 | def __init__(self): 16 | self.binaries = "binaries" 17 | self.cookies = "cookies" 18 | self.temp = "temp" 19 | self.output = "output" 20 | 21 | folder = Folders() 22 | # Add binaries folder to PATH as the first item 23 | environ['PATH'] = pathsep.join([folder.binaries, environ['PATH']]) 24 | 25 | class Binaries: 26 | def __init__(self): 27 | self.mp4decrypt = which("mp4decrypt") 28 | self.mkvmerge = which("mkvmerge") 29 | self.shaka = which("packager") 30 | self.ffmpeg = which("ffmpeg") 31 | self.aria2c = which("aria2c") 32 | 33 | class Filenames: 34 | def __init__(self): 35 | self.base_track_video = '{filename}_{track_type}_{track_no}_' 36 | self.base_track_audio = '{filename}_{track_type}_{track_no}_' 37 | self.base_video_muxed = '{filename}.mkv' 38 | self.base_audio_muxed = '{filename}.m4a' 39 | self.musics_template = '{track} - {artist} - {name}' 40 | self.videos_template = '{artist} - {name}' 41 | self.encrypted_filename_video = join(folder.temp, self.base_track_video + 'encrypted.mp4') 42 | self.decrypted_filename_video = join(folder.temp, self.base_track_video + 'decrypted.mp4') 43 | self.encrypted_filename_audio = join(folder.temp, self.base_track_audio + 'encrypted.m4a') 44 | self.decrypted_filename_audio = join(folder.temp, self.base_track_audio + 'decrypted.m4a') 45 | self.decrypted_filename_audio_ff = join(folder.temp, self.base_track_audio + 'decrypted_fixed.m4a') 46 | self.muxed_video_filename = join(folder.output, '{filename}.mkv') 47 | self.muxed_audio_filename = join(folder.output, '{filename}.m4a') 48 | 49 | 50 | binaries = Binaries() 51 | endpoints = Endpoints() 52 | filenames = Filenames() -------------------------------------------------------------------------------- /cookies/cookies.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/cookies/cookies.txt -------------------------------------------------------------------------------- /pywidevine/L3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/pywidevine/L3/__init__.py -------------------------------------------------------------------------------- /pywidevine/L3/cdm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/pywidevine/L3/cdm/__init__.py -------------------------------------------------------------------------------- /pywidevine/L3/cdm/cdm.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import os 4 | import time 5 | import binascii 6 | 7 | from google.protobuf.message import DecodeError 8 | from google.protobuf import text_format 9 | 10 | from pywidevine.L3.cdm.formats import wv_proto2_pb2 as wv_proto2 11 | from pywidevine.L3.cdm.session import Session 12 | from pywidevine.L3.cdm.key import Key 13 | from Cryptodome.Random import get_random_bytes 14 | from Cryptodome.Random import random 15 | from Cryptodome.Cipher import PKCS1_OAEP, AES 16 | from Cryptodome.Hash import CMAC, SHA256, HMAC, SHA1 17 | from Cryptodome.PublicKey import RSA 18 | from Cryptodome.Signature import pss 19 | from Cryptodome.Util import Padding 20 | import logging 21 | 22 | class Cdm: 23 | def __init__(self): 24 | self.logger = logging.getLogger(__name__) 25 | self.sessions = {} 26 | 27 | def open_session(self, init_data_b64, device, raw_init_data = None, offline=False): 28 | self.logger.debug("open_session(init_data_b64={}, device={}".format(init_data_b64, device)) 29 | self.logger.info("opening new cdm session") 30 | if device.session_id_type == 'android': 31 | # format: 16 random hexdigits, 2 digit counter, 14 0s 32 | rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16)) 33 | counter = '01' # this resets regularly so its fine to use 01 34 | rest = '00000000000000' 35 | session_id = rand_ascii + counter + rest 36 | session_id = session_id.encode('ascii') 37 | elif device.session_id_type == 'chrome': 38 | rand_bytes = get_random_bytes(16) 39 | session_id = rand_bytes 40 | else: 41 | # other formats NYI 42 | self.logger.error("device type is unusable") 43 | return 1 44 | if raw_init_data and isinstance(raw_init_data, (bytes, bytearray)): 45 | # used for NF key exchange, where they don't provide a valid PSSH 46 | init_data = raw_init_data 47 | self.raw_pssh = True 48 | else: 49 | init_data = self._parse_init_data(init_data_b64) 50 | self.raw_pssh = False 51 | 52 | if init_data: 53 | new_session = Session(session_id, init_data, device, offline) 54 | else: 55 | self.logger.error("unable to parse init data") 56 | return 1 57 | self.sessions[session_id] = new_session 58 | self.logger.info("session opened and init data parsed successfully") 59 | return session_id 60 | 61 | def _parse_init_data(self, init_data_b64): 62 | parsed_init_data = wv_proto2.WidevineCencHeader() 63 | try: 64 | self.logger.debug("trying to parse init_data directly") 65 | parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:]) 66 | except DecodeError: 67 | self.logger.debug("unable to parse as-is, trying with removed pssh box header") 68 | try: 69 | id_bytes = parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:]) 70 | except DecodeError: 71 | self.logger.error("unable to parse, unsupported init data format") 72 | return None 73 | self.logger.debug("init_data:") 74 | for line in text_format.MessageToString(parsed_init_data).splitlines(): 75 | self.logger.debug(line) 76 | return parsed_init_data 77 | 78 | def close_session(self, session_id): 79 | self.logger.debug("close_session(session_id={})".format(session_id)) 80 | self.logger.info("closing cdm session") 81 | if session_id in self.sessions: 82 | self.sessions.pop(session_id) 83 | self.logger.info("cdm session closed") 84 | return 0 85 | else: 86 | self.logger.info("session {} not found".format(session_id)) 87 | return 1 88 | 89 | def set_service_certificate(self, session_id, cert_b64): 90 | self.logger.debug("set_service_certificate(session_id={}, cert={})".format(session_id, cert_b64)) 91 | self.logger.info("setting service certificate") 92 | 93 | if session_id not in self.sessions: 94 | self.logger.error("session id doesn't exist") 95 | return 1 96 | 97 | session = self.sessions[session_id] 98 | 99 | message = wv_proto2.SignedMessage() 100 | 101 | try: 102 | message.ParseFromString(base64.b64decode(cert_b64)) 103 | except DecodeError: 104 | self.logger.error("failed to parse cert as SignedMessage") 105 | 106 | service_certificate = wv_proto2.SignedDeviceCertificate() 107 | 108 | if message.Type: 109 | self.logger.debug("service cert provided as signedmessage") 110 | try: 111 | service_certificate.ParseFromString(message.Msg) 112 | except DecodeError: 113 | self.logger.error("failed to parse service certificate") 114 | return 1 115 | else: 116 | self.logger.debug("service cert provided as signeddevicecertificate") 117 | try: 118 | service_certificate.ParseFromString(base64.b64decode(cert_b64)) 119 | except DecodeError: 120 | self.logger.error("failed to parse service certificate") 121 | return 1 122 | 123 | self.logger.debug("service certificate:") 124 | for line in text_format.MessageToString(service_certificate).splitlines(): 125 | self.logger.debug(line) 126 | 127 | session.service_certificate = service_certificate 128 | session.privacy_mode = True 129 | 130 | return 0 131 | 132 | def get_license_request(self, session_id): 133 | self.logger.debug("get_license_request(session_id={})".format(session_id)) 134 | self.logger.info("getting license request") 135 | 136 | if session_id not in self.sessions: 137 | self.logger.error("session ID does not exist") 138 | return 1 139 | 140 | session = self.sessions[session_id] 141 | 142 | # raw pssh will be treated as bytes and not parsed 143 | if self.raw_pssh: 144 | license_request = wv_proto2.SignedLicenseRequestRaw() 145 | else: 146 | license_request = wv_proto2.SignedLicenseRequest() 147 | client_id = wv_proto2.ClientIdentification() 148 | 149 | if not os.path.exists(session.device_config.device_client_id_blob_filename): 150 | self.logger.error("no client ID blob available for this device") 151 | return 1 152 | 153 | with open(session.device_config.device_client_id_blob_filename, "rb") as f: 154 | try: 155 | cid_bytes = client_id.ParseFromString(f.read()) 156 | except DecodeError: 157 | self.logger.error("client id failed to parse as protobuf") 158 | return 1 159 | 160 | self.logger.debug("building license request") 161 | if not self.raw_pssh: 162 | license_request.Type = wv_proto2.SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST') 163 | license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data) 164 | else: 165 | license_request.Type = wv_proto2.SignedLicenseRequestRaw.MessageType.Value('LICENSE_REQUEST') 166 | license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes 167 | 168 | if session.offline: 169 | license_type = wv_proto2.LicenseType.Value('OFFLINE') 170 | else: 171 | license_type = wv_proto2.LicenseType.Value('DEFAULT') 172 | license_request.Msg.ContentId.CencId.LicenseType = license_type 173 | license_request.Msg.ContentId.CencId.RequestId = session_id 174 | license_request.Msg.Type = wv_proto2.LicenseRequest.RequestType.Value('NEW') 175 | license_request.Msg.RequestTime = int(time.time()) 176 | license_request.Msg.ProtocolVersion = wv_proto2.ProtocolVersion.Value('CURRENT') 177 | if session.device_config.send_key_control_nonce: 178 | license_request.Msg.KeyControlNonce = random.randrange(1, 2**31) 179 | 180 | if session.privacy_mode: 181 | if session.device_config.vmp: 182 | self.logger.debug("vmp required, adding to client_id") 183 | self.logger.debug("reading vmp hashes") 184 | vmp_hashes = wv_proto2.FileHashes() 185 | with open(session.device_config.device_vmp_blob_filename, "rb") as f: 186 | try: 187 | vmp_bytes = vmp_hashes.ParseFromString(f.read()) 188 | except DecodeError: 189 | self.logger.error("vmp hashes failed to parse as protobuf") 190 | return 1 191 | client_id._FileHashes.CopyFrom(vmp_hashes) 192 | self.logger.debug("privacy mode & service certificate loaded, encrypting client id") 193 | self.logger.debug("unencrypted client id:") 194 | for line in text_format.MessageToString(client_id).splitlines(): 195 | self.logger.debug(line) 196 | cid_aes_key = get_random_bytes(16) 197 | cid_iv = get_random_bytes(16) 198 | 199 | cid_cipher = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv) 200 | 201 | encrypted_client_id = cid_cipher.encrypt(Padding.pad(client_id.SerializeToString(), 16)) 202 | 203 | service_public_key = RSA.importKey(session.service_certificate._DeviceCertificate.PublicKey) 204 | 205 | service_cipher = PKCS1_OAEP.new(service_public_key) 206 | 207 | encrypted_cid_key = service_cipher.encrypt(cid_aes_key) 208 | 209 | encrypted_client_id_proto = wv_proto2.EncryptedClientIdentification() 210 | 211 | encrypted_client_id_proto.ServiceId = session.service_certificate._DeviceCertificate.ServiceId 212 | encrypted_client_id_proto.ServiceCertificateSerialNumber = session.service_certificate._DeviceCertificate.SerialNumber 213 | encrypted_client_id_proto.EncryptedClientId = encrypted_client_id 214 | encrypted_client_id_proto.EncryptedClientIdIv = cid_iv 215 | encrypted_client_id_proto.EncryptedPrivacyKey = encrypted_cid_key 216 | 217 | license_request.Msg.EncryptedClientId.CopyFrom(encrypted_client_id_proto) 218 | else: 219 | license_request.Msg.ClientId.CopyFrom(client_id) 220 | 221 | if session.device_config.private_key_available: 222 | key = RSA.importKey(open(session.device_config.device_private_key_filename).read()) 223 | session.device_key = key 224 | else: 225 | self.logger.error("need device private key, other methods unimplemented") 226 | return 1 227 | 228 | self.logger.debug("signing license request") 229 | 230 | hash = SHA1.new(license_request.Msg.SerializeToString()) 231 | signature = pss.new(key).sign(hash) 232 | 233 | license_request.Signature = signature 234 | 235 | session.license_request = license_request 236 | 237 | self.logger.debug("license request:") 238 | for line in text_format.MessageToString(session.license_request).splitlines(): 239 | self.logger.debug(line) 240 | self.logger.info("license request created") 241 | self.logger.debug("license request b64: {}".format(base64.b64encode(license_request.SerializeToString()))) 242 | return license_request.SerializeToString() 243 | 244 | def provide_license(self, session_id, license_b64): 245 | self.logger.debug("provide_license(session_id={}, license_b64={})".format(session_id, license_b64)) 246 | self.logger.info("decrypting provided license") 247 | 248 | if session_id not in self.sessions: 249 | self.logger.error("session does not exist") 250 | return 1 251 | 252 | session = self.sessions[session_id] 253 | 254 | if not session.license_request: 255 | self.logger.error("generate a license request first!") 256 | return 1 257 | 258 | license = wv_proto2.SignedLicense() 259 | try: 260 | license.ParseFromString(base64.b64decode(license_b64)) 261 | except DecodeError: 262 | self.logger.error("unable to parse license - check protobufs") 263 | return 1 264 | 265 | session.license = license 266 | 267 | self.logger.debug("license:") 268 | for line in text_format.MessageToString(license).splitlines(): 269 | self.logger.debug(line) 270 | 271 | self.logger.debug("deriving keys from session key") 272 | 273 | oaep_cipher = PKCS1_OAEP.new(session.device_key) 274 | 275 | session.session_key = oaep_cipher.decrypt(license.SessionKey) 276 | 277 | lic_req_msg = session.license_request.Msg.SerializeToString() 278 | 279 | enc_key_base = b"ENCRYPTION\000" + lic_req_msg + b"\0\0\0\x80" 280 | auth_key_base = b"AUTHENTICATION\0" + lic_req_msg + b"\0\0\2\0" 281 | 282 | enc_key = b"\x01" + enc_key_base 283 | auth_key_1 = b"\x01" + auth_key_base 284 | auth_key_2 = b"\x02" + auth_key_base 285 | auth_key_3 = b"\x03" + auth_key_base 286 | auth_key_4 = b"\x04" + auth_key_base 287 | 288 | cmac_obj = CMAC.new(session.session_key, ciphermod=AES) 289 | cmac_obj.update(enc_key) 290 | 291 | enc_cmac_key = cmac_obj.digest() 292 | 293 | cmac_obj = CMAC.new(session.session_key, ciphermod=AES) 294 | cmac_obj.update(auth_key_1) 295 | auth_cmac_key_1 = cmac_obj.digest() 296 | 297 | cmac_obj = CMAC.new(session.session_key, ciphermod=AES) 298 | cmac_obj.update(auth_key_2) 299 | auth_cmac_key_2 = cmac_obj.digest() 300 | 301 | cmac_obj = CMAC.new(session.session_key, ciphermod=AES) 302 | cmac_obj.update(auth_key_3) 303 | auth_cmac_key_3 = cmac_obj.digest() 304 | 305 | cmac_obj = CMAC.new(session.session_key, ciphermod=AES) 306 | cmac_obj.update(auth_key_4) 307 | auth_cmac_key_4 = cmac_obj.digest() 308 | 309 | auth_cmac_combined_1 = auth_cmac_key_1 + auth_cmac_key_2 310 | auth_cmac_combined_2 = auth_cmac_key_3 + auth_cmac_key_4 311 | 312 | session.derived_keys['enc'] = enc_cmac_key 313 | session.derived_keys['auth_1'] = auth_cmac_combined_1 314 | session.derived_keys['auth_2'] = auth_cmac_combined_2 315 | 316 | self.logger.debug('verifying license signature') 317 | 318 | lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256) 319 | lic_hmac.update(license.Msg.SerializeToString()) 320 | 321 | self.logger.debug("calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature))) 322 | 323 | if lic_hmac.digest() != license.Signature: 324 | self.logger.info("license signature doesn't match - writing bin so they can be debugged") 325 | with open("original_lic.bin", "wb") as f: 326 | f.write(base64.b64decode(license_b64)) 327 | with open("parsed_lic.bin", "wb") as f: 328 | f.write(license.SerializeToString()) 329 | self.logger.info("continuing anyway") 330 | 331 | self.logger.debug("key count: {}".format(len(license.Msg.Key))) 332 | for key in license.Msg.Key: 333 | if key.Id: 334 | key_id = key.Id 335 | else: 336 | key_id = wv_proto2.License.KeyContainer.KeyType.Name(key.Type).encode('utf-8') 337 | encrypted_key = key.Key 338 | iv = key.Iv 339 | type = wv_proto2.License.KeyContainer.KeyType.Name(key.Type) 340 | 341 | cipher = AES.new(session.derived_keys['enc'], AES.MODE_CBC, iv=iv) 342 | decrypted_key = cipher.decrypt(encrypted_key) 343 | if type == "OPERATOR_SESSION": 344 | permissions = [] 345 | perms = key._OperatorSessionKeyPermissions 346 | for (descriptor, value) in perms.ListFields(): 347 | if value == 1: 348 | permissions.append(descriptor.name) 349 | print(permissions) 350 | else: 351 | permissions = [] 352 | session.keys.append(Key(key_id, type, Padding.unpad(decrypted_key, 16), permissions)) 353 | 354 | self.logger.info("decrypted all keys") 355 | return 0 356 | 357 | def get_keys(self, session_id): 358 | if session_id in self.sessions: 359 | return self.sessions[session_id].keys 360 | else: 361 | self.logger.error("session not found") 362 | return 1 363 | -------------------------------------------------------------------------------- /pywidevine/L3/cdm/deviceconfig.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | device_android_generic = { 4 | 'name': 'android_generic', 5 | 'description': 'android studio cdm', 6 | 'security_level': 3, 7 | 'session_id_type': 'android', 8 | 'private_key_available': True, 9 | 'vmp': False, 10 | 'send_key_control_nonce': True 11 | } 12 | 13 | devices_available = [device_android_generic] 14 | 15 | FILES_FOLDER = 'devices' 16 | 17 | class DeviceConfig: 18 | def __init__(self, device): 19 | self.device_name = device['name'] 20 | self.description = device['description'] 21 | self.security_level = device['security_level'] 22 | self.session_id_type = device['session_id_type'] 23 | self.private_key_available = device['private_key_available'] 24 | self.vmp = device['vmp'] 25 | self.send_key_control_nonce = device['send_key_control_nonce'] 26 | 27 | if 'keybox_filename' in device: 28 | self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['keybox_filename']) 29 | else: 30 | self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'keybox') 31 | 32 | if 'device_cert_filename' in device: 33 | self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_cert_filename']) 34 | else: 35 | self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_cert') 36 | 37 | if 'device_private_key_filename' in device: 38 | self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_private_key_filename']) 39 | else: 40 | self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_private_key') 41 | 42 | if 'device_client_id_blob_filename' in device: 43 | self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_client_id_blob_filename']) 44 | else: 45 | self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_client_id_blob') 46 | 47 | if 'device_vmp_blob_filename' in device: 48 | self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_vmp_blob_filename']) 49 | else: 50 | self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_vmp_blob') 51 | 52 | def __repr__(self): 53 | return "DeviceConfig(name={}, description={}, security_level={}, session_id_type={}, private_key_available={}, vmp={})".format(self.device_name, self.description, self.security_level, self.session_id_type, self.private_key_available, self.vmp) 54 | -------------------------------------------------------------------------------- /pywidevine/L3/cdm/formats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/pywidevine/L3/cdm/formats/__init__.py -------------------------------------------------------------------------------- /pywidevine/L3/cdm/formats/wv_proto2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | // from x86 (partial), most of it from the ARM version: 4 | message ClientIdentification { 5 | enum TokenType { 6 | KEYBOX = 0; 7 | DEVICE_CERTIFICATE = 1; 8 | REMOTE_ATTESTATION_CERTIFICATE = 2; 9 | } 10 | message NameValue { 11 | required string Name = 1; 12 | required string Value = 2; 13 | } 14 | message ClientCapabilities { 15 | enum HdcpVersion { 16 | HDCP_NONE = 0; 17 | HDCP_V1 = 1; 18 | HDCP_V2 = 2; 19 | HDCP_V2_1 = 3; 20 | HDCP_V2_2 = 4; 21 | } 22 | optional uint32 ClientToken = 1; 23 | optional uint32 SessionToken = 2; 24 | optional uint32 VideoResolutionConstraints = 3; 25 | optional HdcpVersion MaxHdcpVersion = 4; 26 | optional uint32 OemCryptoApiVersion = 5; 27 | } 28 | required TokenType Type = 1; 29 | //optional bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one: 30 | optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob 31 | repeated NameValue ClientInfo = 3; 32 | optional bytes ProviderClientToken = 4; 33 | optional uint32 LicenseCounter = 5; 34 | optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later 35 | optional FileHashes _FileHashes = 7; // vmp blob goes here 36 | } 37 | 38 | message DeviceCertificate { 39 | enum CertificateType { 40 | ROOT = 0; 41 | INTERMEDIATE = 1; 42 | USER_DEVICE = 2; 43 | SERVICE = 3; 44 | } 45 | required CertificateType Type = 1; // the compiled code reused this as ProvisionedDeviceInfo.WvSecurityLevel, however that is incorrect (compiler aliased it as they're both identical as a structure) 46 | optional bytes SerialNumber = 2; 47 | optional uint32 CreationTimeSeconds = 3; 48 | optional bytes PublicKey = 4; 49 | optional uint32 SystemId = 5; 50 | optional uint32 TestDeviceDeprecated = 6; // is it bool or int? 51 | optional bytes ServiceId = 7; // service URL for service certificates 52 | } 53 | 54 | // missing some references, 55 | message DeviceCertificateStatus { 56 | enum CertificateStatus { 57 | VALID = 0; 58 | REVOKED = 1; 59 | } 60 | optional bytes SerialNumber = 1; 61 | optional CertificateStatus Status = 2; 62 | optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated? 63 | } 64 | 65 | message DeviceCertificateStatusList { 66 | optional uint32 CreationTimeSeconds = 1; 67 | repeated DeviceCertificateStatus CertificateStatus = 2; 68 | } 69 | 70 | message EncryptedClientIdentification { 71 | required string ServiceId = 1; 72 | optional bytes ServiceCertificateSerialNumber = 2; 73 | required bytes EncryptedClientId = 3; 74 | required bytes EncryptedClientIdIv = 4; 75 | required bytes EncryptedPrivacyKey = 5; 76 | } 77 | 78 | // todo: fill (for this top-level type, it might be impossible/difficult) 79 | enum LicenseType { 80 | ZERO = 0; 81 | DEFAULT = 1; // 1 is STREAMING/temporary license; on recent versions may go up to 3 (latest x86); it might be persist/don't persist type, unconfirmed 82 | OFFLINE = 2; 83 | } 84 | 85 | // todo: fill (for this top-level type, it might be impossible/difficult) 86 | // this is just a guess because these globals got lost, but really, do we need more? 87 | enum ProtocolVersion { 88 | CURRENT = 21; // don't have symbols for this 89 | } 90 | 91 | 92 | message LicenseIdentification { 93 | optional bytes RequestId = 1; 94 | optional bytes SessionId = 2; 95 | optional bytes PurchaseId = 3; 96 | optional LicenseType Type = 4; 97 | optional uint32 Version = 5; 98 | optional bytes ProviderSessionToken = 6; 99 | } 100 | 101 | 102 | message License { 103 | message Policy { 104 | optional bool CanPlay = 1; // changed from uint32 to bool 105 | optional bool CanPersist = 2; 106 | optional bool CanRenew = 3; 107 | optional uint32 RentalDurationSeconds = 4; 108 | optional uint32 PlaybackDurationSeconds = 5; 109 | optional uint32 LicenseDurationSeconds = 6; 110 | optional uint32 RenewalRecoveryDurationSeconds = 7; 111 | optional string RenewalServerUrl = 8; 112 | optional uint32 RenewalDelaySeconds = 9; 113 | optional uint32 RenewalRetryIntervalSeconds = 10; 114 | optional bool RenewWithUsage = 11; // was uint32 115 | } 116 | message KeyContainer { 117 | enum KeyType { 118 | SIGNING = 1; 119 | CONTENT = 2; 120 | KEY_CONTROL = 3; 121 | OPERATOR_SESSION = 4; 122 | } 123 | enum SecurityLevel { 124 | SW_SECURE_CRYPTO = 1; 125 | SW_SECURE_DECODE = 2; 126 | HW_SECURE_CRYPTO = 3; 127 | HW_SECURE_DECODE = 4; 128 | HW_SECURE_ALL = 5; 129 | } 130 | message OutputProtection { 131 | enum CGMS { 132 | COPY_FREE = 0; 133 | COPY_ONCE = 2; 134 | COPY_NEVER = 3; 135 | CGMS_NONE = 0x2A; // PC default! 136 | } 137 | optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away 138 | optional CGMS CgmsFlags = 2; 139 | } 140 | message KeyControl { 141 | required bytes KeyControlBlock = 1; // what is this? 142 | required bytes Iv = 2; 143 | } 144 | message OperatorSessionKeyPermissions { 145 | optional uint32 AllowEncrypt = 1; 146 | optional uint32 AllowDecrypt = 2; 147 | optional uint32 AllowSign = 3; 148 | optional uint32 AllowSignatureVerify = 4; 149 | } 150 | message VideoResolutionConstraint { 151 | optional uint32 MinResolutionPixels = 1; 152 | optional uint32 MaxResolutionPixels = 2; 153 | optional OutputProtection RequiredProtection = 3; 154 | } 155 | optional bytes Id = 1; 156 | optional bytes Iv = 2; 157 | optional bytes Key = 3; 158 | optional KeyType Type = 4; 159 | optional SecurityLevel Level = 5; 160 | optional OutputProtection RequiredProtection = 6; 161 | optional OutputProtection RequestedProtection = 7; 162 | optional KeyControl _KeyControl = 8; // duped names, etc 163 | optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc 164 | repeated VideoResolutionConstraint VideoResolutionConstraints = 10; 165 | } 166 | optional LicenseIdentification Id = 1; 167 | optional Policy _Policy = 2; // duped names, etc 168 | repeated KeyContainer Key = 3; 169 | optional uint32 LicenseStartTime = 4; 170 | optional uint32 RemoteAttestationVerified = 5; // bool? 171 | optional bytes ProviderClientToken = 6; 172 | // there might be more, check with newer versions (I see field 7-8 in a lic) 173 | // this appeared in latest x86: 174 | optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc) 175 | } 176 | 177 | message LicenseError { 178 | enum Error { 179 | INVALID_DEVICE_CERTIFICATE = 1; 180 | REVOKED_DEVICE_CERTIFICATE = 2; 181 | SERVICE_UNAVAILABLE = 3; 182 | } 183 | //LicenseRequest.RequestType ErrorCode; // clang mismatch 184 | optional Error ErrorCode = 1; 185 | } 186 | 187 | message LicenseRequest { 188 | message ContentIdentification { 189 | message CENC { 190 | //optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: 191 | optional WidevineCencHeader Pssh = 1; 192 | optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!) 193 | optional bytes RequestId = 3; 194 | } 195 | message WebM { 196 | optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used 197 | optional LicenseType LicenseType = 2; 198 | optional bytes RequestId = 3; 199 | } 200 | message ExistingLicense { 201 | optional LicenseIdentification LicenseId = 1; 202 | optional uint32 SecondsSinceStarted = 2; 203 | optional uint32 SecondsSinceLastPlayed = 3; 204 | optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB! 205 | } 206 | optional CENC CencId = 1; 207 | optional WebM WebmId = 2; 208 | optional ExistingLicense License = 3; 209 | } 210 | enum RequestType { 211 | NEW = 1; 212 | RENEWAL = 2; 213 | RELEASE = 3; 214 | } 215 | optional ClientIdentification ClientId = 1; 216 | optional ContentIdentification ContentId = 2; 217 | optional RequestType Type = 3; 218 | optional uint32 RequestTime = 4; 219 | optional bytes KeyControlNonceDeprecated = 5; 220 | optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this 221 | optional uint32 KeyControlNonce = 7; 222 | optional EncryptedClientIdentification EncryptedClientId = 8; 223 | } 224 | 225 | // raw pssh hack 226 | message LicenseRequestRaw { 227 | message ContentIdentification { 228 | message CENC { 229 | optional bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: 230 | //optional WidevineCencHeader Pssh = 1; 231 | optional LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 (is this persist/don't persist? look into it!) 232 | optional bytes RequestId = 3; 233 | } 234 | message WebM { 235 | optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used 236 | optional LicenseType LicenseType = 2; 237 | optional bytes RequestId = 3; 238 | } 239 | message ExistingLicense { 240 | optional LicenseIdentification LicenseId = 1; 241 | optional uint32 SecondsSinceStarted = 2; 242 | optional uint32 SecondsSinceLastPlayed = 3; 243 | optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB! 244 | } 245 | optional CENC CencId = 1; 246 | optional WebM WebmId = 2; 247 | optional ExistingLicense License = 3; 248 | } 249 | enum RequestType { 250 | NEW = 1; 251 | RENEWAL = 2; 252 | RELEASE = 3; 253 | } 254 | optional ClientIdentification ClientId = 1; 255 | optional ContentIdentification ContentId = 2; 256 | optional RequestType Type = 3; 257 | optional uint32 RequestTime = 4; 258 | optional bytes KeyControlNonceDeprecated = 5; 259 | optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this 260 | optional uint32 KeyControlNonce = 7; 261 | optional EncryptedClientIdentification EncryptedClientId = 8; 262 | } 263 | 264 | 265 | message ProvisionedDeviceInfo { 266 | enum WvSecurityLevel { 267 | LEVEL_UNSPECIFIED = 0; 268 | LEVEL_1 = 1; 269 | LEVEL_2 = 2; 270 | LEVEL_3 = 3; 271 | } 272 | optional uint32 SystemId = 1; 273 | optional string Soc = 2; 274 | optional string Manufacturer = 3; 275 | optional string Model = 4; 276 | optional string DeviceType = 5; 277 | optional uint32 ModelYear = 6; 278 | optional WvSecurityLevel SecurityLevel = 7; 279 | optional uint32 TestDevice = 8; // bool? 280 | } 281 | 282 | 283 | // todo: fill 284 | message ProvisioningOptions { 285 | } 286 | 287 | // todo: fill 288 | message ProvisioningRequest { 289 | } 290 | 291 | // todo: fill 292 | message ProvisioningResponse { 293 | } 294 | 295 | message RemoteAttestation { 296 | optional EncryptedClientIdentification Certificate = 1; 297 | optional string Salt = 2; 298 | optional string Signature = 3; 299 | } 300 | 301 | // todo: fill 302 | message SessionInit { 303 | } 304 | 305 | // todo: fill 306 | message SessionState { 307 | } 308 | 309 | // todo: fill 310 | message SignedCertificateStatusList { 311 | } 312 | 313 | message SignedDeviceCertificate { 314 | 315 | //optional bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is: 316 | optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later 317 | optional bytes Signature = 2; 318 | optional SignedDeviceCertificate Signer = 3; 319 | } 320 | 321 | 322 | // todo: fill 323 | message SignedProvisioningMessage { 324 | } 325 | 326 | // the root of all messages, from either server or client 327 | message SignedMessage { 328 | enum MessageType { 329 | LICENSE_REQUEST = 1; 330 | LICENSE = 2; 331 | ERROR_RESPONSE = 3; 332 | SERVICE_CERTIFICATE_REQUEST = 4; 333 | SERVICE_CERTIFICATE = 5; 334 | } 335 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 336 | optional bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 337 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 338 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 339 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 340 | optional RemoteAttestation RemoteAttestation = 5; 341 | } 342 | 343 | 344 | 345 | // This message is copied from google's docs, not reversed: 346 | message WidevineCencHeader { 347 | enum Algorithm { 348 | UNENCRYPTED = 0; 349 | AESCTR = 1; 350 | }; 351 | optional Algorithm algorithm = 1; 352 | repeated bytes key_id = 2; 353 | 354 | // Content provider name. 355 | optional string provider = 3; 356 | 357 | // A content identifier, specified by content provider. 358 | optional bytes content_id = 4; 359 | 360 | // Track type. Acceptable values are SD, HD and AUDIO. Used to 361 | // differentiate content keys used by an asset. 362 | optional string track_type_deprecated = 5; 363 | 364 | // The name of a registered policy to be used for this asset. 365 | optional string policy = 6; 366 | 367 | // Crypto period index, for media using key rotation. 368 | optional uint32 crypto_period_index = 7; 369 | 370 | // Optional protected context for group content. The grouped_license is a 371 | // serialized SignedMessage. 372 | optional bytes grouped_license = 8; 373 | 374 | // Protection scheme identifying the encryption algorithm. 375 | // Represented as one of the following 4CC values: 376 | // 'cenc' (AESCTR), 'cbc1' (AESCBC), 377 | // 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample). 378 | optional uint32 protection_scheme = 9; 379 | 380 | // Optional. For media using key rotation, this represents the duration 381 | // of each crypto period in seconds. 382 | optional uint32 crypto_period_seconds = 10; 383 | } 384 | 385 | 386 | // remove these when using it outside of protoc: 387 | 388 | // from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically 389 | message SignedLicenseRequest { 390 | enum MessageType { 391 | LICENSE_REQUEST = 1; 392 | LICENSE = 2; 393 | ERROR_RESPONSE = 3; 394 | SERVICE_CERTIFICATE_REQUEST = 4; 395 | SERVICE_CERTIFICATE = 5; 396 | } 397 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 398 | optional LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 399 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 400 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 401 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 402 | optional RemoteAttestation RemoteAttestation = 5; 403 | } 404 | 405 | // hack 406 | message SignedLicenseRequestRaw { 407 | enum MessageType { 408 | LICENSE_REQUEST = 1; 409 | LICENSE = 2; 410 | ERROR_RESPONSE = 3; 411 | SERVICE_CERTIFICATE_REQUEST = 4; 412 | SERVICE_CERTIFICATE = 5; 413 | } 414 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 415 | optional LicenseRequestRaw Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 416 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 417 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 418 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 419 | optional RemoteAttestation RemoteAttestation = 5; 420 | } 421 | 422 | 423 | message SignedLicense { 424 | enum MessageType { 425 | LICENSE_REQUEST = 1; 426 | LICENSE = 2; 427 | ERROR_RESPONSE = 3; 428 | SERVICE_CERTIFICATE_REQUEST = 4; 429 | SERVICE_CERTIFICATE = 5; 430 | } 431 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 432 | optional License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 433 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 434 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 435 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 436 | optional RemoteAttestation RemoteAttestation = 5; 437 | } 438 | 439 | message SignedServiceCertificate { 440 | enum MessageType { 441 | LICENSE_REQUEST = 1; 442 | LICENSE = 2; 443 | ERROR_RESPONSE = 3; 444 | SERVICE_CERTIFICATE_REQUEST = 4; 445 | SERVICE_CERTIFICATE = 5; 446 | } 447 | optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 448 | optional SignedDeviceCertificate Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 449 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 450 | optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 451 | optional bytes SessionKey = 4; // often RSA wrapped for licenses 452 | optional RemoteAttestation RemoteAttestation = 5; 453 | } 454 | 455 | //vmp support 456 | message FileHashes { 457 | message Signature { 458 | optional string filename = 1; 459 | optional bool test_signing = 2; //0 - release, 1 - testing 460 | optional bytes SHA512Hash = 3; 461 | optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file 462 | optional bytes signature = 5; 463 | } 464 | optional bytes signer = 1; 465 | repeated Signature signatures = 2; 466 | } 467 | -------------------------------------------------------------------------------- /pywidevine/L3/cdm/formats/wv_proto2_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: wv_proto2.proto 4 | """Generated protocol buffer code.""" 5 | from google.protobuf.internal import builder as _builder 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | 15 | 16 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fwv_proto2.proto\"\xe7\x05\n\x14\x43lientIdentification\x12-\n\x04Type\x18\x01 \x02(\x0e\x32\x1f.ClientIdentification.TokenType\x12\'\n\x05Token\x18\x02 \x01(\x0b\x32\x18.SignedDeviceCertificate\x12\x33\n\nClientInfo\x18\x03 \x03(\x0b\x32\x1f.ClientIdentification.NameValue\x12\x1b\n\x13ProviderClientToken\x18\x04 \x01(\x0c\x12\x16\n\x0eLicenseCounter\x18\x05 \x01(\r\x12\x45\n\x13_ClientCapabilities\x18\x06 \x01(\x0b\x32(.ClientIdentification.ClientCapabilities\x12 \n\x0b_FileHashes\x18\x07 \x01(\x0b\x32\x0b.FileHashes\x1a(\n\tNameValue\x12\x0c\n\x04Name\x18\x01 \x02(\t\x12\r\n\x05Value\x18\x02 \x02(\t\x1a\xa4\x02\n\x12\x43lientCapabilities\x12\x13\n\x0b\x43lientToken\x18\x01 \x01(\r\x12\x14\n\x0cSessionToken\x18\x02 \x01(\r\x12\"\n\x1aVideoResolutionConstraints\x18\x03 \x01(\r\x12L\n\x0eMaxHdcpVersion\x18\x04 \x01(\x0e\x32\x34.ClientIdentification.ClientCapabilities.HdcpVersion\x12\x1b\n\x13OemCryptoApiVersion\x18\x05 \x01(\r\"T\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\"S\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x16\n\x12\x44\x45VICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\"\x9b\x02\n\x11\x44\x65viceCertificate\x12\x30\n\x04Type\x18\x01 \x02(\x0e\x32\".DeviceCertificate.CertificateType\x12\x14\n\x0cSerialNumber\x18\x02 \x01(\x0c\x12\x1b\n\x13\x43reationTimeSeconds\x18\x03 \x01(\r\x12\x11\n\tPublicKey\x18\x04 \x01(\x0c\x12\x10\n\x08SystemId\x18\x05 \x01(\r\x12\x1c\n\x14TestDeviceDeprecated\x18\x06 \x01(\r\x12\x11\n\tServiceId\x18\x07 \x01(\x0c\"K\n\x0f\x43\x65rtificateType\x12\x08\n\x04ROOT\x10\x00\x12\x10\n\x0cINTERMEDIATE\x10\x01\x12\x0f\n\x0bUSER_DEVICE\x10\x02\x12\x0b\n\x07SERVICE\x10\x03\"\xc4\x01\n\x17\x44\x65viceCertificateStatus\x12\x14\n\x0cSerialNumber\x18\x01 \x01(\x0c\x12:\n\x06Status\x18\x02 \x01(\x0e\x32*.DeviceCertificateStatus.CertificateStatus\x12*\n\nDeviceInfo\x18\x04 \x01(\x0b\x32\x16.ProvisionedDeviceInfo\"+\n\x11\x43\x65rtificateStatus\x12\t\n\x05VALID\x10\x00\x12\x0b\n\x07REVOKED\x10\x01\"o\n\x1b\x44\x65viceCertificateStatusList\x12\x1b\n\x13\x43reationTimeSeconds\x18\x01 \x01(\r\x12\x33\n\x11\x43\x65rtificateStatus\x18\x02 \x03(\x0b\x32\x18.DeviceCertificateStatus\"\xaf\x01\n\x1d\x45ncryptedClientIdentification\x12\x11\n\tServiceId\x18\x01 \x02(\t\x12&\n\x1eServiceCertificateSerialNumber\x18\x02 \x01(\x0c\x12\x19\n\x11\x45ncryptedClientId\x18\x03 \x02(\x0c\x12\x1b\n\x13\x45ncryptedClientIdIv\x18\x04 \x02(\x0c\x12\x1b\n\x13\x45ncryptedPrivacyKey\x18\x05 \x02(\x0c\"\x9c\x01\n\x15LicenseIdentification\x12\x11\n\tRequestId\x18\x01 \x01(\x0c\x12\x11\n\tSessionId\x18\x02 \x01(\x0c\x12\x12\n\nPurchaseId\x18\x03 \x01(\x0c\x12\x1a\n\x04Type\x18\x04 \x01(\x0e\x32\x0c.LicenseType\x12\x0f\n\x07Version\x18\x05 \x01(\r\x12\x1c\n\x14ProviderSessionToken\x18\x06 \x01(\x0c\"\xa1\x0e\n\x07License\x12\"\n\x02Id\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12 \n\x07_Policy\x18\x02 \x01(\x0b\x32\x0f.License.Policy\x12\"\n\x03Key\x18\x03 \x03(\x0b\x32\x15.License.KeyContainer\x12\x18\n\x10LicenseStartTime\x18\x04 \x01(\r\x12!\n\x19RemoteAttestationVerified\x18\x05 \x01(\r\x12\x1b\n\x13ProviderClientToken\x18\x06 \x01(\x0c\x12\x18\n\x10ProtectionScheme\x18\x07 \x01(\r\x1a\xbb\x02\n\x06Policy\x12\x0f\n\x07\x43\x61nPlay\x18\x01 \x01(\x08\x12\x12\n\nCanPersist\x18\x02 \x01(\x08\x12\x10\n\x08\x43\x61nRenew\x18\x03 \x01(\x08\x12\x1d\n\x15RentalDurationSeconds\x18\x04 \x01(\r\x12\x1f\n\x17PlaybackDurationSeconds\x18\x05 \x01(\r\x12\x1e\n\x16LicenseDurationSeconds\x18\x06 \x01(\r\x12&\n\x1eRenewalRecoveryDurationSeconds\x18\x07 \x01(\r\x12\x18\n\x10RenewalServerUrl\x18\x08 \x01(\t\x12\x1b\n\x13RenewalDelaySeconds\x18\t \x01(\r\x12#\n\x1bRenewalRetryIntervalSeconds\x18\n \x01(\r\x12\x16\n\x0eRenewWithUsage\x18\x0b \x01(\x08\x1a\xf9\t\n\x0cKeyContainer\x12\n\n\x02Id\x18\x01 \x01(\x0c\x12\n\n\x02Iv\x18\x02 \x01(\x0c\x12\x0b\n\x03Key\x18\x03 \x01(\x0c\x12+\n\x04Type\x18\x04 \x01(\x0e\x32\x1d.License.KeyContainer.KeyType\x12\x32\n\x05Level\x18\x05 \x01(\x0e\x32#.License.KeyContainer.SecurityLevel\x12\x42\n\x12RequiredProtection\x18\x06 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\x12\x43\n\x13RequestedProtection\x18\x07 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\x12\x35\n\x0b_KeyControl\x18\x08 \x01(\x0b\x32 .License.KeyContainer.KeyControl\x12[\n\x1e_OperatorSessionKeyPermissions\x18\t \x01(\x0b\x32\x33.License.KeyContainer.OperatorSessionKeyPermissions\x12S\n\x1aVideoResolutionConstraints\x18\n \x03(\x0b\x32/.License.KeyContainer.VideoResolutionConstraint\x1a\xdb\x01\n\x10OutputProtection\x12\x42\n\x04Hdcp\x18\x01 \x01(\x0e\x32\x34.ClientIdentification.ClientCapabilities.HdcpVersion\x12>\n\tCgmsFlags\x18\x02 \x01(\x0e\x32+.License.KeyContainer.OutputProtection.CGMS\"C\n\x04\x43GMS\x12\r\n\tCOPY_FREE\x10\x00\x12\r\n\tCOPY_ONCE\x10\x02\x12\x0e\n\nCOPY_NEVER\x10\x03\x12\r\n\tCGMS_NONE\x10*\x1a\x31\n\nKeyControl\x12\x17\n\x0fKeyControlBlock\x18\x01 \x02(\x0c\x12\n\n\x02Iv\x18\x02 \x01(\x0c\x1a|\n\x1dOperatorSessionKeyPermissions\x12\x14\n\x0c\x41llowEncrypt\x18\x01 \x01(\r\x12\x14\n\x0c\x41llowDecrypt\x18\x02 \x01(\r\x12\x11\n\tAllowSign\x18\x03 \x01(\r\x12\x1c\n\x14\x41llowSignatureVerify\x18\x04 \x01(\r\x1a\x99\x01\n\x19VideoResolutionConstraint\x12\x1b\n\x13MinResolutionPixels\x18\x01 \x01(\r\x12\x1b\n\x13MaxResolutionPixels\x18\x02 \x01(\r\x12\x42\n\x12RequiredProtection\x18\x03 \x01(\x0b\x32&.License.KeyContainer.OutputProtection\"J\n\x07KeyType\x12\x0b\n\x07SIGNING\x10\x01\x12\x0b\n\x07\x43ONTENT\x10\x02\x12\x0f\n\x0bKEY_CONTROL\x10\x03\x12\x14\n\x10OPERATOR_SESSION\x10\x04\"z\n\rSecurityLevel\x12\x14\n\x10SW_SECURE_CRYPTO\x10\x01\x12\x14\n\x10SW_SECURE_DECODE\x10\x02\x12\x14\n\x10HW_SECURE_CRYPTO\x10\x03\x12\x14\n\x10HW_SECURE_DECODE\x10\x04\x12\x11\n\rHW_SECURE_ALL\x10\x05\"\x98\x01\n\x0cLicenseError\x12&\n\tErrorCode\x18\x01 \x01(\x0e\x32\x13.LicenseError.Error\"`\n\x05\x45rror\x12\x1e\n\x1aINVALID_DEVICE_CERTIFICATE\x10\x01\x12\x1e\n\x1aREVOKED_DEVICE_CERTIFICATE\x10\x02\x12\x17\n\x13SERVICE_UNAVAILABLE\x10\x03\"\xac\x07\n\x0eLicenseRequest\x12\'\n\x08\x43lientId\x18\x01 \x01(\x0b\x32\x15.ClientIdentification\x12\x38\n\tContentId\x18\x02 \x01(\x0b\x32%.LicenseRequest.ContentIdentification\x12)\n\x04Type\x18\x03 \x01(\x0e\x32\x1b.LicenseRequest.RequestType\x12\x13\n\x0bRequestTime\x18\x04 \x01(\r\x12!\n\x19KeyControlNonceDeprecated\x18\x05 \x01(\x0c\x12)\n\x0fProtocolVersion\x18\x06 \x01(\x0e\x32\x10.ProtocolVersion\x12\x17\n\x0fKeyControlNonce\x18\x07 \x01(\r\x12\x39\n\x11\x45ncryptedClientId\x18\x08 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x1a\xa2\x04\n\x15\x43ontentIdentification\x12:\n\x06\x43\x65ncId\x18\x01 \x01(\x0b\x32*.LicenseRequest.ContentIdentification.CENC\x12:\n\x06WebmId\x18\x02 \x01(\x0b\x32*.LicenseRequest.ContentIdentification.WebM\x12\x46\n\x07License\x18\x03 \x01(\x0b\x32\x35.LicenseRequest.ContentIdentification.ExistingLicense\x1a_\n\x04\x43\x45NC\x12!\n\x04Pssh\x18\x01 \x01(\x0b\x32\x13.WidevineCencHeader\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1aL\n\x04WebM\x12\x0e\n\x06Header\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1a\x99\x01\n\x0f\x45xistingLicense\x12)\n\tLicenseId\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12\x1b\n\x13SecondsSinceStarted\x18\x02 \x01(\r\x12\x1e\n\x16SecondsSinceLastPlayed\x18\x03 \x01(\r\x12\x1e\n\x16SessionUsageTableEntry\x18\x04 \x01(\x0c\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xa9\x07\n\x11LicenseRequestRaw\x12\'\n\x08\x43lientId\x18\x01 \x01(\x0b\x32\x15.ClientIdentification\x12;\n\tContentId\x18\x02 \x01(\x0b\x32(.LicenseRequestRaw.ContentIdentification\x12,\n\x04Type\x18\x03 \x01(\x0e\x32\x1e.LicenseRequestRaw.RequestType\x12\x13\n\x0bRequestTime\x18\x04 \x01(\r\x12!\n\x19KeyControlNonceDeprecated\x18\x05 \x01(\x0c\x12)\n\x0fProtocolVersion\x18\x06 \x01(\x0e\x32\x10.ProtocolVersion\x12\x17\n\x0fKeyControlNonce\x18\x07 \x01(\r\x12\x39\n\x11\x45ncryptedClientId\x18\x08 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x1a\x96\x04\n\x15\x43ontentIdentification\x12=\n\x06\x43\x65ncId\x18\x01 \x01(\x0b\x32-.LicenseRequestRaw.ContentIdentification.CENC\x12=\n\x06WebmId\x18\x02 \x01(\x0b\x32-.LicenseRequestRaw.ContentIdentification.WebM\x12I\n\x07License\x18\x03 \x01(\x0b\x32\x38.LicenseRequestRaw.ContentIdentification.ExistingLicense\x1aJ\n\x04\x43\x45NC\x12\x0c\n\x04Pssh\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1aL\n\x04WebM\x12\x0e\n\x06Header\x18\x01 \x01(\x0c\x12!\n\x0bLicenseType\x18\x02 \x01(\x0e\x32\x0c.LicenseType\x12\x11\n\tRequestId\x18\x03 \x01(\x0c\x1a\x99\x01\n\x0f\x45xistingLicense\x12)\n\tLicenseId\x18\x01 \x01(\x0b\x32\x16.LicenseIdentification\x12\x1b\n\x13SecondsSinceStarted\x18\x02 \x01(\r\x12\x1e\n\x16SecondsSinceLastPlayed\x18\x03 \x01(\r\x12\x1e\n\x16SessionUsageTableEntry\x18\x04 \x01(\x0c\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xa6\x02\n\x15ProvisionedDeviceInfo\x12\x10\n\x08SystemId\x18\x01 \x01(\r\x12\x0b\n\x03Soc\x18\x02 \x01(\t\x12\x14\n\x0cManufacturer\x18\x03 \x01(\t\x12\r\n\x05Model\x18\x04 \x01(\t\x12\x12\n\nDeviceType\x18\x05 \x01(\t\x12\x11\n\tModelYear\x18\x06 \x01(\r\x12=\n\rSecurityLevel\x18\x07 \x01(\x0e\x32&.ProvisionedDeviceInfo.WvSecurityLevel\x12\x12\n\nTestDevice\x18\x08 \x01(\r\"O\n\x0fWvSecurityLevel\x12\x15\n\x11LEVEL_UNSPECIFIED\x10\x00\x12\x0b\n\x07LEVEL_1\x10\x01\x12\x0b\n\x07LEVEL_2\x10\x02\x12\x0b\n\x07LEVEL_3\x10\x03\"\x15\n\x13ProvisioningOptions\"\x15\n\x13ProvisioningRequest\"\x16\n\x14ProvisioningResponse\"i\n\x11RemoteAttestation\x12\x33\n\x0b\x43\x65rtificate\x18\x01 \x01(\x0b\x32\x1e.EncryptedClientIdentification\x12\x0c\n\x04Salt\x18\x02 \x01(\t\x12\x11\n\tSignature\x18\x03 \x01(\t\"\r\n\x0bSessionInit\"\x0e\n\x0cSessionState\"\x1d\n\x1bSignedCertificateStatusList\"\x86\x01\n\x17SignedDeviceCertificate\x12.\n\x12_DeviceCertificate\x18\x01 \x01(\x0b\x32\x12.DeviceCertificate\x12\x11\n\tSignature\x18\x02 \x01(\x0c\x12(\n\x06Signer\x18\x03 \x01(\x0b\x32\x18.SignedDeviceCertificate\"\x1b\n\x19SignedProvisioningMessage\"\x9b\x02\n\rSignedMessage\x12(\n\x04Type\x18\x01 \x01(\x0e\x32\x1a.SignedMessage.MessageType\x12\x0b\n\x03Msg\x18\x02 \x01(\x0c\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xc5\x02\n\x12WidevineCencHeader\x12\x30\n\talgorithm\x18\x01 \x01(\x0e\x32\x1d.WidevineCencHeader.Algorithm\x12\x0e\n\x06key_id\x18\x02 \x03(\x0c\x12\x10\n\x08provider\x18\x03 \x01(\t\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x1d\n\x15track_type_deprecated\x18\x05 \x01(\t\x12\x0e\n\x06policy\x18\x06 \x01(\t\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x17\n\x0fgrouped_license\x18\x08 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\t \x01(\r\x12\x1d\n\x15\x63rypto_period_seconds\x18\n \x01(\r\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"\xba\x02\n\x14SignedLicenseRequest\x12/\n\x04Type\x18\x01 \x01(\x0e\x32!.SignedLicenseRequest.MessageType\x12\x1c\n\x03Msg\x18\x02 \x01(\x0b\x32\x0f.LicenseRequest\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xc3\x02\n\x17SignedLicenseRequestRaw\x12\x32\n\x04Type\x18\x01 \x01(\x0e\x32$.SignedLicenseRequestRaw.MessageType\x12\x1f\n\x03Msg\x18\x02 \x01(\x0b\x32\x12.LicenseRequestRaw\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xa5\x02\n\rSignedLicense\x12(\n\x04Type\x18\x01 \x01(\x0e\x32\x1a.SignedLicense.MessageType\x12\x15\n\x03Msg\x18\x02 \x01(\x0b\x32\x08.License\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xcb\x02\n\x18SignedServiceCertificate\x12\x33\n\x04Type\x18\x01 \x01(\x0e\x32%.SignedServiceCertificate.MessageType\x12%\n\x03Msg\x18\x02 \x01(\x0b\x32\x18.SignedDeviceCertificate\x12\x11\n\tSignature\x18\x03 \x01(\x0c\x12\x12\n\nSessionKey\x18\x04 \x01(\x0c\x12-\n\x11RemoteAttestation\x18\x05 \x01(\x0b\x32\x12.RemoteAttestation\"}\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\"\xb5\x01\n\nFileHashes\x12\x0e\n\x06signer\x18\x01 \x01(\x0c\x12)\n\nsignatures\x18\x02 \x03(\x0b\x32\x15.FileHashes.Signature\x1al\n\tSignature\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x14\n\x0ctest_signing\x18\x02 \x01(\x08\x12\x12\n\nSHA512Hash\x18\x03 \x01(\x0c\x12\x10\n\x08main_exe\x18\x04 \x01(\x08\x12\x11\n\tsignature\x18\x05 \x01(\x0c*1\n\x0bLicenseType\x12\x08\n\x04ZERO\x10\x00\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x01\x12\x0b\n\x07OFFLINE\x10\x02*\x1e\n\x0fProtocolVersion\x12\x0b\n\x07\x43URRENT\x10\x15') 17 | 18 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) 19 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'wv_proto2_pb2', globals()) 20 | if _descriptor._USE_C_DESCRIPTORS == False: 21 | 22 | DESCRIPTOR._options = None 23 | _LICENSETYPE._serialized_start=8339 24 | _LICENSETYPE._serialized_end=8388 25 | _PROTOCOLVERSION._serialized_start=8390 26 | _PROTOCOLVERSION._serialized_end=8420 27 | _CLIENTIDENTIFICATION._serialized_start=20 28 | _CLIENTIDENTIFICATION._serialized_end=763 29 | _CLIENTIDENTIFICATION_NAMEVALUE._serialized_start=343 30 | _CLIENTIDENTIFICATION_NAMEVALUE._serialized_end=383 31 | _CLIENTIDENTIFICATION_CLIENTCAPABILITIES._serialized_start=386 32 | _CLIENTIDENTIFICATION_CLIENTCAPABILITIES._serialized_end=678 33 | _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION._serialized_start=594 34 | _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION._serialized_end=678 35 | _CLIENTIDENTIFICATION_TOKENTYPE._serialized_start=680 36 | _CLIENTIDENTIFICATION_TOKENTYPE._serialized_end=763 37 | _DEVICECERTIFICATE._serialized_start=766 38 | _DEVICECERTIFICATE._serialized_end=1049 39 | _DEVICECERTIFICATE_CERTIFICATETYPE._serialized_start=974 40 | _DEVICECERTIFICATE_CERTIFICATETYPE._serialized_end=1049 41 | _DEVICECERTIFICATESTATUS._serialized_start=1052 42 | _DEVICECERTIFICATESTATUS._serialized_end=1248 43 | _DEVICECERTIFICATESTATUS_CERTIFICATESTATUS._serialized_start=1205 44 | _DEVICECERTIFICATESTATUS_CERTIFICATESTATUS._serialized_end=1248 45 | _DEVICECERTIFICATESTATUSLIST._serialized_start=1250 46 | _DEVICECERTIFICATESTATUSLIST._serialized_end=1361 47 | _ENCRYPTEDCLIENTIDENTIFICATION._serialized_start=1364 48 | _ENCRYPTEDCLIENTIDENTIFICATION._serialized_end=1539 49 | _LICENSEIDENTIFICATION._serialized_start=1542 50 | _LICENSEIDENTIFICATION._serialized_end=1698 51 | _LICENSE._serialized_start=1701 52 | _LICENSE._serialized_end=3526 53 | _LICENSE_POLICY._serialized_start=1935 54 | _LICENSE_POLICY._serialized_end=2250 55 | _LICENSE_KEYCONTAINER._serialized_start=2253 56 | _LICENSE_KEYCONTAINER._serialized_end=3526 57 | _LICENSE_KEYCONTAINER_OUTPUTPROTECTION._serialized_start=2774 58 | _LICENSE_KEYCONTAINER_OUTPUTPROTECTION._serialized_end=2993 59 | _LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS._serialized_start=2926 60 | _LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS._serialized_end=2993 61 | _LICENSE_KEYCONTAINER_KEYCONTROL._serialized_start=2995 62 | _LICENSE_KEYCONTAINER_KEYCONTROL._serialized_end=3044 63 | _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS._serialized_start=3046 64 | _LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS._serialized_end=3170 65 | _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT._serialized_start=3173 66 | _LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT._serialized_end=3326 67 | _LICENSE_KEYCONTAINER_KEYTYPE._serialized_start=3328 68 | _LICENSE_KEYCONTAINER_KEYTYPE._serialized_end=3402 69 | _LICENSE_KEYCONTAINER_SECURITYLEVEL._serialized_start=3404 70 | _LICENSE_KEYCONTAINER_SECURITYLEVEL._serialized_end=3526 71 | _LICENSEERROR._serialized_start=3529 72 | _LICENSEERROR._serialized_end=3681 73 | _LICENSEERROR_ERROR._serialized_start=3585 74 | _LICENSEERROR_ERROR._serialized_end=3681 75 | _LICENSEREQUEST._serialized_start=3684 76 | _LICENSEREQUEST._serialized_end=4624 77 | _LICENSEREQUEST_CONTENTIDENTIFICATION._serialized_start=4028 78 | _LICENSEREQUEST_CONTENTIDENTIFICATION._serialized_end=4574 79 | _LICENSEREQUEST_CONTENTIDENTIFICATION_CENC._serialized_start=4245 80 | _LICENSEREQUEST_CONTENTIDENTIFICATION_CENC._serialized_end=4340 81 | _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM._serialized_start=4342 82 | _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBM._serialized_end=4418 83 | _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_start=4421 84 | _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_end=4574 85 | _LICENSEREQUEST_REQUESTTYPE._serialized_start=4576 86 | _LICENSEREQUEST_REQUESTTYPE._serialized_end=4624 87 | _LICENSEREQUESTRAW._serialized_start=4627 88 | _LICENSEREQUESTRAW._serialized_end=5564 89 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION._serialized_start=4980 90 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION._serialized_end=5514 91 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC._serialized_start=5206 92 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_CENC._serialized_end=5280 93 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM._serialized_start=4342 94 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_WEBM._serialized_end=4418 95 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_start=4421 96 | _LICENSEREQUESTRAW_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_end=4574 97 | _LICENSEREQUESTRAW_REQUESTTYPE._serialized_start=4576 98 | _LICENSEREQUESTRAW_REQUESTTYPE._serialized_end=4624 99 | _PROVISIONEDDEVICEINFO._serialized_start=5567 100 | _PROVISIONEDDEVICEINFO._serialized_end=5861 101 | _PROVISIONEDDEVICEINFO_WVSECURITYLEVEL._serialized_start=5782 102 | _PROVISIONEDDEVICEINFO_WVSECURITYLEVEL._serialized_end=5861 103 | _PROVISIONINGOPTIONS._serialized_start=5863 104 | _PROVISIONINGOPTIONS._serialized_end=5884 105 | _PROVISIONINGREQUEST._serialized_start=5886 106 | _PROVISIONINGREQUEST._serialized_end=5907 107 | _PROVISIONINGRESPONSE._serialized_start=5909 108 | _PROVISIONINGRESPONSE._serialized_end=5931 109 | _REMOTEATTESTATION._serialized_start=5933 110 | _REMOTEATTESTATION._serialized_end=6038 111 | _SESSIONINIT._serialized_start=6040 112 | _SESSIONINIT._serialized_end=6053 113 | _SESSIONSTATE._serialized_start=6055 114 | _SESSIONSTATE._serialized_end=6069 115 | _SIGNEDCERTIFICATESTATUSLIST._serialized_start=6071 116 | _SIGNEDCERTIFICATESTATUSLIST._serialized_end=6100 117 | _SIGNEDDEVICECERTIFICATE._serialized_start=6103 118 | _SIGNEDDEVICECERTIFICATE._serialized_end=6237 119 | _SIGNEDPROVISIONINGMESSAGE._serialized_start=6239 120 | _SIGNEDPROVISIONINGMESSAGE._serialized_end=6266 121 | _SIGNEDMESSAGE._serialized_start=6269 122 | _SIGNEDMESSAGE._serialized_end=6552 123 | _SIGNEDMESSAGE_MESSAGETYPE._serialized_start=6427 124 | _SIGNEDMESSAGE_MESSAGETYPE._serialized_end=6552 125 | _WIDEVINECENCHEADER._serialized_start=6555 126 | _WIDEVINECENCHEADER._serialized_end=6880 127 | _WIDEVINECENCHEADER_ALGORITHM._serialized_start=6840 128 | _WIDEVINECENCHEADER_ALGORITHM._serialized_end=6880 129 | _SIGNEDLICENSEREQUEST._serialized_start=6883 130 | _SIGNEDLICENSEREQUEST._serialized_end=7197 131 | _SIGNEDLICENSEREQUEST_MESSAGETYPE._serialized_start=6427 132 | _SIGNEDLICENSEREQUEST_MESSAGETYPE._serialized_end=6552 133 | _SIGNEDLICENSEREQUESTRAW._serialized_start=7200 134 | _SIGNEDLICENSEREQUESTRAW._serialized_end=7523 135 | _SIGNEDLICENSEREQUESTRAW_MESSAGETYPE._serialized_start=6427 136 | _SIGNEDLICENSEREQUESTRAW_MESSAGETYPE._serialized_end=6552 137 | _SIGNEDLICENSE._serialized_start=7526 138 | _SIGNEDLICENSE._serialized_end=7819 139 | _SIGNEDLICENSE_MESSAGETYPE._serialized_start=6427 140 | _SIGNEDLICENSE_MESSAGETYPE._serialized_end=6552 141 | _SIGNEDSERVICECERTIFICATE._serialized_start=7822 142 | _SIGNEDSERVICECERTIFICATE._serialized_end=8153 143 | _SIGNEDSERVICECERTIFICATE_MESSAGETYPE._serialized_start=6427 144 | _SIGNEDSERVICECERTIFICATE_MESSAGETYPE._serialized_end=6552 145 | _FILEHASHES._serialized_start=8156 146 | _FILEHASHES._serialized_end=8337 147 | _FILEHASHES_SIGNATURE._serialized_start=8229 148 | _FILEHASHES_SIGNATURE._serialized_end=8337 149 | # @@protoc_insertion_point(module_scope) 150 | -------------------------------------------------------------------------------- /pywidevine/L3/cdm/formats/wv_proto3.proto: -------------------------------------------------------------------------------- 1 | // beware proto3 won't show missing fields it seems, need to change to "proto2" and add "optional" before every field, and remove all the dummy enum members I added: 2 | syntax = "proto3"; 3 | 4 | // from x86 (partial), most of it from the ARM version: 5 | message ClientIdentification { 6 | enum TokenType { 7 | KEYBOX = 0; 8 | DEVICE_CERTIFICATE = 1; 9 | REMOTE_ATTESTATION_CERTIFICATE = 2; 10 | } 11 | message NameValue { 12 | string Name = 1; 13 | string Value = 2; 14 | } 15 | message ClientCapabilities { 16 | enum HdcpVersion { 17 | HDCP_NONE = 0; 18 | HDCP_V1 = 1; 19 | HDCP_V2 = 2; 20 | HDCP_V2_1 = 3; 21 | HDCP_V2_2 = 4; 22 | } 23 | uint32 ClientToken = 1; 24 | uint32 SessionToken = 2; 25 | uint32 VideoResolutionConstraints = 3; 26 | HdcpVersion MaxHdcpVersion = 4; 27 | uint32 OemCryptoApiVersion = 5; 28 | } 29 | TokenType Type = 1; 30 | //bytes Token = 2; // by default the client treats this as blob, but it's usually a DeviceCertificate, so for usefulness sake, I'm replacing it with this one: 31 | SignedDeviceCertificate Token = 2; 32 | repeated NameValue ClientInfo = 3; 33 | bytes ProviderClientToken = 4; 34 | uint32 LicenseCounter = 5; 35 | ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later 36 | } 37 | 38 | message DeviceCertificate { 39 | enum CertificateType { 40 | ROOT = 0; 41 | INTERMEDIATE = 1; 42 | USER_DEVICE = 2; 43 | SERVICE = 3; 44 | } 45 | //ProvisionedDeviceInfo.WvSecurityLevel Type = 1; // is this how one is supposed to call it? (it's an enum) there might be a bug here, with CertificateType getting confused with WvSecurityLevel, for now renaming it (verify against other binaries) 46 | CertificateType Type = 1; 47 | bytes SerialNumber = 2; 48 | uint32 CreationTimeSeconds = 3; 49 | bytes PublicKey = 4; 50 | uint32 SystemId = 5; 51 | uint32 TestDeviceDeprecated = 6; // is it bool or int? 52 | bytes ServiceId = 7; // service URL for service certificates 53 | } 54 | 55 | // missing some references, 56 | message DeviceCertificateStatus { 57 | enum CertificateStatus { 58 | VALID = 0; 59 | REVOKED = 1; 60 | } 61 | bytes SerialNumber = 1; 62 | CertificateStatus Status = 2; 63 | ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated? 64 | } 65 | 66 | message DeviceCertificateStatusList { 67 | uint32 CreationTimeSeconds = 1; 68 | repeated DeviceCertificateStatus CertificateStatus = 2; 69 | } 70 | 71 | message EncryptedClientIdentification { 72 | string ServiceId = 1; 73 | bytes ServiceCertificateSerialNumber = 2; 74 | bytes EncryptedClientId = 3; 75 | bytes EncryptedClientIdIv = 4; 76 | bytes EncryptedPrivacyKey = 5; 77 | } 78 | 79 | // todo: fill (for this top-level type, it might be impossible/difficult) 80 | enum LicenseType { 81 | ZERO = 0; 82 | DEFAULT = 1; // do not know what this is either, but should be 1; on recent versions may go up to 3 (latest x86) 83 | } 84 | 85 | // todo: fill (for this top-level type, it might be impossible/difficult) 86 | // this is just a guess because these globals got lost, but really, do we need more? 87 | enum ProtocolVersion { 88 | DUMMY = 0; 89 | CURRENT = 21; // don't have symbols for this 90 | } 91 | 92 | 93 | message LicenseIdentification { 94 | bytes RequestId = 1; 95 | bytes SessionId = 2; 96 | bytes PurchaseId = 3; 97 | LicenseType Type = 4; 98 | uint32 Version = 5; 99 | bytes ProviderSessionToken = 6; 100 | } 101 | 102 | 103 | message License { 104 | message Policy { 105 | uint32 CanPlay = 1; 106 | uint32 CanPersist = 2; 107 | uint32 CanRenew = 3; 108 | uint32 RentalDurationSeconds = 4; 109 | uint32 PlaybackDurationSeconds = 5; 110 | uint32 LicenseDurationSeconds = 6; 111 | uint32 RenewalRecoveryDurationSeconds = 7; 112 | string RenewalServerUrl = 8; 113 | uint32 RenewalDelaySeconds = 9; 114 | uint32 RenewalRetryIntervalSeconds = 10; 115 | uint32 RenewWithUsage = 11; 116 | uint32 UnknownPolicy12 = 12; 117 | } 118 | message KeyContainer { 119 | enum KeyType { 120 | _NOKEYTYPE = 0; // dummy, added to satisfy proto3, not present in original 121 | SIGNING = 1; 122 | CONTENT = 2; 123 | KEY_CONTROL = 3; 124 | OPERATOR_SESSION = 4; 125 | } 126 | enum SecurityLevel { 127 | _NOSECLEVEL = 0; // dummy, added to satisfy proto3, not present in original 128 | SW_SECURE_CRYPTO = 1; 129 | SW_SECURE_DECODE = 2; 130 | HW_SECURE_CRYPTO = 3; 131 | HW_SECURE_DECODE = 4; 132 | HW_SECURE_ALL = 5; 133 | } 134 | message OutputProtection { 135 | enum CGMS { 136 | COPY_FREE = 0; 137 | COPY_ONCE = 2; 138 | COPY_NEVER = 3; 139 | CGMS_NONE = 0x2A; // PC default! 140 | } 141 | ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away 142 | CGMS CgmsFlags = 2; 143 | } 144 | message KeyControl { 145 | bytes KeyControlBlock = 1; // what is this? 146 | bytes Iv = 2; 147 | } 148 | message OperatorSessionKeyPermissions { 149 | uint32 AllowEncrypt = 1; 150 | uint32 AllowDecrypt = 2; 151 | uint32 AllowSign = 3; 152 | uint32 AllowSignatureVerify = 4; 153 | } 154 | message VideoResolutionConstraint { 155 | uint32 MinResolutionPixels = 1; 156 | uint32 MaxResolutionPixels = 2; 157 | OutputProtection RequiredProtection = 3; 158 | } 159 | bytes Id = 1; 160 | bytes Iv = 2; 161 | bytes Key = 3; 162 | KeyType Type = 4; 163 | SecurityLevel Level = 5; 164 | OutputProtection RequiredProtection = 6; 165 | OutputProtection RequestedProtection = 7; 166 | KeyControl _KeyControl = 8; // duped names, etc 167 | OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc 168 | repeated VideoResolutionConstraint VideoResolutionConstraints = 10; 169 | } 170 | LicenseIdentification Id = 1; 171 | Policy _Policy = 2; // duped names, etc 172 | repeated KeyContainer Key = 3; 173 | uint32 LicenseStartTime = 4; 174 | uint32 RemoteAttestationVerified = 5; // bool? 175 | bytes ProviderClientToken = 6; 176 | // there might be more, check with newer versions (I see field 7-8 in a lic) 177 | // this appeared in latest x86: 178 | uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc) 179 | bytes UnknownHdcpDataField = 8; 180 | } 181 | 182 | message LicenseError { 183 | enum Error { 184 | DUMMY_NO_ERROR = 0; // dummy, added to satisfy proto3 185 | INVALID_DEVICE_CERTIFICATE = 1; 186 | REVOKED_DEVICE_CERTIFICATE = 2; 187 | SERVICE_UNAVAILABLE = 3; 188 | } 189 | //LicenseRequest.RequestType ErrorCode; // clang mismatch 190 | Error ErrorCode = 1; 191 | } 192 | 193 | message LicenseRequest { 194 | message ContentIdentification { 195 | message CENC { 196 | // bytes Pssh = 1; // the client's definition is opaque, it doesn't care about the contents, but the PSSH has a clear definition that is understood and requested by the server, thus I'll replace it with: 197 | WidevineCencHeader Pssh = 1; 198 | LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1 199 | bytes RequestId = 3; 200 | } 201 | message WebM { 202 | bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used 203 | LicenseType LicenseType = 2; 204 | bytes RequestId = 3; 205 | } 206 | message ExistingLicense { 207 | LicenseIdentification LicenseId = 1; 208 | uint32 SecondsSinceStarted = 2; 209 | uint32 SecondsSinceLastPlayed = 3; 210 | bytes SessionUsageTableEntry = 4; 211 | } 212 | CENC CencId = 1; 213 | WebM WebmId = 2; 214 | ExistingLicense License = 3; 215 | } 216 | enum RequestType { 217 | DUMMY_REQ_TYPE = 0; // dummy, added to satisfy proto3 218 | NEW = 1; 219 | RENEWAL = 2; 220 | RELEASE = 3; 221 | } 222 | ClientIdentification ClientId = 1; 223 | ContentIdentification ContentId = 2; 224 | RequestType Type = 3; 225 | uint32 RequestTime = 4; 226 | bytes KeyControlNonceDeprecated = 5; 227 | ProtocolVersion ProtocolVersion = 6; // lacking symbols for this 228 | uint32 KeyControlNonce = 7; 229 | EncryptedClientIdentification EncryptedClientId = 8; 230 | } 231 | 232 | message ProvisionedDeviceInfo { 233 | enum WvSecurityLevel { 234 | LEVEL_UNSPECIFIED = 0; 235 | LEVEL_1 = 1; 236 | LEVEL_2 = 2; 237 | LEVEL_3 = 3; 238 | } 239 | uint32 SystemId = 1; 240 | string Soc = 2; 241 | string Manufacturer = 3; 242 | string Model = 4; 243 | string DeviceType = 5; 244 | uint32 ModelYear = 6; 245 | WvSecurityLevel SecurityLevel = 7; 246 | uint32 TestDevice = 8; // bool? 247 | } 248 | 249 | 250 | // todo: fill 251 | message ProvisioningOptions { 252 | } 253 | 254 | // todo: fill 255 | message ProvisioningRequest { 256 | } 257 | 258 | // todo: fill 259 | message ProvisioningResponse { 260 | } 261 | 262 | message RemoteAttestation { 263 | EncryptedClientIdentification Certificate = 1; 264 | string Salt = 2; 265 | string Signature = 3; 266 | } 267 | 268 | // todo: fill 269 | message SessionInit { 270 | } 271 | 272 | // todo: fill 273 | message SessionState { 274 | } 275 | 276 | // todo: fill 277 | message SignedCertificateStatusList { 278 | } 279 | 280 | message SignedDeviceCertificate { 281 | 282 | //bytes DeviceCertificate = 1; // again, they use a buffer where it's supposed to be a message, so we'll replace it with what it really is: 283 | DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later 284 | bytes Signature = 2; 285 | SignedDeviceCertificate Signer = 3; 286 | } 287 | 288 | 289 | // todo: fill 290 | message SignedProvisioningMessage { 291 | } 292 | 293 | // the root of all messages, from either server or client 294 | message SignedMessage { 295 | enum MessageType { 296 | DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3 297 | LICENSE_REQUEST = 1; 298 | LICENSE = 2; 299 | ERROR_RESPONSE = 3; 300 | SERVICE_CERTIFICATE_REQUEST = 4; 301 | SERVICE_CERTIFICATE = 5; 302 | } 303 | MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 304 | bytes Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 305 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 306 | bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 307 | bytes SessionKey = 4; // often RSA wrapped for licenses 308 | RemoteAttestation RemoteAttestation = 5; 309 | } 310 | 311 | 312 | 313 | // This message is copied from google's docs, not reversed: 314 | message WidevineCencHeader { 315 | enum Algorithm { 316 | UNENCRYPTED = 0; 317 | AESCTR = 1; 318 | }; 319 | Algorithm algorithm = 1; 320 | repeated bytes key_id = 2; 321 | 322 | // Content provider name. 323 | string provider = 3; 324 | 325 | // A content identifier, specified by content provider. 326 | bytes content_id = 4; 327 | 328 | // Track type. Acceptable values are SD, HD and AUDIO. Used to 329 | // differentiate content keys used by an asset. 330 | string track_type_deprecated = 5; 331 | 332 | // The name of a registered policy to be used for this asset. 333 | string policy = 6; 334 | 335 | // Crypto period index, for media using key rotation. 336 | uint32 crypto_period_index = 7; 337 | 338 | // Optional protected context for group content. The grouped_license is a 339 | // serialized SignedMessage. 340 | bytes grouped_license = 8; 341 | 342 | // Protection scheme identifying the encryption algorithm. 343 | // Represented as one of the following 4CC values: 344 | // 'cenc' (AESCTR), 'cbc1' (AESCBC), 345 | // 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample). 346 | uint32 protection_scheme = 9; 347 | 348 | // Optional. For media using key rotation, this represents the duration 349 | // of each crypto period in seconds. 350 | uint32 crypto_period_seconds = 10; 351 | } 352 | 353 | 354 | 355 | 356 | // from here on, it's just for testing, these messages don't exist in the binaries, I'm adding them to avoid detecting type programmatically 357 | message SignedLicenseRequest { 358 | enum MessageType { 359 | DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3 360 | LICENSE_REQUEST = 1; 361 | LICENSE = 2; 362 | ERROR_RESPONSE = 3; 363 | SERVICE_CERTIFICATE_REQUEST = 4; 364 | SERVICE_CERTIFICATE = 5; 365 | } 366 | MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 367 | LicenseRequest Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 368 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 369 | bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 370 | bytes SessionKey = 4; // often RSA wrapped for licenses 371 | RemoteAttestation RemoteAttestation = 5; 372 | } 373 | 374 | message SignedLicense { 375 | enum MessageType { 376 | DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3 377 | LICENSE_REQUEST = 1; 378 | LICENSE = 2; 379 | ERROR_RESPONSE = 3; 380 | SERVICE_CERTIFICATE_REQUEST = 4; 381 | SERVICE_CERTIFICATE = 5; 382 | } 383 | MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel 384 | License Msg = 2; // this has to be casted dynamically, to LicenseRequest, License or LicenseError (? unconfirmed), for Request, no other fields but Type need to be present 385 | // for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE 386 | bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now) 387 | bytes SessionKey = 4; // often RSA wrapped for licenses 388 | RemoteAttestation RemoteAttestation = 5; 389 | } -------------------------------------------------------------------------------- /pywidevine/L3/cdm/key.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | 3 | class Key: 4 | def __init__(self, kid, type, key, permissions=[]): 5 | self.kid = kid 6 | self.type = type 7 | self.key = key 8 | self.permissions = permissions 9 | 10 | def __repr__(self): 11 | if self.type == "OPERATOR_SESSION": 12 | return "key(kid={}, type={}, key={}, permissions={})".format(self.kid, self.type, binascii.hexlify(self.key), self.permissions) 13 | else: 14 | return "key(kid={}, type={}, key={})".format(self.kid, self.type, binascii.hexlify(self.key)) 15 | -------------------------------------------------------------------------------- /pywidevine/L3/cdm/session.py: -------------------------------------------------------------------------------- 1 | class Session: 2 | def __init__(self, session_id, init_data, device_config, offline): 3 | self.session_id = session_id 4 | self.init_data = init_data 5 | self.offline = offline 6 | self.device_config = device_config 7 | self.device_key = None 8 | self.session_key = None 9 | self.derived_keys = { 10 | 'enc': None, 11 | 'auth_1': None, 12 | 'auth_2': None 13 | } 14 | self.license_request = None 15 | self.license = None 16 | self.service_certificate = None 17 | self.privacy_mode = False 18 | self.keys = [] 19 | -------------------------------------------------------------------------------- /pywidevine/L3/cdm/vmp.py: -------------------------------------------------------------------------------- 1 | try: 2 | from google.protobuf.internal.decoder import _DecodeVarint as _di # this was tested to work with protobuf 3, but it's an internal API (any varint decoder might work) 3 | except ImportError: 4 | # this is generic and does not depend on pb internals, however it will decode "larger" possible numbers than pb decoder which has them fixed 5 | def LEB128_decode(buffer, pos, limit = 64): 6 | result = 0 7 | shift = 0 8 | while True: 9 | b = buffer[pos] 10 | pos += 1 11 | result |= ((b & 0x7F) << shift) 12 | if not (b & 0x80): 13 | return (result, pos) 14 | shift += 7 15 | if shift > limit: 16 | raise Exception("integer too large, shift: {}".format(shift)) 17 | _di = LEB128_decode 18 | 19 | 20 | class FromFileMixin: 21 | @classmethod 22 | def from_file(cls, filename): 23 | """Load given a filename""" 24 | with open(filename,"rb") as f: 25 | return cls(f.read()) 26 | 27 | # the signatures use a format internally similar to 28 | # protobuf's encoding, but without wire types 29 | class VariableReader(FromFileMixin): 30 | """Protobuf-like encoding reader""" 31 | 32 | def __init__(self, buf): 33 | self.buf = buf 34 | self.pos = 0 35 | self.size = len(buf) 36 | 37 | def read_int(self): 38 | """Read a variable length integer""" 39 | # _DecodeVarint will take care of out of range errors 40 | (val, nextpos) = _di(self.buf, self.pos) 41 | self.pos = nextpos 42 | return val 43 | 44 | def read_bytes_raw(self, size): 45 | """Read size bytes""" 46 | b = self.buf[self.pos:self.pos+size] 47 | self.pos += size 48 | return b 49 | 50 | def read_bytes(self): 51 | """Read a bytes object""" 52 | size = self.read_int() 53 | return self.read_bytes_raw(size) 54 | 55 | def is_end(self): 56 | return (self.size == self.pos) 57 | 58 | 59 | class TaggedReader(VariableReader): 60 | """Tagged reader, needed for implementing a WideVine signature reader""" 61 | 62 | def read_tag(self): 63 | """Read a tagged buffer""" 64 | return (self.read_int(), self.read_bytes()) 65 | 66 | def read_all_tags(self, max_tag=3): 67 | tags = {} 68 | while (not self.is_end()): 69 | (tag, bytes) = self.read_tag() 70 | if (tag > max_tag): 71 | raise IndexError("tag out of bound: got {}, max {}".format(tag, max_tag)) 72 | 73 | tags[tag] = bytes 74 | return tags 75 | 76 | class WideVineSignatureReader(FromFileMixin): 77 | """Parses a widevine .sig signature file.""" 78 | 79 | SIGNER_TAG = 1 80 | SIGNATURE_TAG = 2 81 | ISMAINEXE_TAG = 3 82 | 83 | def __init__(self, buf): 84 | reader = TaggedReader(buf) 85 | self.version = reader.read_int() 86 | if (self.version != 0): 87 | raise Exception("Unsupported signature format version {}".format(self.version)) 88 | self.tags = reader.read_all_tags() 89 | 90 | self.signer = self.tags[self.SIGNER_TAG] 91 | self.signature = self.tags[self.SIGNATURE_TAG] 92 | 93 | extra = self.tags[self.ISMAINEXE_TAG] 94 | if (len(extra) != 1 or (extra[0] > 1)): 95 | raise Exception("Unexpected 'ismainexe' field value (not '\\x00' or '\\x01'), please check: {0}".format(extra)) 96 | 97 | self.mainexe = bool(extra[0]) 98 | 99 | @classmethod 100 | def get_tags(cls, filename): 101 | """Return a dictionary of each tag in the signature file""" 102 | return cls.from_file(filename).tags 103 | -------------------------------------------------------------------------------- /pywidevine/L3/decrypt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/pywidevine/L3/decrypt/__init__.py -------------------------------------------------------------------------------- /pywidevine/L3/decrypt/wvdecrypt.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import subprocess 4 | from pywidevine.L3.cdm import cdm, deviceconfig 5 | 6 | class WvDecrypt(object): 7 | 8 | WV_SYSTEM_ID = [237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237] 9 | 10 | def __init__(self, config): 11 | self.config = config 12 | self.logger = logging.getLogger(__name__) 13 | self.wvdecrypt_process = None 14 | 15 | self.logger.debug(self.log_message("wvdecrypt object created")) 16 | self.cdm = cdm.Cdm() 17 | 18 | def check_pssh(pssh_b64): 19 | pssh = base64.b64decode(pssh_b64) 20 | if not pssh[12:28] == bytes(self.WV_SYSTEM_ID): 21 | new_pssh = bytearray([0,0,0]) 22 | new_pssh.append(32+len(pssh)) 23 | new_pssh[4:] = bytearray(b'pssh') 24 | new_pssh[8:] = [0,0,0,0] 25 | new_pssh[13:] = self.WV_SYSTEM_ID 26 | new_pssh[29:] = [0,0,0,0] 27 | new_pssh[31] = len(pssh) 28 | new_pssh[32:] = pssh 29 | return base64.b64encode(new_pssh) 30 | else: 31 | return pssh_b64 32 | 33 | self.session = self.cdm.open_session(check_pssh(config.init_data_b64), 34 | deviceconfig.DeviceConfig(deviceconfig.device_android_generic)) 35 | 36 | self.logger.debug(self.log_message("widevine session opened")) 37 | if self.config.server_cert_required: 38 | self.logger.debug(self.log_message("server cert set")) 39 | self.cdm.set_service_certificate(self.session,config.cert_data_b64) 40 | 41 | 42 | def log_message(self, msg): 43 | return "{}_{} : {}".format(self.config.tracktype, self.config.trackno, msg) 44 | 45 | def start_process(self): 46 | decryption_keys = self.cdm.get_keys(self.session) 47 | if self.config.license: 48 | for key in decryption_keys: 49 | if key.type == 'CONTENT': 50 | self.logger.logkey(self.log_message('{}:{}'.format(key.kid.hex(),key.key.hex()))) 51 | else: 52 | self.logger.debug(self.log_message("starting process")) 53 | self.logger.debug(self.config.build_commandline_list(decryption_keys)) 54 | self.wvdecrypt_process = subprocess.run( 55 | self.config.build_commandline_list(decryption_keys), 56 | check=True 57 | ) 58 | self.logger.debug(self.log_message("decrypted successfully")) 59 | 60 | def get_challenge(self): 61 | return self.cdm.get_license_request(self.session) 62 | 63 | def update_license(self, license_b64): 64 | self.cdm.provide_license(self.session, license_b64) 65 | return True 66 | 67 | -------------------------------------------------------------------------------- /pywidevine/L3/decrypt/wvdecryptconfig.py: -------------------------------------------------------------------------------- 1 | import config as toolcfg 2 | 3 | class WvDecryptConfig(object): 4 | def __init__(self, args, content, filename, tracktype, audio_only, trackno, license_, init_data_b64, cert_data_b64=None): 5 | self.args = args 6 | self.content = content 7 | self.filename = filename 8 | self.tracktype = tracktype 9 | self.audio_only = audio_only 10 | self.trackno = trackno 11 | self.init_data_b64 = init_data_b64 12 | self.license = license_ 13 | if cert_data_b64 is not None: 14 | self.server_cert_required = True 15 | self.cert_data_b64 = cert_data_b64 16 | else: 17 | self.server_cert_required = False 18 | 19 | def get_filename(self, unformatted_filename): 20 | if self.tracktype == 'video': 21 | return unformatted_filename.format(filename=self.filename, track_type='video', track_no='0') 22 | else: 23 | return unformatted_filename.format(filename=self.filename, track_type='audio', track_no='0') 24 | 25 | def build_commandline_list(self, keys): 26 | if self.args.mp4_decrypt: 27 | commandline = [toolcfg.binaries.mp4decrypt] 28 | commandline.append('--show-progress') 29 | for key in keys: 30 | if key.type == 'CONTENT': 31 | commandline.append('--key') 32 | default_KID = 1 33 | commandline.append('{}:{}'.format(str(default_KID), key.key.hex())) 34 | if self.tracktype == 'video': 35 | commandline.append(self.get_filename(toolcfg.filenames.encrypted_filename_video)) 36 | commandline.append(self.get_filename(toolcfg.filenames.decrypted_filename_video)) 37 | elif self.tracktype == 'audio': 38 | out_dec_filename = toolcfg.filenames.decrypted_filename_audio if not self.audio_only \ 39 | else toolcfg.filenames.decrypted_filename_audio_ff 40 | commandline.append(self.get_filename(toolcfg.filenames.encrypted_filename_audio)) 41 | commandline.append(self.get_filename(out_dec_filename)) 42 | else: 43 | commandline = [toolcfg.binaries.shaka] 44 | commandline.append('-quiet') 45 | key_id = '00000000000000000000000000000000' 46 | if self.tracktype == 'video': 47 | commandline.append('in={input},stream={stream},output={output},drm_label={drm_label}'.format( 48 | input=self.get_filename(toolcfg.filenames.encrypted_filename_video), 49 | stream='video', 50 | output=self.get_filename(toolcfg.filenames.decrypted_filename_video), 51 | drm_label='UHD1')) 52 | commandline.append('--enable_raw_key_decryption') 53 | for key in keys: 54 | if key.type == 'CONTENT': 55 | if self.content != 'music-video': 56 | key_id = key.kid.hex() 57 | commandline.append('--keys') 58 | commandline.append('label=UHD1:key_id={kid}:key={key}'.format( 59 | key=key.key.hex(), 60 | kid=key_id)) 61 | 62 | elif self.tracktype == 'audio': 63 | out_dec_filename = toolcfg.filenames.decrypted_filename_audio if not self.audio_only \ 64 | else toolcfg.filenames.decrypted_filename_audio_ff 65 | 66 | commandline.append('in={input},stream={stream},output={output},drm_label={drm_label}'.format( 67 | input=self.get_filename(toolcfg.filenames.encrypted_filename_audio), 68 | stream='audio', 69 | output=self.get_filename(out_dec_filename), 70 | drm_label='HD')) 71 | commandline.append('--enable_raw_key_decryption') 72 | for key in keys: 73 | if key.type == 'CONTENT': 74 | if self.content != 'music-video': 75 | key_id = key.kid.hex() 76 | commandline.append('--keys') 77 | commandline.append('label=HD:key_id={kid}:key={key}'.format( 78 | key=key.key.hex(), kid=key_id)) 79 | #~ print(commandline) 80 | #~ input() 81 | return commandline -------------------------------------------------------------------------------- /pywidevine/L3/decrypt/wvdecryptcustom.py: -------------------------------------------------------------------------------- 1 | # uncompyle6 version 3.7.3 2 | # Python bytecode 3.6 (3379) 3 | # Decompiled from: Python 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)] 4 | # Embedded file name: pywidevine\decrypt\wvdecryptcustom.py 5 | import logging, subprocess, re, base64 6 | from pywidevine.L1.cdm import cdm, deviceconfig 7 | 8 | class WvDecrypt(object): 9 | WV_SYSTEM_ID = [ 10 | 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237] 11 | 12 | def __init__(self, init_data_b64, cert_data_b64, device): 13 | self.init_data_b64 = init_data_b64 14 | self.cert_data_b64 = cert_data_b64 15 | self.device = device 16 | self.cdm = cdm.Cdm() 17 | 18 | def check_pssh(pssh_b64): 19 | pssh = base64.b64decode(pssh_b64) 20 | if not pssh[12:28] == bytes(self.WV_SYSTEM_ID): 21 | new_pssh = bytearray([0, 0, 0]) 22 | new_pssh.append(32 + len(pssh)) 23 | new_pssh[4:] = bytearray(b'pssh') 24 | new_pssh[8:] = [0, 0, 0, 0] 25 | new_pssh[13:] = self.WV_SYSTEM_ID 26 | new_pssh[29:] = [0, 0, 0, 0] 27 | new_pssh[31] = len(pssh) 28 | new_pssh[32:] = pssh 29 | return base64.b64encode(new_pssh) 30 | else: 31 | return pssh_b64 32 | 33 | self.session = self.cdm.open_session(check_pssh(self.init_data_b64), deviceconfig.DeviceConfig(self.device)) 34 | if self.cert_data_b64: 35 | self.cdm.set_service_certificate(self.session, self.cert_data_b64) 36 | 37 | def log_message(self, msg): 38 | return '{}'.format(msg) 39 | 40 | def start_process(self): 41 | keyswvdecrypt = [] 42 | try: 43 | for key in self.cdm.get_keys(self.session): 44 | if key.type == 'CONTENT': 45 | keyswvdecrypt.append(self.log_message('{}:{}'.format(key.kid.hex(), key.key.hex()))) 46 | 47 | except Exception: 48 | return ( 49 | False, keyswvdecrypt) 50 | else: 51 | return ( 52 | True, keyswvdecrypt) 53 | 54 | def get_challenge(self): 55 | return self.cdm.get_license_request(self.session) 56 | 57 | def update_license(self, license_b64): 58 | self.cdm.provide_license(self.session, license_b64) 59 | return True -------------------------------------------------------------------------------- /pywidevine/L3/getPSSH.py: -------------------------------------------------------------------------------- 1 | import requests, xmltodict, json 2 | 3 | def get_pssh(mpd_url): 4 | r = requests.get(url=mpd_url) 5 | r.raise_for_status() 6 | xml = xmltodict.parse(r.text) 7 | mpd = json.loads(json.dumps(xml)) 8 | tracks = mpd['MPD']['Period']['AdaptationSet'] 9 | for video_tracks in tracks: 10 | if video_tracks['@mimeType'] == 'video/mp4': 11 | for t in video_tracks["ContentProtection"]: 12 | if t['@schemeIdUri'].lower() == "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": 13 | pssh = t["pssh"] 14 | return pssh -------------------------------------------------------------------------------- /yt-dlp.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loveyoursupport/AppleMusic-Downloader/661a274d62586b521feec5a7de6bee0e230fdb7d/yt-dlp.exe --------------------------------------------------------------------------------