feat(init): Repository init
[plugin.video.netflix.git] / resources / lib / MSL.py
1 import base64
2 import gzip
3 import json
4 import os
5 import pprint
6 import random
7 from StringIO import StringIO
8 from hmac import HMAC
9 import hashlib
10 import requests
11 import zlib
12
13 import time
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
21 from lib import log
22
23 pp = pprint.PrettyPrinter(indent=4)
24
25 def base64key_decode(payload):
26     l = len(payload) % 4
27     if l == 2:
28         payload += '=='
29     elif l == 3:
30         payload += '='
31     elif l != 0:
32         raise ValueError('Invalid base64 string')
33     return base64.urlsafe_b64decode(payload.encode('utf-8'))
34
35
36 class MSL:
37     handshake_performed = False  # Is a handshake already performed and the keys loaded
38     last_drm_context = ''
39     last_playback_context = ''
40     esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
41     current_message_id = 0
42     session = requests.session()
43     rndm = random.SystemRandom()
44     tokens = []
45     endpoints = {
46         'manifest': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/manifest',
47         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
48     }
49
50     def __init__(self, email, password):
51         """
52         The Constructor checks for already existing crypto Keys.
53         If they exist it will load the existing keys
54         """
55         self.email = email
56         self.password = password
57
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()
65         else:
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()
71
72     def load_manifest(self, viewable_id):
73         manifest_request_data = {
74             'method': 'manifest',
75             'lookupType': 'PREPARE',
76             'viewableIds': [viewable_id],
77             'profiles': [
78                 'playready-h264mpl30-dash',
79                 'playready-h264mpl31-dash',
80                 'heaac-2-dash',
81                 'dfxp-ls-sdh',
82                 'simplesdh',
83                 'nflx-cmisc',
84                 'BIF240',
85                 'BIF320'
86             ],
87             'drmSystem': 'widevine',
88             'appId': '14673889385265',
89             'sessionParams': {
90                 'pinCapableClient': False,
91                 'uiplaycontext': 'null'
92             },
93             'sessionId': '14673889385265',
94             'trackId': 0,
95             'flavor': 'PRE_FETCH',
96             'secureUrls': False,
97             'supportPreviewContent': True,
98             'forceClearStreams': False,
99             'languages': ['de-DE'],
100             'clientVersion': '4.0004.899.011',
101             'uiVersion': 'akira'
102         }
103         request_data = self.__generate_msl_request_data(manifest_request_data)
104
105         resp = self.session.post(self.endpoints['manifest'], request_data)
106
107
108         try:
109             resp.json()
110             log('MANIFEST RESPONE JSON: '+resp.text)
111         except ValueError:
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)
117
118
119     def get_license(self, challenge, sid):
120
121         """
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);
129
130         :param viewable_id:
131         :param challenge:
132         :param kid:
133         :return:
134         """
135
136         license_request_data = {
137             'method': 'license',
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],
144             'challenges': [{
145                 'dataBase64': challenge,
146                 'sessionId': sid
147             }],
148             'clientTime': int(time.time()),
149             'xid': int((int(time.time()) + 0.1612) * 1000)
150
151         }
152         request_data = self.__generate_msl_request_data(license_request_data)
153
154         resp = self.session.post(self.endpoints['license'], request_data)
155
156         try:
157             resp.json()
158             log('LICENSE RESPONE JSON: '+resp.text)
159         except ValueError:
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']
166             else:
167                 return ''
168
169
170     def __decrypt_payload_chunk(self, payloadchunk):
171         payloadchunk = json.JSONDecoder().decode(payloadchunk)
172         encryption_envelope = json.JSONDecoder().decode(base64.standard_b64decode(payloadchunk['payload']))
173         # Decrypt the text
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']
179
180         # uncompress data if compressed
181         if plaintext['compressionalgo'] == 'GZIP':
182             data = zlib.decompress(base64.standard_b64decode(data), 16 + zlib.MAX_WBITS)
183         else:
184             data = base64.standard_b64decode(data)
185
186         data = json.JSONDecoder().decode(data)[1]['payload']['data']
187         data = base64.standard_b64decode(data)
188         return json.JSONDecoder().decode(data)
189
190
191     def __tranform_to_dash(self, manifest):
192
193         self.save_file('/home/johannes/manifest.json', json.dumps(manifest))
194         manifest = manifest['result']['viewables'][0]
195
196         self.last_playback_context = manifest['playbackContextId']
197         self.last_drm_context = manifest['drmContextId']
198
199         #Check for pssh
200         pssh = ''
201         if 'psshb64' in manifest:
202             if len(manifest['psshb64']) >= 1:
203                 pssh = manifest['psshb64'][0]
204
205
206
207         root = ET.Element('MPD')
208         root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
209         root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
210
211
212         seconds = manifest['runtime']/1000
213         duration = "PT"+str(seconds)+".00S"
214
215         period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
216
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")
220             # Content Protection
221             protection = ET.SubElement(video_adaption_set, 'ContentProtection',
222                           schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED')
223             if pssh is not '':
224                 ET.SubElement(protection, 'cenc:pssh').text = pssh
225
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')
232
233                 #BaseURL
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')
238
239
240
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'],
245                                                contentType='audio',
246                                                mimeType='audio/mp4')
247             for downloadable in audio_track['downloadables']:
248                 rep = ET.SubElement(audio_adaption_set, 'Representation',
249                                     codecs='aac',
250                                     bitrate=str(downloadable['bitrate'] * 8 * 1024),
251                                     mimeType='audio/mp4')
252
253                 #AudioChannel Config
254                 ET.SubElement(rep, 'AudioChannelConfiguration',
255                               schemeIdUri='urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
256                               value=str(audio_track['channelsCount']))
257
258                 #BaseURL
259                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
260                 # Index range
261                 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
262                 ET.SubElement(segment_base, 'Initialization', range='0-60000')
263
264
265         xml = ET.tostring(root, encoding='utf-8', method='xml')
266         xml = xml.replace('\n', '').replace('\r', '')
267         return xml
268
269     def __get_base_url(self, urls):
270         for key in urls:
271             return urls[key]
272
273     def __parse_chunked_msl_response(self, message):
274         i = 0
275         opencount = 0
276         closecount = 0
277         header = ""
278         payloads = []
279         old_end = 0
280
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:
287                 if header == "":
288                     header = message[:i]
289                     old_end = i + 1
290                 else:
291                     payloads.append(message[old_end:i + 1])
292             i += 1
293
294         return {
295             'header': header,
296             'payloads': payloads
297         }
298
299     def __generate_msl_request_data(self, data):
300         header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
301         header = {
302             'headerdata': base64.standard_b64encode(header_encryption_envelope),
303             'signature': self.__sign(header_encryption_envelope),
304             'mastertoken': self.mastertoken,
305         }
306
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'
311
312         compressed_data = self.__compress_data(serialized_data)
313
314         # Create FIRST Payload Chunks
315         first_payload = {
316             "messageid": self.current_message_id,
317             "data": compressed_data,
318             "compressionalgo": "GZIP",
319             "sequencenumber": 1,
320             "endofmsg": True
321         }
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),
326         }
327
328
329         # Create Second Payload
330         second_payload = {
331             "messageid": self.current_message_id,
332             "data": "",
333             "endofmsg": True,
334             "sequencenumber": 2
335         }
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)),
340         }
341
342         request_data = json.dumps(header) + json.dumps(first_payload_chunk) # + json.dumps(second_payload_chunk)
343         return request_data
344
345
346
347     def __compress_data(self, data):
348         # GZIP THE DATA
349         out = StringIO()
350         with gzip.GzipFile(fileobj=out, mode="w") as f:
351             f.write(data)
352         return base64.standard_b64encode(out.getvalue())
353
354
355     def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
356         """
357         Function that generates a MSL header dict
358         :return: The base64 encoded JSON String of the header
359         """
360         self.current_message_id = self.rndm.randint(0, pow(2, 52))
361
362         header_data = {
363             'sender': self.esn,
364             'handshake': is_handshake,
365             'nonreplayable': False,
366             'capabilities': {
367                 'languages': ["en-US"],
368                 'compressionalgos': []
369             },
370             'recipient': 'Netflix',
371             'renewable': True,
372             'messageid': self.current_message_id,
373             'timestamp': 1467733923
374         }
375
376         # Add compression algo if not empty
377         if compressionalgo is not "":
378             header_data['capabilities']['compressionalgos'].append(compressionalgo)
379
380         # If this is a keyrequest act diffrent then other requests
381         if is_key_request:
382             public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER'))
383             header_data['keyrequestdata'] = [{
384                 'scheme': 'ASYMMETRIC_WRAPPED',
385                 'keydata': {
386                     'publickey': public_key,
387                     'mechanism': 'JWK_RSA',
388                     'keypairid': 'superKeyPair'
389                 }
390             }]
391         else:
392             if 'usertoken' in self.tokens:
393                 pass
394             else:
395                 # Auth via email and password
396                 header_data['userauthdata'] = {
397                     'scheme': 'EMAIL_PASSWORD',
398                     'authdata': {
399                         'email': self.email,
400                         'password': self.password
401                     }
402                 }
403
404         return json.dumps(header_data)
405
406
407
408     def __encrypt(self, plaintext):
409         """
410         Encrypt the given Plaintext with the encryption key
411         :param plaintext:
412         :return: Serialized JSON String of the encryption Envelope
413         """
414         iv = get_random_bytes(16)
415         encryption_envelope = {
416             'ciphertext': '',
417             'keyid': self.esn + '_' + str(self.sequence_number),
418             'sha256': 'AA==',
419             'iv': base64.standard_b64encode(iv)
420         }
421         # Padd the plaintext
422         plaintext = Padding.pad(plaintext, 16)
423         # Encrypt the text
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)
428
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()
432
433
434         # hmac = HMAC.new(self.sign_key, digestmod=SHA256)
435         # hmac.update(text)
436         return base64.standard_b64encode(signature)
437
438
439     def __perform_key_handshake(self):
440
441         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
442         request = {
443             'entityauthdata': {
444                 'scheme': 'NONE',
445                 'authdata': {
446                     'identity': self.esn
447                 }
448             },
449             'headerdata': base64.standard_b64encode(header),
450             'signature': '',
451         }
452         log('Key Handshake Request:')
453         log(json.dumps(request))
454
455
456         resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
457         if resp.status_code == 200:
458             resp = resp.json()
459             if 'errordata' in resp:
460                 log('Key Exchange failed')
461                 log(base64.standard_b64decode(resp['errordata']))
462                 return False
463             self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
464         else:
465             log('Key Exchange failed')
466             log(resp.text)
467
468     def __parse_crypto_keys(self, headerdata):
469         self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
470         # Init Decryption
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)
474
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'])
478
479         # Decrypt sign key
480         sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key))
481         self.sign_key = base64key_decode(sign_key_data['k'])
482
483         self.__save_msl_data()
484         self.handshake_performed = True
485
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'])
491
492     def __save_msl_data(self):
493         """
494         Saves the keys and tokens in json file
495         :return:
496         """
497         data = {
498             "encryption_key": base64.standard_b64encode(self.encryption_key),
499             'sign_key': base64.standard_b64encode(self.sign_key),
500             'tokens': {
501                 'mastertoken': self.mastertoken
502             }
503         }
504         serialized_data = json.JSONEncoder().encode(data)
505         self.save_file('msl_data.json', serialized_data)
506
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']))[
510             'sequencenumber']
511
512     def __load_rsa_keys(self):
513         loaded_key = self.load_file('rsa_key.bin')
514         self.rsa_key = RSA.importKey(loaded_key)
515
516     def __save_rsa_keys(self):
517         log('Save RSA Keys')
518         # Get the DER Base64 of the keys
519         encrypted_key = self.rsa_key.exportKey()
520         self.save_file('rsa_key.bin', encrypted_key)
521
522     @staticmethod
523     def file_exists(filename):
524         """
525         Checks if a given file exists
526         :param filename: The filename
527         :return: True if so
528         """
529         return os.path.isfile(filename)
530
531     @staticmethod
532     def save_file(filename, content):
533         """
534         Saves the given content under given filename
535         :param filename: The filename
536         :param content: The content of the file
537         """
538         with open(filename, 'w') as file_:
539             file_.write(content)
540             file_.flush()
541
542     @staticmethod
543     def load_file(filename):
544         """
545         Loads the content of a given filename
546         :param filename: The file to load
547         :return: The content of the file
548         """
549         with open(filename) as file_:
550             file_content = file_.read()
551         return file_content