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
23 pp = pprint.PrettyPrinter(indent=4)
25 def base64key_decode(payload):
32 raise ValueError('Invalid base64 string')
33 return base64.urlsafe_b64decode(payload.encode('utf-8'))
37 handshake_performed = False # Is a handshake already performed and the keys loaded
39 last_playback_context = ''
40 esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
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):
52 The Constructor checks for already existing crypto Keys.
53 If they exist it will load the existing keys
56 self.password = password
58 if self.file_exists('msl_data.json'):
59 self.__load_msl_data()
60 self.handshake_performed = True
61 elif self.file_exists('rsa_key.bin'):
62 log('RSA Keys do already exist load old ones')
63 self.__load_rsa_keys()
64 self.__perform_key_handshake()
66 log('Create new RSA Keys')
67 # Create new Key Pair and save
68 self.rsa_key = RSA.generate(2048)
69 self.__save_rsa_keys()
70 self.__perform_key_handshake()
72 def load_manifest(self, viewable_id):
73 manifest_request_data = {
75 'lookupType': 'PREPARE',
76 'viewableIds': [viewable_id],
78 'playready-h264mpl30-dash',
79 'playready-h264mpl31-dash',
87 'drmSystem': 'widevine',
88 'appId': '14673889385265',
90 'pinCapableClient': False,
91 'uiplaycontext': 'null'
93 'sessionId': '14673889385265',
95 'flavor': 'PRE_FETCH',
97 'supportPreviewContent': True,
98 'forceClearStreams': False,
99 'languages': ['de-DE'],
100 'clientVersion': '4.0004.899.011',
103 request_data = self.__generate_msl_request_data(manifest_request_data)
105 resp = self.session.post(self.endpoints['manifest'], request_data)
110 log('MANIFEST RESPONE JSON: '+resp.text)
112 # Maybe we have a CHUNKED response
113 resp = self.__parse_chunked_msl_response(resp.text)
114 data = self.__decrypt_payload_chunk(resp['payloads'][0])
115 # pprint.pprint(data)
116 return self.__tranform_to_dash(data)
119 def get_license(self, challenge, sid):
122 std::time_t t = std::time(0); // t is an integer type
123 licenseRequestData["clientTime"] = (int)t;
124 //licenseRequestData["challengeBase64"] = challengeStr;
125 licenseRequestData["licenseType"] = "STANDARD";
126 licenseRequestData["playbackContextId"] = playbackContextId;//"E1-BQFRAAELEB32o6Se-GFvjwEIbvDydEtfj6zNzEC3qwfweEPAL3gTHHT2V8rS_u1Mc3mw5BWZrUlKYIu4aArdjN8z_Z8t62E5jRjLMdCKMsVhlSJpiQx0MNW4aGqkYz-1lPh85Quo4I_mxVBG5lgd166B5NDizA8.";
127 licenseRequestData["drmContextIds"] = Json::arrayValue;
128 licenseRequestData["drmContextIds"].append(drmContextId);
136 license_request_data = {
138 'licenseType': 'STANDARD',
139 'clientVersion': '4.0004.899.011',
140 'uiVersion': 'akira',
141 'languages': ['de-DE'],
142 'playbackContextId': self.last_playback_context,
143 'drmContextIds': [self.last_drm_context],
145 'dataBase64': challenge,
148 'clientTime': int(time.time()),
149 'xid': int((int(time.time()) + 0.1612) * 1000)
152 request_data = self.__generate_msl_request_data(license_request_data)
154 resp = self.session.post(self.endpoints['license'], request_data)
158 log('LICENSE RESPONE JSON: '+resp.text)
160 # Maybe we have a CHUNKED response
161 resp = self.__parse_chunked_msl_response(resp.text)
162 data = self.__decrypt_payload_chunk(resp['payloads'][0])
163 # pprint.pprint(data)
164 if data['success'] is True:
165 return data['result']['licenses'][0]['data']
170 def __decrypt_payload_chunk(self, payloadchunk):
171 payloadchunk = json.JSONDecoder().decode(payloadchunk)
172 encryption_envelope = json.JSONDecoder().decode(base64.standard_b64decode(payloadchunk['payload']))
174 cipher = AES.new(self.encryption_key, AES.MODE_CBC, base64.standard_b64decode(encryption_envelope['iv']))
175 plaintext = cipher.decrypt(base64.standard_b64decode(encryption_envelope['ciphertext']))
176 # unpad the plaintext
177 plaintext = json.JSONDecoder().decode(Padding.unpad(plaintext, 16))
178 data = plaintext['data']
180 # uncompress data if compressed
181 if plaintext['compressionalgo'] == 'GZIP':
182 data = zlib.decompress(base64.standard_b64decode(data), 16 + zlib.MAX_WBITS)
184 data = base64.standard_b64decode(data)
186 data = json.JSONDecoder().decode(data)[1]['payload']['data']
187 data = base64.standard_b64decode(data)
188 return json.JSONDecoder().decode(data)
191 def __tranform_to_dash(self, manifest):
193 self.save_file('/home/johannes/manifest.json', json.dumps(manifest))
194 manifest = manifest['result']['viewables'][0]
196 self.last_playback_context = manifest['playbackContextId']
197 self.last_drm_context = manifest['drmContextId']
201 if 'psshb64' in manifest:
202 if len(manifest['psshb64']) >= 1:
203 pssh = manifest['psshb64'][0]
207 root = ET.Element('MPD')
208 root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
209 root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
212 seconds = manifest['runtime']/1000
213 duration = "PT"+str(seconds)+".00S"
215 period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
217 # One Adaption Set for Video
218 for video_track in manifest['videoTracks']:
219 video_adaption_set = ET.SubElement(period, 'AdaptationSet', mimeType='video/mp4', contentType="video")
221 protection = ET.SubElement(video_adaption_set, 'ContentProtection',
222 schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED')
224 ET.SubElement(protection, 'cenc:pssh').text = pssh
226 for downloadable in video_track['downloadables']:
227 rep = ET.SubElement(video_adaption_set, 'Representation',
228 width=str(downloadable['width']),
229 height=str(downloadable['height']),
230 bitrate=str(downloadable['bitrate']*8*1024),
231 mimeType='video/mp4')
234 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
235 # Init an Segment block
236 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
237 ET.SubElement(segment_base, 'Initialization', range='0-60000')
241 # Multiple Adaption Set for audio
242 for audio_track in manifest['audioTracks']:
243 audio_adaption_set = ET.SubElement(period, 'AdaptationSet',
244 lang=audio_track['bcp47'],
246 mimeType='audio/mp4')
247 for downloadable in audio_track['downloadables']:
248 rep = ET.SubElement(audio_adaption_set, 'Representation',
250 bitrate=str(downloadable['bitrate'] * 8 * 1024),
251 mimeType='audio/mp4')
254 ET.SubElement(rep, 'AudioChannelConfiguration',
255 schemeIdUri='urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
256 value=str(audio_track['channelsCount']))
259 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
261 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
262 ET.SubElement(segment_base, 'Initialization', range='0-60000')
265 xml = ET.tostring(root, encoding='utf-8', method='xml')
266 xml = xml.replace('\n', '').replace('\r', '')
269 def __get_base_url(self, urls):
273 def __parse_chunked_msl_response(self, message):
281 while i < len(message):
282 if message[i] == '{':
283 opencount = opencount + 1
284 if message[i] == '}':
285 closecount = closecount + 1
286 if opencount == closecount:
291 payloads.append(message[old_end:i + 1])
299 def __generate_msl_request_data(self, data):
300 header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
302 'headerdata': base64.standard_b64encode(header_encryption_envelope),
303 'signature': self.__sign(header_encryption_envelope),
304 'mastertoken': self.mastertoken,
307 # Serialize the given Data
308 serialized_data = json.dumps(data)
309 serialized_data = serialized_data.replace('"', '\\"')
310 serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-11","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
312 compressed_data = self.__compress_data(serialized_data)
314 # Create FIRST Payload Chunks
316 "messageid": self.current_message_id,
317 "data": compressed_data,
318 "compressionalgo": "GZIP",
322 first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
323 first_payload_chunk = {
324 'payload': base64.standard_b64encode(first_payload_encryption_envelope),
325 'signature': self.__sign(first_payload_encryption_envelope),
329 # Create Second Payload
331 "messageid": self.current_message_id,
336 second_payload_encryption_envelope = self.__encrypt(json.dumps(second_payload))
337 second_payload_chunk = {
338 'payload': base64.standard_b64encode(second_payload_encryption_envelope),
339 'signature': base64.standard_b64encode(self.__sign(second_payload_encryption_envelope)),
342 request_data = json.dumps(header) + json.dumps(first_payload_chunk) # + json.dumps(second_payload_chunk)
347 def __compress_data(self, data):
350 with gzip.GzipFile(fileobj=out, mode="w") as f:
352 return base64.standard_b64encode(out.getvalue())
355 def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
357 Function that generates a MSL header dict
358 :return: The base64 encoded JSON String of the header
360 self.current_message_id = self.rndm.randint(0, pow(2, 52))
364 'handshake': is_handshake,
365 'nonreplayable': False,
367 'languages': ["en-US"],
368 'compressionalgos': []
370 'recipient': 'Netflix',
372 'messageid': self.current_message_id,
373 'timestamp': 1467733923
376 # Add compression algo if not empty
377 if compressionalgo is not "":
378 header_data['capabilities']['compressionalgos'].append(compressionalgo)
380 # If this is a keyrequest act diffrent then other requests
382 public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER'))
383 header_data['keyrequestdata'] = [{
384 'scheme': 'ASYMMETRIC_WRAPPED',
386 'publickey': public_key,
387 'mechanism': 'JWK_RSA',
388 'keypairid': 'superKeyPair'
392 if 'usertoken' in self.tokens:
395 # Auth via email and password
396 header_data['userauthdata'] = {
397 'scheme': 'EMAIL_PASSWORD',
400 'password': self.password
404 return json.dumps(header_data)
408 def __encrypt(self, plaintext):
410 Encrypt the given Plaintext with the encryption key
412 :return: Serialized JSON String of the encryption Envelope
414 iv = get_random_bytes(16)
415 encryption_envelope = {
417 'keyid': self.esn + '_' + str(self.sequence_number),
419 'iv': base64.standard_b64encode(iv)
422 plaintext = Padding.pad(plaintext, 16)
424 cipher = AES.new(self.encryption_key, AES.MODE_CBC, iv)
425 ciphertext = cipher.encrypt(plaintext)
426 encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
427 return json.dumps(encryption_envelope)
429 def __sign(self, text):
430 #signature = hmac.new(self.sign_key, text, hashlib.sha256).digest()
431 signature = HMAC(self.sign_key, text, hashlib.sha256).digest()
434 # hmac = HMAC.new(self.sign_key, digestmod=SHA256)
436 return base64.standard_b64encode(signature)
439 def __perform_key_handshake(self):
441 header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
449 'headerdata': base64.standard_b64encode(header),
452 log('Key Handshake Request:')
453 log(json.dumps(request))
456 resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
457 if resp.status_code == 200:
459 if 'errordata' in resp:
460 log('Key Exchange failed')
461 log(base64.standard_b64decode(resp['errordata']))
463 self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
465 log('Key Exchange failed')
468 def __parse_crypto_keys(self, headerdata):
469 self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
471 encrypted_encryption_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['encryptionkey'])
472 encrypted_sign_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['hmackey'])
473 cipher_rsa = PKCS1_OAEP.new(self.rsa_key)
475 # Decrypt encryption key
476 encryption_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_encryption_key))
477 self.encryption_key = base64key_decode(encryption_key_data['k'])
480 sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key))
481 self.sign_key = base64key_decode(sign_key_data['k'])
483 self.__save_msl_data()
484 self.handshake_performed = True
486 def __load_msl_data(self):
487 msl_data = json.JSONDecoder().decode(self.load_file('msl_data.json'))
488 self.__set_master_token(msl_data['tokens']['mastertoken'])
489 self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
490 self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
492 def __save_msl_data(self):
494 Saves the keys and tokens in json file
498 "encryption_key": base64.standard_b64encode(self.encryption_key),
499 'sign_key': base64.standard_b64encode(self.sign_key),
501 'mastertoken': self.mastertoken
504 serialized_data = json.JSONEncoder().encode(data)
505 self.save_file('msl_data.json', serialized_data)
507 def __set_master_token(self, master_token):
508 self.mastertoken = master_token
509 self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))[
512 def __load_rsa_keys(self):
513 loaded_key = self.load_file('rsa_key.bin')
514 self.rsa_key = RSA.importKey(loaded_key)
516 def __save_rsa_keys(self):
518 # Get the DER Base64 of the keys
519 encrypted_key = self.rsa_key.exportKey()
520 self.save_file('rsa_key.bin', encrypted_key)
523 def file_exists(filename):
525 Checks if a given file exists
526 :param filename: The filename
529 return os.path.isfile(filename)
532 def save_file(filename, content):
534 Saves the given content under given filename
535 :param filename: The filename
536 :param content: The content of the file
538 with open(filename, 'w') as file_:
543 def load_file(filename):
545 Loads the content of a given filename
546 :param filename: The file to load
547 :return: The content of the file
549 with open(filename) as file_:
550 file_content = file_.read()