7 from StringIO import StringIO
14 from Crypto.PublicKey import RSA
15 from Crypto.Cipher import PKCS1_OAEP
16 from Crypto.Cipher import AES
17 from Crypto.Random import get_random_bytes
18 # from Crypto.Hash import HMAC, SHA256
19 from Crypto.Util import Padding
20 import xml.etree.ElementTree as ET
22 pp = pprint.PrettyPrinter(indent=4)
24 def base64key_decode(payload):
31 raise ValueError('Invalid base64 string')
32 return base64.urlsafe_b64decode(payload.encode('utf-8'))
36 handshake_performed = False # Is a handshake already performed and the keys loaded
38 last_playback_context = ''
39 #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
40 esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
41 current_message_id = 0
42 session = requests.session()
43 rndm = random.SystemRandom()
46 'manifest': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/manifest',
47 'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
50 def __init__(self, email, password, kodi_helper):
52 The Constructor checks for already existing crypto Keys.
53 If they exist it will load the existing keys
56 self.password = password
57 self.kodi_helper = kodi_helper
59 os.mkdir(self.kodi_helper.msl_data_path)
63 if self.file_exists(self.kodi_helper.msl_data_path, 'msl_data.json'):
64 self.__load_msl_data()
65 self.handshake_performed = True
66 elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
67 self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
68 self.__load_rsa_keys()
69 self.__perform_key_handshake()
71 self.kodi_helper.log(msg='Create new RSA Keys')
72 # Create new Key Pair and save
73 self.rsa_key = RSA.generate(2048)
74 self.__save_rsa_keys()
75 self.__perform_key_handshake()
77 def load_manifest(self, viewable_id):
78 manifest_request_data = {
80 'lookupType': 'PREPARE',
81 'viewableIds': [viewable_id],
83 'playready-h264mpl30-dash',
84 'playready-h264mpl31-dash',
92 'drmSystem': 'widevine',
93 'appId': '14673889385265',
95 'pinCapableClient': False,
96 'uiplaycontext': 'null'
98 'sessionId': '14673889385265',
100 'flavor': 'PRE_FETCH',
102 'supportPreviewContent': True,
103 'forceClearStreams': False,
104 'languages': ['de-DE'],
105 'clientVersion': '4.0004.899.011',
108 request_data = self.__generate_msl_request_data(manifest_request_data)
110 resp = self.session.post(self.endpoints['manifest'], request_data)
115 self.kodi_helper.log(msg='MANIFEST RESPONE JSON: '+resp.text)
117 # Maybe we have a CHUNKED response
118 resp = self.__parse_chunked_msl_response(resp.text)
119 data = self.__decrypt_payload_chunk(resp['payloads'][0])
120 # pprint.pprint(data)
121 return self.__tranform_to_dash(data)
124 def get_license(self, challenge, sid):
127 std::time_t t = std::time(0); // t is an integer type
128 licenseRequestData["clientTime"] = (int)t;
129 //licenseRequestData["challengeBase64"] = challengeStr;
130 licenseRequestData["licenseType"] = "STANDARD";
131 licenseRequestData["playbackContextId"] = playbackContextId;//"E1-BQFRAAELEB32o6Se-GFvjwEIbvDydEtfj6zNzEC3qwfweEPAL3gTHHT2V8rS_u1Mc3mw5BWZrUlKYIu4aArdjN8z_Z8t62E5jRjLMdCKMsVhlSJpiQx0MNW4aGqkYz-1lPh85Quo4I_mxVBG5lgd166B5NDizA8.";
132 licenseRequestData["drmContextIds"] = Json::arrayValue;
133 licenseRequestData["drmContextIds"].append(drmContextId);
141 license_request_data = {
143 'licenseType': 'STANDARD',
144 'clientVersion': '4.0004.899.011',
145 'uiVersion': 'akira',
146 'languages': ['de-DE'],
147 'playbackContextId': self.last_playback_context,
148 'drmContextIds': [self.last_drm_context],
150 'dataBase64': challenge,
153 'clientTime': int(time.time()),
154 'xid': int((int(time.time()) + 0.1612) * 1000)
157 request_data = self.__generate_msl_request_data(license_request_data)
159 resp = self.session.post(self.endpoints['license'], request_data)
163 self.kodi_helper.log(msg='LICENSE RESPONE JSON: '+resp.text)
165 # Maybe we have a CHUNKED response
166 resp = self.__parse_chunked_msl_response(resp.text)
167 data = self.__decrypt_payload_chunk(resp['payloads'][0])
168 # pprint.pprint(data)
169 if data['success'] is True:
170 return data['result']['licenses'][0]['data']
175 def __decrypt_payload_chunk(self, payloadchunk):
176 payloadchunk = json.JSONDecoder().decode(payloadchunk)
177 encryption_envelope = json.JSONDecoder().decode(base64.standard_b64decode(payloadchunk['payload']))
179 cipher = AES.new(self.encryption_key, AES.MODE_CBC, base64.standard_b64decode(encryption_envelope['iv']))
180 plaintext = cipher.decrypt(base64.standard_b64decode(encryption_envelope['ciphertext']))
181 # unpad the plaintext
182 plaintext = json.JSONDecoder().decode(Padding.unpad(plaintext, 16))
183 data = plaintext['data']
185 # uncompress data if compressed
186 if plaintext['compressionalgo'] == 'GZIP':
187 data = zlib.decompress(base64.standard_b64decode(data), 16 + zlib.MAX_WBITS)
189 data = base64.standard_b64decode(data)
191 data = json.JSONDecoder().decode(data)[1]['payload']['data']
192 data = base64.standard_b64decode(data)
193 return json.JSONDecoder().decode(data)
196 def __tranform_to_dash(self, manifest):
198 self.save_file(self.kodi_helper.msl_data_path, 'manifest.json', json.dumps(manifest))
199 manifest = manifest['result']['viewables'][0]
201 self.last_playback_context = manifest['playbackContextId']
202 self.last_drm_context = manifest['drmContextId']
206 if 'psshb64' in manifest:
207 if len(manifest['psshb64']) >= 1:
208 pssh = manifest['psshb64'][0]
210 seconds = manifest['runtime']/1000
211 duration = "PT"+str(seconds)+".00S"
213 root = ET.Element('MPD')
214 root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
215 root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
216 root.attrib['mediaPresentationDuration'] = duration
218 period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
220 # One Adaption Set for Video
221 for video_track in manifest['videoTracks']:
222 video_adaption_set = ET.SubElement(period, 'AdaptationSet', mimeType='video/mp4', contentType="video")
224 protection = ET.SubElement(video_adaption_set, 'ContentProtection',
225 schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED')
227 ET.SubElement(protection, 'cenc:pssh').text = pssh
229 for downloadable in video_track['downloadables']:
230 rep = ET.SubElement(video_adaption_set, 'Representation',
231 width=str(downloadable['width']),
232 height=str(downloadable['height']),
233 bandwidth=str(downloadable['bitrate']*1024),
235 mimeType='video/mp4')
238 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
239 # Init an Segment block
240 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
241 ET.SubElement(segment_base, 'Initialization', range='0-60000')
245 # Multiple Adaption Set for audio
246 for audio_track in manifest['audioTracks']:
247 audio_adaption_set = ET.SubElement(period, 'AdaptationSet',
248 lang=audio_track['bcp47'],
250 mimeType='audio/mp4')
251 for downloadable in audio_track['downloadables']:
252 rep = ET.SubElement(audio_adaption_set, 'Representation',
254 bandwidth=str(downloadable['bitrate']*1024),
255 mimeType='audio/mp4')
258 ET.SubElement(rep, 'AudioChannelConfiguration',
259 schemeIdUri='urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
260 value=str(audio_track['channelsCount']))
263 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
265 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
266 ET.SubElement(segment_base, 'Initialization', range='0-60000')
269 xml = ET.tostring(root, encoding='utf-8', method='xml')
270 xml = xml.replace('\n', '').replace('\r', '')
273 def __get_base_url(self, urls):
277 def __parse_chunked_msl_response(self, message):
285 while i < len(message):
286 if message[i] == '{':
287 opencount = opencount + 1
288 if message[i] == '}':
289 closecount = closecount + 1
290 if opencount == closecount:
295 payloads.append(message[old_end:i + 1])
303 def __generate_msl_request_data(self, data):
304 header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
306 'headerdata': base64.standard_b64encode(header_encryption_envelope),
307 'signature': self.__sign(header_encryption_envelope),
308 'mastertoken': self.mastertoken,
311 # Serialize the given Data
312 serialized_data = json.dumps(data)
313 serialized_data = serialized_data.replace('"', '\\"')
314 serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-11","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
316 compressed_data = self.__compress_data(serialized_data)
318 # Create FIRST Payload Chunks
320 "messageid": self.current_message_id,
321 "data": compressed_data,
322 "compressionalgo": "GZIP",
326 first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
327 first_payload_chunk = {
328 'payload': base64.standard_b64encode(first_payload_encryption_envelope),
329 'signature': self.__sign(first_payload_encryption_envelope),
333 # Create Second Payload
335 "messageid": self.current_message_id,
340 second_payload_encryption_envelope = self.__encrypt(json.dumps(second_payload))
341 second_payload_chunk = {
342 'payload': base64.standard_b64encode(second_payload_encryption_envelope),
343 'signature': base64.standard_b64encode(self.__sign(second_payload_encryption_envelope)),
346 request_data = json.dumps(header) + json.dumps(first_payload_chunk) # + json.dumps(second_payload_chunk)
351 def __compress_data(self, data):
354 with gzip.GzipFile(fileobj=out, mode="w") as f:
356 return base64.standard_b64encode(out.getvalue())
359 def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
361 Function that generates a MSL header dict
362 :return: The base64 encoded JSON String of the header
364 self.current_message_id = self.rndm.randint(0, pow(2, 52))
368 'handshake': is_handshake,
369 'nonreplayable': False,
371 'languages': ["en-US"],
372 'compressionalgos': []
374 'recipient': 'Netflix',
376 'messageid': self.current_message_id,
377 'timestamp': 1467733923
380 # Add compression algo if not empty
381 if compressionalgo is not "":
382 header_data['capabilities']['compressionalgos'].append(compressionalgo)
384 # If this is a keyrequest act diffrent then other requests
386 public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER'))
387 header_data['keyrequestdata'] = [{
388 'scheme': 'ASYMMETRIC_WRAPPED',
390 'publickey': public_key,
391 'mechanism': 'JWK_RSA',
392 'keypairid': 'superKeyPair'
396 if 'usertoken' in self.tokens:
399 # Auth via email and password
400 header_data['userauthdata'] = {
401 'scheme': 'EMAIL_PASSWORD',
404 'password': self.password
408 return json.dumps(header_data)
412 def __encrypt(self, plaintext):
414 Encrypt the given Plaintext with the encryption key
416 :return: Serialized JSON String of the encryption Envelope
418 iv = get_random_bytes(16)
419 encryption_envelope = {
421 'keyid': self.esn + '_' + str(self.sequence_number),
423 'iv': base64.standard_b64encode(iv)
426 plaintext = Padding.pad(plaintext, 16)
428 cipher = AES.new(self.encryption_key, AES.MODE_CBC, iv)
429 ciphertext = cipher.encrypt(plaintext)
430 encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
431 return json.dumps(encryption_envelope)
433 def __sign(self, text):
434 #signature = hmac.new(self.sign_key, text, hashlib.sha256).digest()
435 signature = HMAC(self.sign_key, text, hashlib.sha256).digest()
438 # hmac = HMAC.new(self.sign_key, digestmod=SHA256)
440 return base64.standard_b64encode(signature)
443 def __perform_key_handshake(self):
445 header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
453 'headerdata': base64.standard_b64encode(header),
456 self.kodi_helper.log(msg='Key Handshake Request:')
457 self.kodi_helper.log(msg=json.dumps(request))
460 resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
461 if resp.status_code == 200:
463 if 'errordata' in resp:
464 self.kodi_helper.log(msg='Key Exchange failed')
465 self.kodi_helper.log(msg=base64.standard_b64decode(resp['errordata']))
467 self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
469 self.kodi_helper.log(msg='Key Exchange failed')
470 self.kodi_helper.log(msg=resp.text)
472 def __parse_crypto_keys(self, headerdata):
473 self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
475 encrypted_encryption_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['encryptionkey'])
476 encrypted_sign_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['hmackey'])
477 cipher_rsa = PKCS1_OAEP.new(self.rsa_key)
479 # Decrypt encryption key
480 encryption_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_encryption_key))
481 self.encryption_key = base64key_decode(encryption_key_data['k'])
484 sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key))
485 self.sign_key = base64key_decode(sign_key_data['k'])
487 self.__save_msl_data()
488 self.handshake_performed = True
490 def __load_msl_data(self):
491 msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
492 self.__set_master_token(msl_data['tokens']['mastertoken'])
493 self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
494 self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
496 def __save_msl_data(self):
498 Saves the keys and tokens in json file
502 "encryption_key": base64.standard_b64encode(self.encryption_key),
503 'sign_key': base64.standard_b64encode(self.sign_key),
505 'mastertoken': self.mastertoken
508 serialized_data = json.JSONEncoder().encode(data)
509 self.save_file(self.kodi_helper.msl_data_path, 'msl_data.json', serialized_data)
511 def __set_master_token(self, master_token):
512 self.mastertoken = master_token
513 self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))[
516 def __load_rsa_keys(self):
517 loaded_key = self.load_file(self.kodi_helper.msl_data_path, 'rsa_key.bin')
518 self.rsa_key = RSA.importKey(loaded_key)
520 def __save_rsa_keys(self):
521 self.kodi_helper.log(msg='Save RSA Keys')
522 # Get the DER Base64 of the keys
523 encrypted_key = self.rsa_key.exportKey()
524 self.save_file(self.kodi_helper.msl_data_path, 'rsa_key.bin', encrypted_key)
527 def file_exists(msl_data_path, filename):
529 Checks if a given file exists
530 :param filename: The filename
533 return os.path.isfile(msl_data_path + filename)
536 def save_file(msl_data_path, filename, content):
538 Saves the given content under given filename
539 :param filename: The filename
540 :param content: The content of the file
542 with open(msl_data_path + filename, 'w') as file_:
547 def load_file(msl_data_path, filename):
549 Loads the content of a given filename
550 :param filename: The file to load
551 :return: The content of the file
553 with open(msl_data_path + filename) as file_:
554 file_content = file_.read()