main
widevinedump 2021-12-23 15:51:08 +05:30
parent aba5a2d349
commit f910bf33ca
83 changed files with 8442 additions and 0 deletions

View File

@ -0,0 +1,8 @@
@@ECHO OFF
set/p URL="URL: "
movie.py --url %URL% -a
pause
@@ECHO OFF

8
GPLAY.bat 100644
View File

@ -0,0 +1,8 @@
@@ECHO OFF
set/p URL="URL: "
movie.py --url %URL%
pause
@@ECHO OFF

4
auth.json 100644
View File

@ -0,0 +1,4 @@
{
"authorization": "ya29.a0ARrdaM9BV33Jclsg1u_i-XYtanOmZ9cY4XYGJgkmzIq1CptxMIl0VS9I25lIYjPoJTs2EjtKVTZo8AHnqQdT7uRoZNiQ3XNyNdLQrPWjMcWmjnGgmFUzbgz0y1oB6ZukYEjayFTJt0x3euOmrXnGdHr-TPJQ",
"x-client-data": "CLC1yQEIkbbJAQiltskBCMS2yQEIqZ3KAQj4x8oBCIrbygEI0JrLAQioncsBCKCgywEIv6DLAQjc8ssBCKjzywE="
}

BIN
binaries/aria2c.exe 100644

Binary file not shown.

BIN
binaries/ffmpeg.exe 100644

Binary file not shown.

Binary file not shown.

Binary file not shown.

257
movie.py 100644
View File

@ -0,0 +1,257 @@
import argparse
import base64
import binascii
import json
import os
import requests
import subprocess
import sys
from colorama import init, Fore
from prettytable import PrettyTable
from pywidevine.decrypt.wvdecrypt import WvDecrypt
init(autoreset=True)
class Main(object):
def __init__(self, folders, args):
self.folders = folders
self.args = args
self.auth_json = None
self.movie_id = args.url.split('id=')[-1]
self.movie_details = None
self.movie_resources = {}
self.mpd_representations = {'video': [], 'audio': [], 'subtitle': []}
self.license = None
def auth(self):
if os.path.exists('auth.json'):
with open('auth.json', 'r') as src:
self.auth_json = json.loads(src.read())
else:
sys.exit()
def requests_headers(self):
return {
'accept': '*/*',
'accept-language': 'en-US,en;q=0.9',
'authorization': 'Bearer {0}'.format(self.auth_json['authorization']),
'Host': 'www.googleapis.com',
'origin': 'chrome-extension://gdijeikdkaembjbdobgfkoidjkpbmlkd',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
'x-client-data': '{0}'.format(self.auth_json['x-client-data']),
}
def get_movie_details(self):
url = 'https://www.googleapis.com/android_video/v1/asset/list?id=yt%3Amovie%3A{0}&if=imbrg&lr=en_US&cr=US&alt=json&access_token={1}&make=Google&model=ChromeCDM-Windows-x86-32&product=generic&device=generic'.format(self.movie_id, self.auth_json['authorization'])
self.movie_details = requests.get(url=url, headers=self.requests_headers()).json()
def get_movie_resources(self):
url = 'https://www.googleapis.com/android_video/v1/mpd?id=yt%3Amovie%3A{0}&ac3=true&all51=true&nd=false&all=false&secure=true&msu=false&ma=true&fc=true&hdcp=true&alt={1}&ssrc=googlevideo&access_token={2}&make=Google&model=ChromeCDM-Windows-x86-32&product=generic&device=generic'
self.movie_resources['json'] = requests.get(
url = url.format(self.movie_id, 'json', self.auth_json['authorization']),
headers = self.requests_headers()
).json()
#self.movie_resources['protojson'] = requests.get(
#url = url.format(self.movie_id, 'protojson', self.auth_json['authorization']),
#headers = self.requests_headers()
#).json()
def parse_movie_resources(self):
av_representations = self.movie_resources['json']['representations']
for x in av_representations:
if 'audio_info' not in x:
self.mpd_representations['video'].append({
'playback_url': x['playback_url'],
'codec': x['codec'],
'init': x['init'],
'bitrate': x['bitrate'],
'quality': str(x['height'])+'p',
'fps': x['video_fps']
})
elif 'audio_info' in x:
self.mpd_representations['audio'].append({
'playback_url': x['playback_url'],
'codec': x['codec'],
'init': x['init'],
'bitrate': x['bitrate'],
'language': x['audio_info']['language']
})
#subtitle_representations = self.movie_resources['protojson']['1007']['4']
#for x in subtitle_representations:
#self.mpd_representations['subtitle'].append({
#'language': x['1'],
#'url': x['3'] ,
#'format': x['5']
#})
def aria2c(self, url, output_file_name):
aria2c = os.path.join(self.folders['binaries'], 'aria2c.exe')
aria2c_command = [
aria2c, url,
'-d', self.folders['temp'], '-j16',
'-o', output_file_name, '-s16', '-x16',
'-U', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
'--allow-overwrite=false',
'--async-dns=false',
'--auto-file-renaming=false',
'--console-log-level=warn',
'--retry-wait=5',
'--summary-interval=0',
]
subprocess.run(aria2c_command)
return os.path.join(self.folders['temp'], output_file_name)
def extract_pssh(self, mp4_file):
mp4dump = os.path.join(self.folders['binaries'], 'mp4dump.exe')
wv_system_id = '[ed ef 8b a9 79 d6 4a ce a3 c8 27 dc d5 1d 21 ed]'
pssh = None
data = subprocess.check_output([mp4dump, '--format', 'json', '--verbosity', '1', mp4_file])
data = json.loads(data)
for atom in data:
if atom['name'] == 'moov':
for child in atom['children']:
if child['name'] == 'pssh' and child['system_id'] == wv_system_id:
pssh = child['data'][1:-1].replace(' ', '')
pssh = binascii.unhexlify(pssh)
pssh = pssh[0:]
pssh = base64.b64encode(pssh).decode('utf-8')
return pssh
def license_request(self, pssh):
license_url = 'https://play.google.com/video/license/GetCencLicense?source=YOUTUBE&video_id={0}&oauth={1}'.format(self.movie_id, self.auth_json['authorization'])
license_headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
}
wvdecrypt = WvDecrypt(pssh)
challenge = wvdecrypt.get_challenge()
resp = requests.post(url=license_url, headers=license_headers, data=challenge)
resp1 = resp.content.split('\r\n\r\n'.encode('utf-8'))
resp2 = resp1[1]
license_b64 = base64.b64encode(resp2).decode('utf-8')
wvdecrypt.update_license(license_b64)
keys = wvdecrypt.start_process()
return keys
def mp4decrypt(self, keys):
mp4decrypt_command = [os.path.join(self.folders['binaries'], 'mp4decrypt.exe')]
for key in keys:
if key.type == 'CONTENT':
mp4decrypt_command.append('--show-progress')
mp4decrypt_command.append('--key')
mp4decrypt_command.append('{}:{}'.format(key.kid.hex(), key.key.hex()))
return mp4decrypt_command
def decrypt(self, keys, input, output):
mp4decrypt_command = self.mp4decrypt(keys)
mp4decrypt_command.append(input)
mp4decrypt_command.append(output)
wvdecrypt_process = subprocess.Popen(mp4decrypt_command)
wvdecrypt_process.communicate()
wvdecrypt_process.wait()
def video(self):
table = PrettyTable()
table.field_names = ['ID', 'CODEC', 'QUALITY', 'BITRATE', 'FPS']
for i, j in enumerate(self.mpd_representations['video']):
table.add_row([i, j['codec'], j['quality'], j['bitrate'], j['fps']])
print('\n' + Fore.RED + 'VIDEO')
print(table)
selected_video = self.mpd_representations['video'][int(input('ID: '))]
init_url = selected_video['playback_url'] + '?range={0}-{1}'.format(selected_video['init']['first'], selected_video['init']['last'])
self.aria2c(init_url, 'init.mp4')
selected_video['pssh'] = self.extract_pssh(os.path.join(self.folders['temp'], 'init.mp4'))
os.remove(os.path.join(self.folders['temp'], 'init.mp4'))
print(Fore.YELLOW+'\nAcquiring Content License')
self.license = self.license_request(selected_video['pssh'])
print(Fore.GREEN+'License Acquired Successfully')
print(Fore.YELLOW+'\nURL:', selected_video['playback_url'])
if not self.args.keys:
output_file_name = self.movie_details['resource'][0]['metadata']['title'] + ' ' + f'[{selected_video["quality"]}] Encrypted.mp4'
print(Fore.YELLOW+'\nDownloading', output_file_name)
video_downloaded = self.aria2c(selected_video['playback_url'], output_file_name.replace(':', ''))
print(Fore.YELLOW+'\nDecrypting Video')
self.decrypt(self.license, video_downloaded, video_downloaded.replace(' Encrypted', ''))
os.remove(video_downloaded)
else:
print(Fore.GREEN + 'n\KEYS')
for key in self.license:
if key.type == 'CONTENT':
print('{}:{}'.format(key.kid.hex(), key.key.hex()))
def audio(self):
table = PrettyTable()
table.field_names = ['ID', 'CODEC', 'BITRATE', 'LANGUAGE']
for i, j in enumerate(self.mpd_representations['audio']):
table.add_row([i, j['codec'], j['bitrate'], j['language']])
print('\n' + Fore.RED +'AUDIO')
print(table)
selected_audio = input('ID: ')
if self.args.audio:
init_url = self.mpd_representations['audio'][int(selected_audio.split(',')[-1])]['playback_url']
init_url += '?range={0}-{1}'.format(self.mpd_representations['audio'][int(selected_audio.split(',')[-1])]['init']['first'], self.mpd_representations['audio'][int(selected_audio.split(',')[-1])]['init']['last'])
self.aria2c(init_url, 'init.mp4')
pssh = self.extract_pssh(os.path.join(self.folders['temp'], 'init.mp4'))
os.remove(os.path.join(self.folders['temp'], 'init.mp4'))
print(Fore.YELLOW+'\nAcquiring Content License')
self.license = self.license_request(pssh)
print(Fore.GREEN+'License Acquired Successfully')
for x in selected_audio.split(','):
x = int(x.strip())
playback_url = self.mpd_representations['audio'][x]['playback_url']
print(Fore.YELLOW+'\nURL:', playback_url)
if not self.args.keys:
output_file_name = self.movie_details['resource'][0]['metadata']['title'] + ' ' + f'[{self.mpd_representations["audio"][x]["language"]}-{self.mpd_representations["audio"][x]["codec"]}-{self.mpd_representations["audio"][x]["bitrate"]}] Encrypted.mp4'
print(Fore.YELLOW+'\nDownloading', output_file_name)
audio_downloaded = self.aria2c(playback_url, output_file_name.replace(':', ''))
self.decrypt(self.license, audio_downloaded, audio_downloaded.replace(' Encrypted', ''))
os.remove(audio_downloaded)
else:
print(Fore.GREEN + 'n\KEYS')
for key in self.license:
if key.type == 'CONTENT':
print('{}:{}'.format(key.kid.hex(), key.key.hex()))
def subtitle(self):
table = PrettyTable()
table.field_names = ['ID', 'LANGUAGE', 'FORMAT']
for i, j in enumerate(self.mpd_representations['subtitle']):
table.add_row([i, j['language'], j['format']])
print('\n' + Fore.RED +'SUBTITLE')
print(table)
selected_subtitle = input('ID: ')
for x in selected_subtitle.split(','):
x = int(x.strip())
url = self.mpd_representations['subtitle'][x]['url']
output_file_name = self.movie_details['resource'][0]['metadata']['title'] + ' ' + f'{self.mpd_representations["subtitle"][x]["language"]}-{self.mpd_representations["subtitle"][x]["format"]}'
print(Fore.YELLOW+'\nDownloading', output_file_name)
self.aria2c(url, output_file_name)
cwd = os.getcwd()
folders = {'binaries': os.path.join(cwd, 'binaries'), 'output': os.path.join(cwd, 'output'), 'temp': os.path.join(cwd, 'temp')}
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('-u', '--url', required=True)
arg_parser.add_argument('-a', '--audio', action='store_true')
arg_parser.add_argument('-k', '--keys', action='store_true')
args = arg_parser.parse_args()
if __name__ == "__main__":
movie = Main(folders, args)
movie.auth()
movie.get_movie_details()
movie.get_movie_resources()
movie.parse_movie_resources()
if not args.audio:
#movie.subtitle()
movie.video()
movie.audio()

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,549 @@
import datetime
import iso8601
import itertools
import re
from pyhls import protocol
ATTRIBUTELISTPATTERN = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''')
def cast_date_time(value):
return iso8601.parse_date(value)
def format_date_time(value):
return value.isoformat()
class ParseError(Exception):
def __init__(self, lineno, line):
self.lineno = lineno
self.line = line
def __str__(self):
return 'Syntax error in manifest on line %d: %s' % (self.lineno, self.line)
def parse_m3u8(content, strict=False, custom_tags_parser=None):
'''
Given a M3U8 playlist content returns a dictionary with all data found
'''
data = {
'media_sequence': 0,
'is_variant': False,
'is_endlist': False,
'is_i_frames_only': False,
'is_independent_segments': False,
'playlist_type': None,
'playlists': [],
'segments': [],
'iframe_playlists': [],
'media': [],
'keys': [],
'rendition_reports': [],
'skip': {},
'part_inf': {},
'session_data': [],
'session_keys': [],
}
state = {
'expect_segment': False,
'expect_playlist': False,
'current_key': None,
'current_segment_map': None,
}
lineno = 0
for line in string_to_lines(content):
lineno += 1
line = line.strip()
if line.startswith(protocol.ext_x_byterange):
_parse_byterange(line, state)
state['expect_segment'] = True
elif line.startswith(protocol.ext_x_targetduration):
_parse_simple_parameter(line, data, float)
elif line.startswith(protocol.ext_x_media_sequence):
_parse_simple_parameter(line, data, int)
elif line.startswith(protocol.ext_x_discontinuity_sequence):
_parse_simple_parameter(line, data, int)
elif line.startswith(protocol.ext_x_program_date_time):
_, program_date_time = _parse_simple_parameter_raw_value(line, cast_date_time)
if not data.get('program_date_time'):
data['program_date_time'] = program_date_time
state['current_program_date_time'] = program_date_time
state['program_date_time'] = program_date_time
elif line.startswith(protocol.ext_x_discontinuity):
state['discontinuity'] = True
elif line.startswith(protocol.ext_x_cue_out_cont):
_parse_cueout_cont(line, state)
state['cue_out'] = True
elif line.startswith(protocol.ext_x_cue_out):
_parse_cueout(line, state, string_to_lines(content)[lineno - 2])
state['cue_out_start'] = True
state['cue_out'] = True
elif line.startswith(protocol.ext_x_cue_in):
state['cue_in'] = True
elif line.startswith(protocol.ext_x_cue_span):
state['cue_out'] = True
elif line.startswith(protocol.ext_x_version):
_parse_simple_parameter(line, data, int)
elif line.startswith(protocol.ext_x_allow_cache):
_parse_simple_parameter(line, data)
elif line.startswith(protocol.ext_x_key):
key = _parse_key(line)
state['current_key'] = key
if key not in data['keys']:
data['keys'].append(key)
elif line.startswith(protocol.extinf):
_parse_extinf(line, data, state, lineno, strict)
state['expect_segment'] = True
elif line.startswith(protocol.ext_x_stream_inf):
state['expect_playlist'] = True
_parse_stream_inf(line, data, state)
elif line.startswith(protocol.ext_x_i_frame_stream_inf):
_parse_i_frame_stream_inf(line, data)
elif line.startswith(protocol.ext_x_media):
_parse_media(line, data, state)
elif line.startswith(protocol.ext_x_playlist_type):
_parse_simple_parameter(line, data)
elif line.startswith(protocol.ext_i_frames_only):
data['is_i_frames_only'] = True
elif line.startswith(protocol.ext_is_independent_segments):
data['is_independent_segments'] = True
elif line.startswith(protocol.ext_x_endlist):
data['is_endlist'] = True
elif line.startswith(protocol.ext_x_map):
quoted_parser = remove_quotes_parser('uri')
segment_map_info = _parse_attribute_list(protocol.ext_x_map, line, quoted_parser)
state['current_segment_map'] = segment_map_info
# left for backward compatibility
data['segment_map'] = segment_map_info
elif line.startswith(protocol.ext_x_start):
attribute_parser = {
"time_offset": lambda x: float(x)
}
start_info = _parse_attribute_list(protocol.ext_x_start, line, attribute_parser)
data['start'] = start_info
elif line.startswith(protocol.ext_x_server_control):
_parse_server_control(line, data, state)
elif line.startswith(protocol.ext_x_part_inf):
_parse_part_inf(line, data, state)
elif line.startswith(protocol.ext_x_rendition_report):
_parse_rendition_report(line, data, state)
elif line.startswith(protocol.ext_x_part):
_parse_part(line, data, state)
elif line.startswith(protocol.ext_x_skip):
_parse_skip(line, data, state)
elif line.startswith(protocol.ext_x_session_data):
_parse_session_data(line, data, state)
elif line.startswith(protocol.ext_x_session_key):
_parse_session_key(line, data, state)
elif line.startswith(protocol.ext_x_preload_hint):
_parse_preload_hint(line, data, state)
elif line.startswith(protocol.ext_x_daterange):
_parse_daterange(line, data, state)
elif line.startswith(protocol.ext_x_gap):
state['gap'] = True
# Comments and whitespace
elif line.startswith('#'):
if callable(custom_tags_parser):
custom_tags_parser(line, data, lineno)
elif line.strip() == '':
# blank lines are legal
pass
elif state['expect_segment']:
_parse_ts_chunk(line, data, state)
state['expect_segment'] = False
elif state['expect_playlist']:
_parse_variant_playlist(line, data, state)
state['expect_playlist'] = False
elif strict:
raise ParseError(lineno, line)
# there could be remaining partial segments
if 'segment' in state:
data['segments'].append(state.pop('segment'))
return data
def _parse_key(line):
params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_key + ':', ''))[1::2]
key = {}
for param in params:
name, value = param.split('=', 1)
key[normalize_attribute(name)] = remove_quotes(value)
return key
def _parse_extinf(line, data, state, lineno, strict):
chunks = line.replace(protocol.extinf + ':', '').split(',', 1)
if len(chunks) == 2:
duration, title = chunks
elif len(chunks) == 1:
if strict:
raise ParseError(lineno, line)
else:
duration = chunks[0]
title = ''
if 'segment' not in state:
state['segment'] = {}
state['segment']['duration'] = float(duration)
state['segment']['title'] = title
def _parse_ts_chunk(line, data, state):
segment = state.pop('segment')
if state.get('program_date_time'):
segment['program_date_time'] = state.pop('program_date_time')
if state.get('current_program_date_time'):
segment['current_program_date_time'] = state['current_program_date_time']
state['current_program_date_time'] += datetime.timedelta(seconds=segment['duration'])
segment['uri'] = line
segment['cue_in'] = state.pop('cue_in', False)
segment['cue_out'] = state.pop('cue_out', False)
segment['cue_out_start'] = state.pop('cue_out_start', False)
if state.get('current_cue_out_scte35'):
segment['scte35'] = state['current_cue_out_scte35']
if state.get('current_cue_out_duration'):
segment['scte35_duration'] = state['current_cue_out_duration']
segment['discontinuity'] = state.pop('discontinuity', False)
if state.get('current_key'):
segment['key'] = state['current_key']
else:
# For unencrypted segments, the initial key would be None
if None not in data['keys']:
data['keys'].append(None)
if state.get('current_segment_map'):
segment['init_section'] = state['current_segment_map']
segment['dateranges'] = state.pop('dateranges', None)
segment['gap_tag'] = state.pop('gap', None)
data['segments'].append(segment)
def _parse_attribute_list(prefix, line, atribute_parser):
params = ATTRIBUTELISTPATTERN.split(line.replace(prefix + ':', ''))[1::2]
attributes = {}
for param in params:
name, value = param.split('=', 1)
name = normalize_attribute(name)
if name in atribute_parser:
value = atribute_parser[name](value)
attributes[name] = value
return attributes
def _parse_stream_inf(line, data, state):
data['is_variant'] = True
data['media_sequence'] = None
atribute_parser = remove_quotes_parser('codecs', 'audio', 'video', 'subtitles', 'closed_captions')
atribute_parser["program_id"] = int
atribute_parser["bandwidth"] = lambda x: int(float(x))
atribute_parser["average_bandwidth"] = int
atribute_parser["frame_rate"] = float
atribute_parser["video_range"] = str
state['stream_info'] = _parse_attribute_list(protocol.ext_x_stream_inf, line, atribute_parser)
def _parse_i_frame_stream_inf(line, data):
atribute_parser = remove_quotes_parser('codecs', 'uri')
atribute_parser["program_id"] = int
atribute_parser["bandwidth"] = int
atribute_parser["average_bandwidth"] = int
atribute_parser["video_range"] = str
iframe_stream_info = _parse_attribute_list(protocol.ext_x_i_frame_stream_inf, line, atribute_parser)
iframe_playlist = {'uri': iframe_stream_info.pop('uri'),
'iframe_stream_info': iframe_stream_info}
data['iframe_playlists'].append(iframe_playlist)
def _parse_media(line, data, state):
quoted = remove_quotes_parser('uri', 'group_id', 'language', 'assoc_language', 'name', 'instream_id', 'characteristics', 'channels')
media = _parse_attribute_list(protocol.ext_x_media, line, quoted)
data['media'].append(media)
def _parse_variant_playlist(line, data, state):
playlist = {'uri': line,
'stream_info': state.pop('stream_info')}
data['playlists'].append(playlist)
def _parse_byterange(line, state):
if 'segment' not in state:
state['segment'] = {}
state['segment']['byterange'] = line.replace(protocol.ext_x_byterange + ':', '')
def _parse_simple_parameter_raw_value(line, cast_to=str, normalize=False):
param, value = line.split(':', 1)
param = normalize_attribute(param.replace('#EXT-X-', ''))
if normalize:
value = value.strip().lower()
return param, cast_to(value)
def _parse_and_set_simple_parameter_raw_value(line, data, cast_to=str, normalize=False):
param, value = _parse_simple_parameter_raw_value(line, cast_to, normalize)
data[param] = value
return data[param]
def _parse_simple_parameter(line, data, cast_to=str):
return _parse_and_set_simple_parameter_raw_value(line, data, cast_to, True)
def _parse_cueout_cont(line, state):
param, value = line.split(':', 1)
res = re.match('.*Duration=(.*),SCTE35=(.*)$', value)
if res:
state['current_cue_out_duration'] = res.group(1)
state['current_cue_out_scte35'] = res.group(2)
def _cueout_no_duration(line):
# this needs to be called first since line.split in all other
# parsers will throw a ValueError if passed just this tag
if line == protocol.ext_x_cue_out:
return (None, None)
def _cueout_elemental(line, state, prevline):
param, value = line.split(':', 1)
res = re.match('.*EXT-OATCLS-SCTE35:(.*)$', prevline)
if res:
return (res.group(1), value)
else:
return None
def _cueout_envivio(line, state, prevline):
param, value = line.split(':', 1)
res = re.match('.*DURATION=(.*),.*,CUE="(.*)"', value)
if res:
return (res.group(2), res.group(1))
else:
return None
def _cueout_duration(line):
# this needs to be called after _cueout_elemental
# as it would capture those cues incompletely
# This was added seperately rather than modifying "simple"
param, value = line.split(':', 1)
res = re.match(r'DURATION=(.*)', value)
if res:
return (None, res.group(1))
def _cueout_simple(line):
# this needs to be called after _cueout_elemental
# as it would capture those cues incompletely
param, value = line.split(':', 1)
res = re.match(r'^(\d+(?:\.\d)?\d*)$', value)
if res:
return (None, res.group(1))
def _parse_cueout(line, state, prevline):
_cueout_state = (_cueout_no_duration(line)
or _cueout_elemental(line, state, prevline)
or _cueout_envivio(line, state, prevline)
or _cueout_duration(line)
or _cueout_simple(line))
if _cueout_state:
state['current_cue_out_scte35'] = _cueout_state[0]
state['current_cue_out_duration'] = _cueout_state[1]
def _parse_server_control(line, data, state):
attribute_parser = {
"can_block_reload": str,
"hold_back": lambda x: float(x),
"part_hold_back": lambda x: float(x),
"can_skip_until": lambda x: float(x)
}
data['server_control'] = _parse_attribute_list(
protocol.ext_x_server_control, line, attribute_parser
)
def _parse_part_inf(line, data, state):
attribute_parser = {
"part_target": lambda x: float(x)
}
data['part_inf'] = _parse_attribute_list(
protocol.ext_x_part_inf, line, attribute_parser
)
def _parse_rendition_report(line, data, state):
attribute_parser = remove_quotes_parser('uri')
attribute_parser['last_msn'] = int
attribute_parser['last_part'] = int
rendition_report = _parse_attribute_list(
protocol.ext_x_rendition_report, line, attribute_parser
)
data['rendition_reports'].append(rendition_report)
def _parse_part(line, data, state):
attribute_parser = remove_quotes_parser('uri')
attribute_parser['duration'] = lambda x: float(x)
attribute_parser['independent'] = str
attribute_parser['gap'] = str
attribute_parser['byterange'] = str
part = _parse_attribute_list(protocol.ext_x_part, line, attribute_parser)
# this should always be true according to spec
if state.get('current_program_date_time'):
part['program_date_time'] = state['current_program_date_time']
state['current_program_date_time'] += datetime.timedelta(seconds=part['duration'])
part['dateranges'] = state.pop('dateranges', None)
part['gap_tag'] = state.pop('gap', None)
if 'segment' not in state:
state['segment'] = {}
segment = state['segment']
if 'parts' not in segment:
segment['parts'] = []
segment['parts'].append(part)
def _parse_skip(line, data, state):
attribute_parser = {
"skipped_segments": int
}
data['skip'] = _parse_attribute_list(protocol.ext_x_skip, line, attribute_parser)
def _parse_session_data(line, data, state):
quoted = remove_quotes_parser('data_id', 'value', 'uri', 'language')
session_data = _parse_attribute_list(protocol.ext_x_session_data, line, quoted)
data['session_data'].append(session_data)
def _parse_session_key(line, data, state):
params = ATTRIBUTELISTPATTERN.split(line.replace(protocol.ext_x_session_key + ':', ''))[1::2]
key = {}
for param in params:
name, value = param.split('=', 1)
key[normalize_attribute(name)] = remove_quotes(value)
data['session_keys'].append(key)
def _parse_preload_hint(line, data, state):
attribute_parser = remove_quotes_parser('uri')
attribute_parser['type'] = str
attribute_parser['byterange_start'] = int
attribute_parser['byterange_length'] = int
data['preload_hint'] = _parse_attribute_list(
protocol.ext_x_preload_hint, line, attribute_parser
)
def _parse_daterange(line, date, state):
attribute_parser = remove_quotes_parser('id', 'class', 'start_date', 'end_date')
attribute_parser['duration'] = float
attribute_parser['planned_duration'] = float
attribute_parser['end_on_next'] = str
attribute_parser['scte35_cmd'] = str
attribute_parser['scte35_out'] = str
attribute_parser['scte35_in'] = str
parsed = _parse_attribute_list(
protocol.ext_x_daterange, line, attribute_parser
)
if 'dateranges' not in state:
state['dateranges'] = []
state['dateranges'].append(parsed)
def string_to_lines(string):
return string.strip().splitlines()
def remove_quotes_parser(*attrs):
return dict(zip(attrs, itertools.repeat(remove_quotes)))
def remove_quotes(string):
'''
Remove quotes from string.
Ex.:
"foo" -> foo
'foo' -> foo
'foo -> 'foo
'''
quotes = ('"', "'")
if string.startswith(quotes) and string.endswith(quotes):
return string[1:-1]
return string
def normalize_attribute(attribute):
return attribute.replace('-', '_').lower().strip()
def is_url(uri):
return uri.startswith(('https://', 'http://'))

34
pyhls/protocol.py 100644
View File

@ -0,0 +1,34 @@
ext_x_targetduration = '#EXT-X-TARGETDURATION'
ext_x_media_sequence = '#EXT-X-MEDIA-SEQUENCE'
ext_x_discontinuity_sequence = '#EXT-X-DISCONTINUITY-SEQUENCE'
ext_x_program_date_time = '#EXT-X-PROGRAM-DATE-TIME'
ext_x_media = '#EXT-X-MEDIA'
ext_x_playlist_type = '#EXT-X-PLAYLIST-TYPE'
ext_x_key = '#EXT-X-KEY'
ext_x_stream_inf = '#EXT-X-STREAM-INF'
ext_x_version = '#EXT-X-VERSION'
ext_x_allow_cache = '#EXT-X-ALLOW-CACHE'
ext_x_endlist = '#EXT-X-ENDLIST'
extinf = '#EXTINF'
ext_i_frames_only = '#EXT-X-I-FRAMES-ONLY'
ext_x_byterange = '#EXT-X-BYTERANGE'
ext_x_i_frame_stream_inf = '#EXT-X-I-FRAME-STREAM-INF'
ext_x_discontinuity = '#EXT-X-DISCONTINUITY'
ext_x_cue_out = '#EXT-X-CUE-OUT'
ext_x_cue_out_cont = '#EXT-X-CUE-OUT-CONT'
ext_x_cue_in = '#EXT-X-CUE-IN'
ext_x_cue_span = '#EXT-X-CUE-SPAN'
ext_x_scte35 = '#EXT-OATCLS-SCTE35'
ext_is_independent_segments = '#EXT-X-INDEPENDENT-SEGMENTS'
ext_x_map = '#EXT-X-MAP'
ext_x_start = '#EXT-X-START'
ext_x_server_control = '#EXT-X-SERVER-CONTROL'
ext_x_part_inf = '#EXT-X-PART-INF'
ext_x_part = '#EXT-X-PART'
ext_x_rendition_report = '#EXT-X-RENDITION-REPORT'
ext_x_skip = '#EXT-X-SKIP'
ext_x_session_data = '#EXT-X-SESSION-DATA'
ext_x_session_key = '#EXT-X-SESSION-KEY'
ext_x_preload_hint = '#EXT-X-PRELOAD-HINT'
ext_x_daterange = "#EXT-X-DATERANGE"
ext_x_gap = "#EXT-X-GAP"

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,362 @@
import base64
import os
import time
import binascii
from google.protobuf.message import DecodeError
from google.protobuf import text_format
from pywidevine.cdm.formats import wv_proto2_pb2 as wv_proto2
from pywidevine.cdm.session import Session
from pywidevine.cdm.key import Key
from Cryptodome.Random import get_random_bytes
from Cryptodome.Random import random
from Cryptodome.Cipher import PKCS1_OAEP, AES
from Cryptodome.Hash import CMAC, SHA256, HMAC, SHA1
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import pss
from Cryptodome.Util import Padding
import logging
class Cdm:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.sessions = {}
def open_session(self, init_data_b64, device, raw_init_data = None, offline=False):
self.logger.debug("open_session(init_data_b64={}, device={}".format(init_data_b64, device))
self.logger.info("opening new cdm session")
if device.session_id_type == 'android':
# format: 16 random hexdigits, 2 digit counter, 14 0s
rand_ascii = ''.join(random.choice('ABCDEF0123456789') for _ in range(16))
counter = '01' # this resets regularly so its fine to use 01
rest = '00000000000000'
session_id = rand_ascii + counter + rest
session_id = session_id.encode('ascii')
elif device.session_id_type == 'chrome':
rand_bytes = get_random_bytes(16)
session_id = rand_bytes
else:
# other formats NYI
self.logger.error("device type is unusable")
return 1
if raw_init_data and isinstance(raw_init_data, (bytes, bytearray)):
# used for NF key exchange, where they don't provide a valid PSSH
init_data = raw_init_data
self.raw_pssh = True
else:
init_data = self._parse_init_data(init_data_b64)
self.raw_pssh = False
if init_data:
new_session = Session(session_id, init_data, device, offline)
else:
self.logger.error("unable to parse init data")
return 1
self.sessions[session_id] = new_session
self.logger.info("session opened and init data parsed successfully")
return session_id
def _parse_init_data(self, init_data_b64):
parsed_init_data = wv_proto2.WidevineCencHeader()
try:
self.logger.debug("trying to parse init_data directly")
parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
self.logger.debug("unable to parse as-is, trying with removed pssh box header")
try:
id_bytes = parsed_init_data.ParseFromString(base64.b64decode(init_data_b64)[32:])
except DecodeError:
self.logger.error("unable to parse, unsupported init data format")
return None
self.logger.debug("init_data:")
for line in text_format.MessageToString(parsed_init_data).splitlines():
self.logger.debug(line)
return parsed_init_data
def close_session(self, session_id):
self.logger.debug("close_session(session_id={})".format(session_id))
self.logger.info("closing cdm session")
if session_id in self.sessions:
self.sessions.pop(session_id)
self.logger.info("cdm session closed")
return 0
else:
self.logger.info("session {} not found".format(session_id))
return 1
def set_service_certificate(self, session_id, cert_b64):
self.logger.debug("set_service_certificate(session_id={}, cert={})".format(session_id, cert_b64))
self.logger.info("setting service certificate")
if session_id not in self.sessions:
self.logger.error("session id doesn't exist")
return 1
session = self.sessions[session_id]
message = wv_proto2.SignedMessage()
try:
message.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
self.logger.error("failed to parse cert as SignedMessage")
service_certificate = wv_proto2.SignedDeviceCertificate()
if message.Type:
self.logger.debug("service cert provided as signedmessage")
try:
service_certificate.ParseFromString(message.Msg)
except DecodeError:
self.logger.error("failed to parse service certificate")
return 1
else:
self.logger.debug("service cert provided as signeddevicecertificate")
try:
service_certificate.ParseFromString(base64.b64decode(cert_b64))
except DecodeError:
self.logger.error("failed to parse service certificate")
return 1
self.logger.debug("service certificate:")
for line in text_format.MessageToString(service_certificate).splitlines():
self.logger.debug(line)
session.service_certificate = service_certificate
session.privacy_mode = True
return 0
def get_license_request(self, session_id):
self.logger.debug("get_license_request(session_id={})".format(session_id))
self.logger.info("getting license request")
if session_id not in self.sessions:
self.logger.error("session ID does not exist")
return 1
session = self.sessions[session_id]
# raw pssh will be treated as bytes and not parsed
if self.raw_pssh:
license_request = wv_proto2.SignedLicenseRequestRaw()
else:
license_request = wv_proto2.SignedLicenseRequest()
client_id = wv_proto2.ClientIdentification()
if not os.path.exists(session.device_config.device_client_id_blob_filename):
self.logger.error("no client ID blob available for this device")
return 1
with open(session.device_config.device_client_id_blob_filename, "rb") as f:
try:
cid_bytes = client_id.ParseFromString(f.read())
except DecodeError:
self.logger.error("client id failed to parse as protobuf")
return 1
self.logger.debug("building license request")
if not self.raw_pssh:
license_request.Type = wv_proto2.SignedLicenseRequest.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh.CopyFrom(session.init_data)
else:
license_request.Type = wv_proto2.SignedLicenseRequestRaw.MessageType.Value('LICENSE_REQUEST')
license_request.Msg.ContentId.CencId.Pssh = session.init_data # bytes
if session.offline:
license_type = wv_proto2.LicenseType.Value('OFFLINE')
else:
license_type = wv_proto2.LicenseType.Value('DEFAULT')
license_request.Msg.ContentId.CencId.LicenseType = license_type
license_request.Msg.ContentId.CencId.RequestId = session_id
license_request.Msg.Type = wv_proto2.LicenseRequest.RequestType.Value('NEW')
license_request.Msg.RequestTime = int(time.time())
license_request.Msg.ProtocolVersion = wv_proto2.ProtocolVersion.Value('CURRENT')
if session.device_config.send_key_control_nonce:
license_request.Msg.KeyControlNonce = random.randrange(1, 2**31)
if session.privacy_mode:
if session.device_config.vmp:
self.logger.debug("vmp required, adding to client_id")
self.logger.debug("reading vmp hashes")
vmp_hashes = wv_proto2.FileHashes()
with open(session.device_config.device_vmp_blob_filename, "rb") as f:
try:
vmp_bytes = vmp_hashes.ParseFromString(f.read())
except DecodeError:
self.logger.error("vmp hashes failed to parse as protobuf")
return 1
client_id._FileHashes.CopyFrom(vmp_hashes)
self.logger.debug("privacy mode & service certificate loaded, encrypting client id")
self.logger.debug("unencrypted client id:")
for line in text_format.MessageToString(client_id).splitlines():
self.logger.debug(line)
cid_aes_key = get_random_bytes(16)
cid_iv = get_random_bytes(16)
cid_cipher = AES.new(cid_aes_key, AES.MODE_CBC, cid_iv)
encrypted_client_id = cid_cipher.encrypt(Padding.pad(client_id.SerializeToString(), 16))
service_public_key = RSA.importKey(session.service_certificate._DeviceCertificate.PublicKey)
service_cipher = PKCS1_OAEP.new(service_public_key)
encrypted_cid_key = service_cipher.encrypt(cid_aes_key)
encrypted_client_id_proto = wv_proto2.EncryptedClientIdentification()
encrypted_client_id_proto.ServiceId = session.service_certificate._DeviceCertificate.ServiceId
encrypted_client_id_proto.ServiceCertificateSerialNumber = session.service_certificate._DeviceCertificate.SerialNumber
encrypted_client_id_proto.EncryptedClientId = encrypted_client_id
encrypted_client_id_proto.EncryptedClientIdIv = cid_iv
encrypted_client_id_proto.EncryptedPrivacyKey = encrypted_cid_key
license_request.Msg.EncryptedClientId.CopyFrom(encrypted_client_id_proto)
else:
license_request.Msg.ClientId.CopyFrom(client_id)
if session.device_config.private_key_available:
key = RSA.importKey(open(session.device_config.device_private_key_filename).read())
session.device_key = key
else:
self.logger.error("need device private key, other methods unimplemented")
return 1
self.logger.debug("signing license request")
hash = SHA1.new(license_request.Msg.SerializeToString())
signature = pss.new(key).sign(hash)
license_request.Signature = signature
session.license_request = license_request
self.logger.debug("license request:")
for line in text_format.MessageToString(session.license_request).splitlines():
self.logger.debug(line)
self.logger.info("license request created")
self.logger.debug("license request b64: {}".format(base64.b64encode(license_request.SerializeToString())))
return license_request.SerializeToString()
def provide_license(self, session_id, license_b64):
self.logger.debug("provide_license(session_id={}, license_b64={})".format(session_id, license_b64))
self.logger.info("decrypting provided license")
if session_id not in self.sessions:
self.logger.error("session does not exist")
return 1
session = self.sessions[session_id]
if not session.license_request:
self.logger.error("generate a license request first!")
return 1
license = wv_proto2.SignedLicense()
try:
license.ParseFromString(base64.b64decode(license_b64))
except DecodeError:
self.logger.error("unable to parse license - check protobufs")
return 1
session.license = license
self.logger.debug("license:")
for line in text_format.MessageToString(license).splitlines():
self.logger.debug(line)
self.logger.debug("deriving keys from session key")
oaep_cipher = PKCS1_OAEP.new(session.device_key)
session.session_key = oaep_cipher.decrypt(license.SessionKey)
lic_req_msg = session.license_request.Msg.SerializeToString()
enc_key_base = b"ENCRYPTION\000" + lic_req_msg + b"\0\0\0\x80"
auth_key_base = b"AUTHENTICATION\0" + lic_req_msg + b"\0\0\2\0"
enc_key = b"\x01" + enc_key_base
auth_key_1 = b"\x01" + auth_key_base
auth_key_2 = b"\x02" + auth_key_base
auth_key_3 = b"\x03" + auth_key_base
auth_key_4 = b"\x04" + auth_key_base
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(enc_key)
enc_cmac_key = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_1)
auth_cmac_key_1 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_2)
auth_cmac_key_2 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_3)
auth_cmac_key_3 = cmac_obj.digest()
cmac_obj = CMAC.new(session.session_key, ciphermod=AES)
cmac_obj.update(auth_key_4)
auth_cmac_key_4 = cmac_obj.digest()
auth_cmac_combined_1 = auth_cmac_key_1 + auth_cmac_key_2
auth_cmac_combined_2 = auth_cmac_key_3 + auth_cmac_key_4
session.derived_keys['enc'] = enc_cmac_key
session.derived_keys['auth_1'] = auth_cmac_combined_1
session.derived_keys['auth_2'] = auth_cmac_combined_2
self.logger.debug('verifying license signature')
lic_hmac = HMAC.new(session.derived_keys['auth_1'], digestmod=SHA256)
lic_hmac.update(license.Msg.SerializeToString())
self.logger.debug("calculated sig: {} actual sig: {}".format(lic_hmac.hexdigest(), binascii.hexlify(license.Signature)))
if lic_hmac.digest() != license.Signature:
self.logger.info("license signature doesn't match - writing bin so they can be debugged")
with open("original_lic.bin", "wb") as f:
f.write(base64.b64decode(license_b64))
with open("parsed_lic.bin", "wb") as f:
f.write(license.SerializeToString())
self.logger.info("continuing anyway")
self.logger.debug("key count: {}".format(len(license.Msg.Key)))
for key in license.Msg.Key:
if key.Id:
key_id = key.Id
else:
key_id = wv_proto2.License.KeyContainer.KeyType.Name(key.Type).encode('utf-8')
encrypted_key = key.Key
iv = key.Iv
type = wv_proto2.License.KeyContainer.KeyType.Name(key.Type)
cipher = AES.new(session.derived_keys['enc'], AES.MODE_CBC, iv=iv)
decrypted_key = cipher.decrypt(encrypted_key)
if type == "OPERATOR_SESSION":
permissions = []
perms = key._OperatorSessionKeyPermissions
for (descriptor, value) in perms.ListFields():
if value == 1:
permissions.append(descriptor.name)
print(permissions)
else:
permissions = []
session.keys.append(Key(key_id, type, Padding.unpad(decrypted_key, 16), permissions))
self.logger.info("decrypted all keys")
return 0
def get_keys(self, session_id):
if session_id in self.sessions:
return self.sessions[session_id].keys
else:
self.logger.error("session not found")
return 1

View File

@ -0,0 +1,64 @@
import os
device_chromecdm_1610 = {
'name': 'chromecdm_1610',
'description': 'chrome cdm windows 1610',
'security_level': 3,
'session_id_type': 'chrome',
'private_key_available': True,
'vmp': True,
'send_key_control_nonce': False
}
device_space_l3 = {
'name': 'space_l3',
'description': 'Spacecastgfsc100',
'security_level': 3,
'session_id_type': 'android',
'private_key_available': True,
'vmp': False,
'send_key_control_nonce': True
}
devices_available = [device_chromecdm_1610]
FILES_FOLDER = 'devices'
class DeviceConfig:
def __init__(self, device):
self.device_name = device['name']
self.description = device['description']
self.security_level = device['security_level']
self.session_id_type = device['session_id_type']
self.private_key_available = device['private_key_available']
self.vmp = device['vmp']
self.send_key_control_nonce = device['send_key_control_nonce']
if 'keybox_filename' in device:
self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['keybox_filename'])
else:
self.keybox_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'keybox')
if 'device_cert_filename' in device:
self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_cert_filename'])
else:
self.device_cert_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_cert')
if 'device_private_key_filename' in device:
self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_private_key_filename'])
else:
self.device_private_key_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_private_key')
if 'device_client_id_blob_filename' in device:
self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_client_id_blob_filename'])
else:
self.device_client_id_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_client_id_blob')
if 'device_vmp_blob_filename' in device:
self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], device['device_vmp_blob_filename'])
else:
self.device_vmp_blob_filename = os.path.join(os.path.dirname(__file__), FILES_FOLDER, device['name'], 'device_vmp_blob')
def __repr__(self):
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)

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmWQki2QefKW8qIs4jUCmWLU+cOJkEt4Kh/5J1cer56hZU0da
IDxVVEDkFJ/LoxgZF5bZJP7r2nMhlZco9e8WNF+dSMgIALd9vIwlhXoiNSxOVNMT
VSopBO8Tm1TdhRpHuGI14WrEmhDrL26nSC7upAa3GS94Il8JIq/DdF0hierqw9Zo
TgKIuYN1iTVmV98J2mRGCSMVE5YnDYUiW0YLyVsdDDHQ2ckIG8GLHiIz7FzAfylO
tuNKMC4IxqIoV2J5sTuZEbiE1Vq7ZpaUbXikX+dAkVKnRMuGhPyONURIvSHu6N5P
g542lkc6WlKOLnnh9zC+bILOQ8+AwaG9mkAa7QIDAQABAoIBAAlJ+feSANGiFMZt
JefR7DDviuA5qX9GAIPDR7PhQ10EQiKLrd7JYI55XoaMNcAC79QQn8ZAhMZmFfjR
3lpkV+ckiikT6f4nHn9qSkRmxMcND5JN9PwBkVnZ44lSzpZmpMS699HWjiDZWPC2
pf02UF/RA0oMaJ1GPY6i77ZuhF8uaXQmXeBZL1V6inn4+OmS+X2MZ87hT4r49ZF2
Ux7MMup6koVtE8iy4LZHYmxP+FwnxUfpbQWnPsWeWjPv/43c2PRKQn9KOSzwwW2v
nXIIa6jst7SJefMcrfcs7vFYbML0Wu1nmkNrbguCU5tpidzJTkyM0Vfg8dcpYIFO
BM0cquECgYEAzbfkN/b35/J5G8MLMgWel/NPgayrxywwUoKmPPhEe7BJy41xOhsP
cmEWcmdchAvce3RFPlcmzDStcTCtJeXHDjhCXv700q1F8JxcS1madv1aRFGQk15M
wWPBGIiwRVVBoU/Owt/jGn5f800INAg39tYn78wRZ+LCBkgxxXWOF+ECgYEAvuIP
9Kj1Iu+W19idxUZXiH5hvuBFrGqIvYkm8Q3XHyPbT/q49r3HeaCqwtBzoHwTfOhF
a7j58/wr7RVKfnIIYPc1xwsfY/ecZ2znX5BWPSqJdiUjdqJSg+z5dun6dGyod2l0
dHcdLJP8K1kjPrr+Iq0dAPWaElb528RPDKGLdI0CgYA2YFQvwtUWd04x22VbLIcW
LRcn9KdXN0Pym1wro4gelaN77YAvVrXHiwgu50laEfSOoVYoO0sjEQ2DbNVnvgvW
o2JPz96QzYI+LmZq+F1O9HrmshSgD34EZETHImJNgVKevASwGBPkjeD447S2ZDG+
yi62QN+c8SBOHskhI4iSoQKBgQCrC5tMm1H+mj7K7/qAWgX583XXOtR0KYqafJn+
i25nIxRU2NCBmZFztbsOkwfpmQVFekUqwDiKnz8lVVzJbZmAekUgNSFNzQXDz9yM
z6PXb5R539GlbtMOEH2CRyv8w5k6V67Y5huoZHskxN1GSv2LSSCiSXJkWLfQbFqB
cQiryQKBgDNhLhI/p7mkl7DnRGRstoWWUdUi8u02OchctTpnVPs5v9oYTfLrq/k5
K1faq00p/FAgKVRV6KavD6TvhU/JeYUbrQIvIGFCMbKDMy7AUNlWjiFPAkKE7Pdz
TO7fnFba0NkZjHd9R1Ljs2Mm3Lzo8rjdFzfFor9YdfGr9qVDkOOp
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAmWQki2QefKW8qIs4jUCmWLU+cOJkEt4Kh/5J1cer56hZU0da
IDxVVEDkFJ/LoxgZF5bZJP7r2nMhlZco9e8WNF+dSMgIALd9vIwlhXoiNSxOVNMT
VSopBO8Tm1TdhRpHuGI14WrEmhDrL26nSC7upAa3GS94Il8JIq/DdF0hierqw9Zo
TgKIuYN1iTVmV98J2mRGCSMVE5YnDYUiW0YLyVsdDDHQ2ckIG8GLHiIz7FzAfylO
tuNKMC4IxqIoV2J5sTuZEbiE1Vq7ZpaUbXikX+dAkVKnRMuGhPyONURIvSHu6N5P
g542lkc6WlKOLnnh9zC+bILOQ8+AwaG9mkAa7QIDAQABAoIBAAlJ+feSANGiFMZt
JefR7DDviuA5qX9GAIPDR7PhQ10EQiKLrd7JYI55XoaMNcAC79QQn8ZAhMZmFfjR
3lpkV+ckiikT6f4nHn9qSkRmxMcND5JN9PwBkVnZ44lSzpZmpMS699HWjiDZWPC2
pf02UF/RA0oMaJ1GPY6i77ZuhF8uaXQmXeBZL1V6inn4+OmS+X2MZ87hT4r49ZF2
Ux7MMup6koVtE8iy4LZHYmxP+FwnxUfpbQWnPsWeWjPv/43c2PRKQn9KOSzwwW2v
nXIIa6jst7SJefMcrfcs7vFYbML0Wu1nmkNrbguCU5tpidzJTkyM0Vfg8dcpYIFO
BM0cquECgYEAzbfkN/b35/J5G8MLMgWel/NPgayrxywwUoKmPPhEe7BJy41xOhsP
cmEWcmdchAvce3RFPlcmzDStcTCtJeXHDjhCXv700q1F8JxcS1madv1aRFGQk15M
wWPBGIiwRVVBoU/Owt/jGn5f800INAg39tYn78wRZ+LCBkgxxXWOF+ECgYEAvuIP
9Kj1Iu+W19idxUZXiH5hvuBFrGqIvYkm8Q3XHyPbT/q49r3HeaCqwtBzoHwTfOhF
a7j58/wr7RVKfnIIYPc1xwsfY/ecZ2znX5BWPSqJdiUjdqJSg+z5dun6dGyod2l0
dHcdLJP8K1kjPrr+Iq0dAPWaElb528RPDKGLdI0CgYA2YFQvwtUWd04x22VbLIcW
LRcn9KdXN0Pym1wro4gelaN77YAvVrXHiwgu50laEfSOoVYoO0sjEQ2DbNVnvgvW
o2JPz96QzYI+LmZq+F1O9HrmshSgD34EZETHImJNgVKevASwGBPkjeD447S2ZDG+
yi62QN+c8SBOHskhI4iSoQKBgQCrC5tMm1H+mj7K7/qAWgX583XXOtR0KYqafJn+
i25nIxRU2NCBmZFztbsOkwfpmQVFekUqwDiKnz8lVVzJbZmAekUgNSFNzQXDz9yM
z6PXb5R539GlbtMOEH2CRyv8w5k6V67Y5huoZHskxN1GSv2LSSCiSXJkWLfQbFqB
cQiryQKBgDNhLhI/p7mkl7DnRGRstoWWUdUi8u02OchctTpnVPs5v9oYTfLrq/k5
K1faq00p/FAgKVRV6KavD6TvhU/JeYUbrQIvIGFCMbKDMy7AUNlWjiFPAkKE7Pdz
TO7fnFba0NkZjHd9R1Ljs2Mm3Lzo8rjdFzfFor9YdfGr9qVDkOOp
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,466 @@
syntax = "proto2";
// from x86 (partial), most of it from the ARM version:
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
DEVICE_CERTIFICATE = 1;
REMOTE_ATTESTATION_CERTIFICATE = 2;
}
message NameValue {
required string Name = 1;
required string Value = 2;
}
message ClientCapabilities {
enum HdcpVersion {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
HDCP_V2_1 = 3;
HDCP_V2_2 = 4;
}
optional uint32 ClientToken = 1;
optional uint32 SessionToken = 2;
optional uint32 VideoResolutionConstraints = 3;
optional HdcpVersion MaxHdcpVersion = 4;
optional uint32 OemCryptoApiVersion = 5;
}
required TokenType Type = 1;
//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:
optional SignedDeviceCertificate Token = 2; // use this when parsing, "bytes" when building a client id blob
repeated NameValue ClientInfo = 3;
optional bytes ProviderClientToken = 4;
optional uint32 LicenseCounter = 5;
optional ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
optional FileHashes _FileHashes = 7; // vmp blob goes here
}
message DeviceCertificate {
enum CertificateType {
ROOT = 0;
INTERMEDIATE = 1;
USER_DEVICE = 2;
SERVICE = 3;
}
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)
optional bytes SerialNumber = 2;
optional uint32 CreationTimeSeconds = 3;
optional bytes PublicKey = 4;
optional uint32 SystemId = 5;
optional uint32 TestDeviceDeprecated = 6; // is it bool or int?
optional bytes ServiceId = 7; // service URL for service certificates
}
// missing some references,
message DeviceCertificateStatus {
enum CertificateStatus {
VALID = 0;
REVOKED = 1;
}
optional bytes SerialNumber = 1;
optional CertificateStatus Status = 2;
optional ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
}
message DeviceCertificateStatusList {
optional uint32 CreationTimeSeconds = 1;
repeated DeviceCertificateStatus CertificateStatus = 2;
}
message EncryptedClientIdentification {
required string ServiceId = 1;
optional bytes ServiceCertificateSerialNumber = 2;
required bytes EncryptedClientId = 3;
required bytes EncryptedClientIdIv = 4;
required bytes EncryptedPrivacyKey = 5;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
enum LicenseType {
ZERO = 0;
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
OFFLINE = 2;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
// this is just a guess because these globals got lost, but really, do we need more?
enum ProtocolVersion {
CURRENT = 21; // don't have symbols for this
}
message LicenseIdentification {
optional bytes RequestId = 1;
optional bytes SessionId = 2;
optional bytes PurchaseId = 3;
optional LicenseType Type = 4;
optional uint32 Version = 5;
optional bytes ProviderSessionToken = 6;
}
message License {
message Policy {
optional bool CanPlay = 1; // changed from uint32 to bool
optional bool CanPersist = 2;
optional bool CanRenew = 3;
optional uint32 RentalDurationSeconds = 4;
optional uint32 PlaybackDurationSeconds = 5;
optional uint32 LicenseDurationSeconds = 6;
optional uint32 RenewalRecoveryDurationSeconds = 7;
optional string RenewalServerUrl = 8;
optional uint32 RenewalDelaySeconds = 9;
optional uint32 RenewalRetryIntervalSeconds = 10;
optional bool RenewWithUsage = 11; // was uint32
}
message KeyContainer {
enum KeyType {
SIGNING = 1;
CONTENT = 2;
KEY_CONTROL = 3;
OPERATOR_SESSION = 4;
}
enum SecurityLevel {
SW_SECURE_CRYPTO = 1;
SW_SECURE_DECODE = 2;
HW_SECURE_CRYPTO = 3;
HW_SECURE_DECODE = 4;
HW_SECURE_ALL = 5;
}
message OutputProtection {
enum CGMS {
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
CGMS_NONE = 0x2A; // PC default!
}
optional ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
optional CGMS CgmsFlags = 2;
}
message KeyControl {
required bytes KeyControlBlock = 1; // what is this?
required bytes Iv = 2;
}
message OperatorSessionKeyPermissions {
optional uint32 AllowEncrypt = 1;
optional uint32 AllowDecrypt = 2;
optional uint32 AllowSign = 3;
optional uint32 AllowSignatureVerify = 4;
}
message VideoResolutionConstraint {
optional uint32 MinResolutionPixels = 1;
optional uint32 MaxResolutionPixels = 2;
optional OutputProtection RequiredProtection = 3;
}
optional bytes Id = 1;
optional bytes Iv = 2;
optional bytes Key = 3;
optional KeyType Type = 4;
optional SecurityLevel Level = 5;
optional OutputProtection RequiredProtection = 6;
optional OutputProtection RequestedProtection = 7;
optional KeyControl _KeyControl = 8; // duped names, etc
optional OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
}
optional LicenseIdentification Id = 1;
optional Policy _Policy = 2; // duped names, etc
repeated KeyContainer Key = 3;
optional uint32 LicenseStartTime = 4;
optional uint32 RemoteAttestationVerified = 5; // bool?
optional bytes ProviderClientToken = 6;
// there might be more, check with newer versions (I see field 7-8 in a lic)
// this appeared in latest x86:
optional uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
}
message LicenseError {
enum Error {
INVALID_DEVICE_CERTIFICATE = 1;
REVOKED_DEVICE_CERTIFICATE = 2;
SERVICE_UNAVAILABLE = 3;
}
//LicenseRequest.RequestType ErrorCode; // clang mismatch
optional Error ErrorCode = 1;
}
message LicenseRequest {
message ContentIdentification {
message CENC {
//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:
optional WidevineCencHeader Pssh = 1;
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!)
optional bytes RequestId = 3;
}
message WebM {
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
optional LicenseType LicenseType = 2;
optional bytes RequestId = 3;
}
message ExistingLicense {
optional LicenseIdentification LicenseId = 1;
optional uint32 SecondsSinceStarted = 2;
optional uint32 SecondsSinceLastPlayed = 3;
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
}
optional CENC CencId = 1;
optional WebM WebmId = 2;
optional ExistingLicense License = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification ClientId = 1;
optional ContentIdentification ContentId = 2;
optional RequestType Type = 3;
optional uint32 RequestTime = 4;
optional bytes KeyControlNonceDeprecated = 5;
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
optional uint32 KeyControlNonce = 7;
optional EncryptedClientIdentification EncryptedClientId = 8;
}
// raw pssh hack
message LicenseRequestRaw {
message ContentIdentification {
message CENC {
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:
//optional WidevineCencHeader Pssh = 1;
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!)
optional bytes RequestId = 3;
}
message WebM {
optional bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
optional LicenseType LicenseType = 2;
optional bytes RequestId = 3;
}
message ExistingLicense {
optional LicenseIdentification LicenseId = 1;
optional uint32 SecondsSinceStarted = 2;
optional uint32 SecondsSinceLastPlayed = 3;
optional bytes SessionUsageTableEntry = 4; // interesting! try to figure out the connection between the usage table blob and KCB!
}
optional CENC CencId = 1;
optional WebM WebmId = 2;
optional ExistingLicense License = 3;
}
enum RequestType {
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
optional ClientIdentification ClientId = 1;
optional ContentIdentification ContentId = 2;
optional RequestType Type = 3;
optional uint32 RequestTime = 4;
optional bytes KeyControlNonceDeprecated = 5;
optional ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
optional uint32 KeyControlNonce = 7;
optional EncryptedClientIdentification EncryptedClientId = 8;
}
message ProvisionedDeviceInfo {
enum WvSecurityLevel {
LEVEL_UNSPECIFIED = 0;
LEVEL_1 = 1;
LEVEL_2 = 2;
LEVEL_3 = 3;
}
optional uint32 SystemId = 1;
optional string Soc = 2;
optional string Manufacturer = 3;
optional string Model = 4;
optional string DeviceType = 5;
optional uint32 ModelYear = 6;
optional WvSecurityLevel SecurityLevel = 7;
optional uint32 TestDevice = 8; // bool?
}
// todo: fill
message ProvisioningOptions {
}
// todo: fill
message ProvisioningRequest {
}
// todo: fill
message ProvisioningResponse {
}
message RemoteAttestation {
optional EncryptedClientIdentification Certificate = 1;
optional string Salt = 2;
optional string Signature = 3;
}
// todo: fill
message SessionInit {
}
// todo: fill
message SessionState {
}
// todo: fill
message SignedCertificateStatusList {
}
message SignedDeviceCertificate {
//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:
optional DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
optional bytes Signature = 2;
optional SignedDeviceCertificate Signer = 3;
}
// todo: fill
message SignedProvisioningMessage {
}
// the root of all messages, from either server or client
message SignedMessage {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
// This message is copied from google's docs, not reversed:
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
optional Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
optional string provider = 3;
// A content identifier, specified by content provider.
optional bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
optional string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
optional string policy = 6;
// Crypto period index, for media using key rotation.
optional uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
optional bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
optional uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
optional uint32 crypto_period_seconds = 10;
}
// remove these when using it outside of protoc:
// 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
message SignedLicenseRequest {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
// hack
message SignedLicenseRequestRaw {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
message SignedLicense {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
message SignedServiceCertificate {
enum MessageType {
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
optional MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
optional bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
optional bytes SessionKey = 4; // often RSA wrapped for licenses
optional RemoteAttestation RemoteAttestation = 5;
}
//vmp support
message FileHashes {
message Signature {
optional string filename = 1;
optional bool test_signing = 2; //0 - release, 1 - testing
optional bytes SHA512Hash = 3;
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
optional bytes signature = 5;
}
optional bytes signer = 1;
repeated Signature signatures = 2;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,389 @@
// 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:
syntax = "proto3";
// from x86 (partial), most of it from the ARM version:
message ClientIdentification {
enum TokenType {
KEYBOX = 0;
DEVICE_CERTIFICATE = 1;
REMOTE_ATTESTATION_CERTIFICATE = 2;
}
message NameValue {
string Name = 1;
string Value = 2;
}
message ClientCapabilities {
enum HdcpVersion {
HDCP_NONE = 0;
HDCP_V1 = 1;
HDCP_V2 = 2;
HDCP_V2_1 = 3;
HDCP_V2_2 = 4;
}
uint32 ClientToken = 1;
uint32 SessionToken = 2;
uint32 VideoResolutionConstraints = 3;
HdcpVersion MaxHdcpVersion = 4;
uint32 OemCryptoApiVersion = 5;
}
TokenType Type = 1;
//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:
SignedDeviceCertificate Token = 2;
repeated NameValue ClientInfo = 3;
bytes ProviderClientToken = 4;
uint32 LicenseCounter = 5;
ClientCapabilities _ClientCapabilities = 6; // how should we deal with duped names? will have to look at proto docs later
}
message DeviceCertificate {
enum CertificateType {
ROOT = 0;
INTERMEDIATE = 1;
USER_DEVICE = 2;
SERVICE = 3;
}
//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)
CertificateType Type = 1;
bytes SerialNumber = 2;
uint32 CreationTimeSeconds = 3;
bytes PublicKey = 4;
uint32 SystemId = 5;
uint32 TestDeviceDeprecated = 6; // is it bool or int?
bytes ServiceId = 7; // service URL for service certificates
}
// missing some references,
message DeviceCertificateStatus {
enum CertificateStatus {
VALID = 0;
REVOKED = 1;
}
bytes SerialNumber = 1;
CertificateStatus Status = 2;
ProvisionedDeviceInfo DeviceInfo = 4; // where is 3? is it deprecated?
}
message DeviceCertificateStatusList {
uint32 CreationTimeSeconds = 1;
repeated DeviceCertificateStatus CertificateStatus = 2;
}
message EncryptedClientIdentification {
string ServiceId = 1;
bytes ServiceCertificateSerialNumber = 2;
bytes EncryptedClientId = 3;
bytes EncryptedClientIdIv = 4;
bytes EncryptedPrivacyKey = 5;
}
// todo: fill (for this top-level type, it might be impossible/difficult)
enum LicenseType {
ZERO = 0;
DEFAULT = 1; // do not know what this is either, but should be 1; on recent versions may go up to 3 (latest x86)
}
// todo: fill (for this top-level type, it might be impossible/difficult)
// this is just a guess because these globals got lost, but really, do we need more?
enum ProtocolVersion {
DUMMY = 0;
CURRENT = 21; // don't have symbols for this
}
message LicenseIdentification {
bytes RequestId = 1;
bytes SessionId = 2;
bytes PurchaseId = 3;
LicenseType Type = 4;
uint32 Version = 5;
bytes ProviderSessionToken = 6;
}
message License {
message Policy {
uint32 CanPlay = 1;
uint32 CanPersist = 2;
uint32 CanRenew = 3;
uint32 RentalDurationSeconds = 4;
uint32 PlaybackDurationSeconds = 5;
uint32 LicenseDurationSeconds = 6;
uint32 RenewalRecoveryDurationSeconds = 7;
string RenewalServerUrl = 8;
uint32 RenewalDelaySeconds = 9;
uint32 RenewalRetryIntervalSeconds = 10;
uint32 RenewWithUsage = 11;
uint32 UnknownPolicy12 = 12;
}
message KeyContainer {
enum KeyType {
_NOKEYTYPE = 0; // dummy, added to satisfy proto3, not present in original
SIGNING = 1;
CONTENT = 2;
KEY_CONTROL = 3;
OPERATOR_SESSION = 4;
}
enum SecurityLevel {
_NOSECLEVEL = 0; // dummy, added to satisfy proto3, not present in original
SW_SECURE_CRYPTO = 1;
SW_SECURE_DECODE = 2;
HW_SECURE_CRYPTO = 3;
HW_SECURE_DECODE = 4;
HW_SECURE_ALL = 5;
}
message OutputProtection {
enum CGMS {
COPY_FREE = 0;
COPY_ONCE = 2;
COPY_NEVER = 3;
CGMS_NONE = 0x2A; // PC default!
}
ClientIdentification.ClientCapabilities.HdcpVersion Hdcp = 1; // it's most likely a copy of Hdcp version available here, but compiler optimized it away
CGMS CgmsFlags = 2;
}
message KeyControl {
bytes KeyControlBlock = 1; // what is this?
bytes Iv = 2;
}
message OperatorSessionKeyPermissions {
uint32 AllowEncrypt = 1;
uint32 AllowDecrypt = 2;
uint32 AllowSign = 3;
uint32 AllowSignatureVerify = 4;
}
message VideoResolutionConstraint {
uint32 MinResolutionPixels = 1;
uint32 MaxResolutionPixels = 2;
OutputProtection RequiredProtection = 3;
}
bytes Id = 1;
bytes Iv = 2;
bytes Key = 3;
KeyType Type = 4;
SecurityLevel Level = 5;
OutputProtection RequiredProtection = 6;
OutputProtection RequestedProtection = 7;
KeyControl _KeyControl = 8; // duped names, etc
OperatorSessionKeyPermissions _OperatorSessionKeyPermissions = 9; // duped names, etc
repeated VideoResolutionConstraint VideoResolutionConstraints = 10;
}
LicenseIdentification Id = 1;
Policy _Policy = 2; // duped names, etc
repeated KeyContainer Key = 3;
uint32 LicenseStartTime = 4;
uint32 RemoteAttestationVerified = 5; // bool?
bytes ProviderClientToken = 6;
// there might be more, check with newer versions (I see field 7-8 in a lic)
// this appeared in latest x86:
uint32 ProtectionScheme = 7; // type unconfirmed fully, but it's likely as WidevineCencHeader describesit (fourcc)
bytes UnknownHdcpDataField = 8;
}
message LicenseError {
enum Error {
DUMMY_NO_ERROR = 0; // dummy, added to satisfy proto3
INVALID_DEVICE_CERTIFICATE = 1;
REVOKED_DEVICE_CERTIFICATE = 2;
SERVICE_UNAVAILABLE = 3;
}
//LicenseRequest.RequestType ErrorCode; // clang mismatch
Error ErrorCode = 1;
}
message LicenseRequest {
message ContentIdentification {
message CENC {
// 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:
WidevineCencHeader Pssh = 1;
LicenseType LicenseType = 2; // unfortunately the LicenseType symbols are not present, acceptable value seems to only be 1
bytes RequestId = 3;
}
message WebM {
bytes Header = 1; // identical to CENC, aside from PSSH and the parent field number used
LicenseType LicenseType = 2;
bytes RequestId = 3;
}
message ExistingLicense {
LicenseIdentification LicenseId = 1;
uint32 SecondsSinceStarted = 2;
uint32 SecondsSinceLastPlayed = 3;
bytes SessionUsageTableEntry = 4;
}
CENC CencId = 1;
WebM WebmId = 2;
ExistingLicense License = 3;
}
enum RequestType {
DUMMY_REQ_TYPE = 0; // dummy, added to satisfy proto3
NEW = 1;
RENEWAL = 2;
RELEASE = 3;
}
ClientIdentification ClientId = 1;
ContentIdentification ContentId = 2;
RequestType Type = 3;
uint32 RequestTime = 4;
bytes KeyControlNonceDeprecated = 5;
ProtocolVersion ProtocolVersion = 6; // lacking symbols for this
uint32 KeyControlNonce = 7;
EncryptedClientIdentification EncryptedClientId = 8;
}
message ProvisionedDeviceInfo {
enum WvSecurityLevel {
LEVEL_UNSPECIFIED = 0;
LEVEL_1 = 1;
LEVEL_2 = 2;
LEVEL_3 = 3;
}
uint32 SystemId = 1;
string Soc = 2;
string Manufacturer = 3;
string Model = 4;
string DeviceType = 5;
uint32 ModelYear = 6;
WvSecurityLevel SecurityLevel = 7;
uint32 TestDevice = 8; // bool?
}
// todo: fill
message ProvisioningOptions {
}
// todo: fill
message ProvisioningRequest {
}
// todo: fill
message ProvisioningResponse {
}
message RemoteAttestation {
EncryptedClientIdentification Certificate = 1;
string Salt = 2;
string Signature = 3;
}
// todo: fill
message SessionInit {
}
// todo: fill
message SessionState {
}
// todo: fill
message SignedCertificateStatusList {
}
message SignedDeviceCertificate {
//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:
DeviceCertificate _DeviceCertificate = 1; // how should we deal with duped names? will have to look at proto docs later
bytes Signature = 2;
SignedDeviceCertificate Signer = 3;
}
// todo: fill
message SignedProvisioningMessage {
}
// the root of all messages, from either server or client
message SignedMessage {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
// This message is copied from google's docs, not reversed:
message WidevineCencHeader {
enum Algorithm {
UNENCRYPTED = 0;
AESCTR = 1;
};
Algorithm algorithm = 1;
repeated bytes key_id = 2;
// Content provider name.
string provider = 3;
// A content identifier, specified by content provider.
bytes content_id = 4;
// Track type. Acceptable values are SD, HD and AUDIO. Used to
// differentiate content keys used by an asset.
string track_type_deprecated = 5;
// The name of a registered policy to be used for this asset.
string policy = 6;
// Crypto period index, for media using key rotation.
uint32 crypto_period_index = 7;
// Optional protected context for group content. The grouped_license is a
// serialized SignedMessage.
bytes grouped_license = 8;
// Protection scheme identifying the encryption algorithm.
// Represented as one of the following 4CC values:
// 'cenc' (AESCTR), 'cbc1' (AESCBC),
// 'cens' (AESCTR subsample), 'cbcs' (AESCBC subsample).
uint32 protection_scheme = 9;
// Optional. For media using key rotation, this represents the duration
// of each crypto period in seconds.
uint32 crypto_period_seconds = 10;
}
// 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
message SignedLicenseRequest {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}
message SignedLicense {
enum MessageType {
DUMMY_MSG_TYPE = 0; // dummy, added to satisfy proto3
LICENSE_REQUEST = 1;
LICENSE = 2;
ERROR_RESPONSE = 3;
SERVICE_CERTIFICATE_REQUEST = 4;
SERVICE_CERTIFICATE = 5;
}
MessageType Type = 1; // has in incorrect overlap with License_KeyContainer_SecurityLevel
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
// for SERVICE_CERTIFICATE, only Type and Msg are present, and it's just a DeviceCertificate with CertificateType set to SERVICE
bytes Signature = 3; // might be different type of signatures (ex. RSA vs AES CMAC(??), unconfirmed for now)
bytes SessionKey = 4; // often RSA wrapped for licenses
RemoteAttestation RemoteAttestation = 5;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
import binascii
class Key:
def __init__(self, kid, type, key, permissions=[]):
self.kid = kid
self.type = type
self.key = key
self.permissions = permissions
def __repr__(self):
if self.type == "OPERATOR_SESSION":
return "key(kid={}, type={}, key={}, permissions={})".format(self.kid, self.type, binascii.hexlify(self.key), self.permissions)
else:
return "key(kid={}, type={}, key={})".format(self.kid, self.type, binascii.hexlify(self.key))

View File

@ -0,0 +1,18 @@
class Session:
def __init__(self, session_id, init_data, device_config, offline):
self.session_id = session_id
self.init_data = init_data
self.offline = offline
self.device_config = device_config
self.device_key = None
self.session_key = None
self.derived_keys = {
'enc': None,
'auth_1': None,
'auth_2': None
}
self.license_request = None
self.license = None
self.service_certificate = None
self.privacy_mode = False
self.keys = []

View File

@ -0,0 +1,102 @@
try:
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)
except ImportError:
# this is generic and does not depend on pb internals, however it will decode "larger" possible numbers than pb decoder which has them fixed
def LEB128_decode(buffer, pos, limit = 64):
result = 0
shift = 0
while True:
b = buffer[pos]
pos += 1
result |= ((b & 0x7F) << shift)
if not (b & 0x80):
return (result, pos)
shift += 7
if shift > limit:
raise Exception("integer too large, shift: {}".format(shift))
_di = LEB128_decode
class FromFileMixin:
@classmethod
def from_file(cls, filename):
"""Load given a filename"""
with open(filename,"rb") as f:
return cls(f.read())
# the signatures use a format internally similar to
# protobuf's encoding, but without wire types
class VariableReader(FromFileMixin):
"""Protobuf-like encoding reader"""
def __init__(self, buf):
self.buf = buf
self.pos = 0
self.size = len(buf)
def read_int(self):
"""Read a variable length integer"""
# _DecodeVarint will take care of out of range errors
(val, nextpos) = _di(self.buf, self.pos)
self.pos = nextpos
return val
def read_bytes_raw(self, size):
"""Read size bytes"""
b = self.buf[self.pos:self.pos+size]
self.pos += size
return b
def read_bytes(self):
"""Read a bytes object"""
size = self.read_int()
return self.read_bytes_raw(size)
def is_end(self):
return (self.size == self.pos)
class TaggedReader(VariableReader):
"""Tagged reader, needed for implementing a WideVine signature reader"""
def read_tag(self):
"""Read a tagged buffer"""
return (self.read_int(), self.read_bytes())
def read_all_tags(self, max_tag=3):
tags = {}
while (not self.is_end()):
(tag, bytes) = self.read_tag()
if (tag > max_tag):
raise IndexError("tag out of bound: got {}, max {}".format(tag, max_tag))
tags[tag] = bytes
return tags
class WideVineSignatureReader(FromFileMixin):
"""Parses a widevine .sig signature file."""
SIGNER_TAG = 1
SIGNATURE_TAG = 2
ISMAINEXE_TAG = 3
def __init__(self, buf):
reader = TaggedReader(buf)
self.version = reader.read_int()
if (self.version != 0):
raise Exception("Unsupported signature format version {}".format(self.version))
self.tags = reader.read_all_tags()
self.signer = self.tags[self.SIGNER_TAG]
self.signature = self.tags[self.SIGNATURE_TAG]
extra = self.tags[self.ISMAINEXE_TAG]
if (len(extra) != 1 or (extra[0] > 1)):
raise Exception("Unexpected 'ismainexe' field value (not '\\x00' or '\\x01'), please check: {0}".format(extra))
self.mainexe = bool(extra[0])
@classmethod
def get_tags(cls, filename):
"""Return a dictionary of each tag in the signature file"""
return cls.from_file(filename).tags

View File

View File

@ -0,0 +1,46 @@
import logging
import subprocess
import re
from tqdm import tqdm
import base64
from pywidevine.cdm import cdm, deviceconfig
class WvDecrypt(object):
WV_SYSTEM_ID = [237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]
def __init__(self, PSSH):
self.logger = logging.getLogger(__name__)
self.wvdecrypt_process = None
self.pssh = PSSH
self.cdm = cdm.Cdm()
def check_pssh(pssh_b64):
pssh = base64.b64decode(pssh_b64)
if not pssh[12:28] == bytes(self.WV_SYSTEM_ID):
new_pssh = bytearray([0,0,0])
new_pssh.append(32+len(pssh))
new_pssh[4:] = bytearray(b'pssh')
new_pssh[8:] = [0,0,0,0]
new_pssh[13:] = self.WV_SYSTEM_ID
new_pssh[29:] = [0,0,0,0]
new_pssh[31] = len(pssh)
new_pssh[32:] = pssh
return base64.b64encode(new_pssh)
else:
return pssh_b64
self.session = self.cdm.open_session(check_pssh(self.pssh),
deviceconfig.DeviceConfig(deviceconfig.device_chromecdm_1610))
def start_process(self):
keysR = self.cdm.get_keys(self.session)
return keysR
def get_challenge(self):
return self.cdm.get_license_request(self.session)
def update_license(self, license_b64):
self.cdm.provide_license(self.session, license_b64)
return True

View File

@ -0,0 +1,57 @@
import pywidevine.downloader.wvdownloaderconfig as wvdl_cfg
import subprocess
class WvDecryptConfig(object):
def __init__(self, filename, tracktype, trackno, license, init_data_b64, cert_data_b64=None):
self.filename = filename
self.tracktype = tracktype
self.trackno = trackno
self.init_data_b64 = init_data_b64
self.license = license
if cert_data_b64 is not None:
self.server_cert_required = True
self.cert_data_b64 = cert_data_b64
else:
self.server_cert_required = False
def get_filename(self, unformatted_filename):
return unformatted_filename.format(filename=self.filename, track_type=self.tracktype, track_no=self.trackno)
def find_str(self, s, char):
index = 0
if char in s:
c = char[0]
for ch in s:
if ch == c and s[index:index + len(char)] == char:
return index
index += 1
return -1
def get_kid(self, filename):
mp4dump = subprocess.Popen([wvdl_cfg.MP4DUMP_BINARY_PATH, filename], stdout=subprocess.PIPE)
mp4dump = str(mp4dump.stdout.read())
A = self.find_str(mp4dump, 'default_KID')
KID = mp4dump[A:A + 63].replace('default_KID = ', '').replace('[', '').replace(']', '').replace(' ', '')
KID = KID.upper()
KID_video = KID[0:8] + '-' + KID[8:12] + '-' + KID[12:16] + '-' + KID[16:20] + '-' + KID[20:32]
if KID == '':
KID = 'nothing'
return KID.lower()
def build_commandline_list(self, keys):
KID_file = self.get_kid(self.get_filename(wvdl_cfg.ENCRYPTED_FILENAME))
print(KID_file)
commandline = [wvdl_cfg.MP4DECRYPT_BINARY_PATH]
for key in keys:
if key.type == 'CONTENT' and key.kid.hex() == KID_file:
print("OK")
commandline.append('--key')
#key.kid.hex()
#2 main high 1 hdr dv hevc
commandline.append('{}:{}'.format('2', key.key.hex()))
commandline.append(self.get_filename(wvdl_cfg.ENCRYPTED_FILENAME))
commandline.append(self.get_filename(wvdl_cfg.DECRYPTED_FILENAME))
return commandline