ef7e93be5a6c20ba6ab7ce85881eb09bfa009dc3
[plugin.video.netflix.git] / resources / lib / MSL.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # Module: MSL
4 # Created on: 26.01.2017
5
6 import base64
7 import gzip
8 import json
9 import os
10 import pprint
11 import random
12 from StringIO import StringIO
13
14 from datetime import datetime
15 import requests
16 import zlib
17
18 import time
19 from Cryptodome.PublicKey import RSA
20 from Cryptodome.Cipher import PKCS1_OAEP
21 from Cryptodome.Cipher import AES
22 from Cryptodome.Random import get_random_bytes
23 from Cryptodome.Hash import HMAC, SHA256
24 from Cryptodome.Util import Padding
25 import xml.etree.ElementTree as ET
26
27 pp = pprint.PrettyPrinter(indent=4)
28
29 def base64key_decode(payload):
30     l = len(payload) % 4
31     if l == 2:
32         payload += '=='
33     elif l == 3:
34         payload += '='
35     elif l != 0:
36         raise ValueError('Invalid base64 string')
37     return base64.urlsafe_b64decode(payload.encode('utf-8'))
38
39
40 class MSL:
41     handshake_performed = False  # Is a handshake already performed and the keys loaded
42     last_drm_context = ''
43     last_playback_context = ''
44     #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
45     esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
46     #esn = "NFCDIE-02-DCH84Q2EK3N6VFVQJ0NLRQ27498N0F"
47     current_message_id = 0
48     session = requests.session()
49     rndm = random.SystemRandom()
50     tokens = []
51     endpoints = {
52         'manifest': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/manifest',
53         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
54     }
55
56     def __init__(self, kodi_helper):
57         """
58         The Constructor checks for already existing crypto Keys.
59         If they exist it will load the existing keys
60         """
61         self.kodi_helper = kodi_helper
62         try:
63             os.mkdir(self.kodi_helper.msl_data_path)
64         except OSError:
65             pass
66
67         if self.file_exists(self.kodi_helper.msl_data_path, 'msl_data.json'):
68             self.kodi_helper.log(msg='MSL Data exists. Use old Tokens.')
69             self.__load_msl_data()
70             self.handshake_performed = True
71         elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
72             self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
73             self.__load_rsa_keys()
74             self.__perform_key_handshake()
75         else:
76             self.kodi_helper.log(msg='Create new RSA Keys')
77             # Create new Key Pair and save
78             self.rsa_key = RSA.generate(2048)
79             self.__save_rsa_keys()
80             self.__perform_key_handshake()
81
82     def load_manifest(self, viewable_id):
83         """
84         Loads the manifets for the given viewable_id and returns a mpd-XML-Manifest
85         :param viewable_id: The id of of the viewable
86         :return: MPD XML Manifest or False if no success
87         """
88         manifest_request_data = {
89             'method': 'manifest',
90             'lookupType': 'PREPARE',
91             'viewableIds': [viewable_id],
92             'profiles': [
93                 "playready-h264bpl30-dash",
94                 "playready-h264mpl30-dash",
95                 "playready-h264mpl31-dash",
96                 "playready-h264mpl40-dash",
97                 # "hevc-main-L30-dash-cenc",
98                 # "hevc-main-L31-dash-cenc",
99                 # "hevc-main-L40-dash-cenc",
100                 # "hevc-main-L41-dash-cenc",
101                 # "hevc-main-L50-dash-cenc",
102                 # "hevc-main-L51-dash-cenc",
103                 # "hevc-main10-L30-dash-cenc",
104                 # "hevc-main10-L31-dash-cenc",
105                 # "hevc-main10-L40-dash-cenc",
106                 # "hevc-main10-L41-dash-cenc",
107                 # "hevc-main10-L50-dash-cenc",
108                 # "hevc-main10-L51-dash-cenc",
109                 # "hevc-main10-L30-dash-cenc-prk",
110                 # "hevc-main10-L31-dash-cenc-prk",
111                 # "hevc-main10-L40-dash-cenc-prk",
112                 # "hevc-main10-L41-dash-cenc-prk",
113                 # "hevc-main-L30-L31-dash-cenc-tl",
114                 # "hevc-main-L31-L40-dash-cenc-tl",
115                 # "hevc-main-L40-L41-dash-cenc-tl",
116                 # "hevc-main-L50-L51-dash-cenc-tl",
117                 # "hevc-main10-L30-L31-dash-cenc-tl",
118                 # "hevc-main10-L31-L40-dash-cenc-tl",
119                 # "hevc-main10-L40-L41-dash-cenc-tl",
120                 # "hevc-main10-L50-L51-dash-cenc-tl",
121                 # "hevc-dv-main10-L30-dash-cenc",
122                 # "hevc-dv-main10-L31-dash-cenc",
123                 # "hevc-dv-main10-L40-dash-cenc",
124                 # "hevc-dv-main10-L41-dash-cenc",
125                 # "hevc-dv-main10-L50-dash-cenc",
126                 # "hevc-dv-main10-L51-dash-cenc",
127                 # "hevc-dv5-main10-L30-dash-cenc-prk",
128                 # "hevc-dv5-main10-L31-dash-cenc-prk",
129                 # "hevc-dv5-main10-L40-dash-cenc-prk",
130                 # "hevc-dv5-main10-L41-dash-cenc-prk",
131                 # "hevc-dv5-main10-L50-dash-cenc-prk",
132                 # "hevc-dv5-main10-L51-dash-cenc-prk",
133                 # "hevc-hdr-main10-L30-dash-cenc",
134                 # "hevc-hdr-main10-L31-dash-cenc",
135                 # "hevc-hdr-main10-L40-dash-cenc",
136                 # "hevc-hdr-main10-L41-dash-cenc",
137                 # "hevc-hdr-main10-L50-dash-cenc",
138                 # "hevc-hdr-main10-L51-dash-cenc",
139                 # "hevc-hdr-main10-L30-dash-cenc-prk",
140                 # "hevc-hdr-main10-L31-dash-cenc-prk",
141                 # "hevc-hdr-main10-L40-dash-cenc-prk",
142                 # "hevc-hdr-main10-L41-dash-cenc-prk",
143                 # "hevc-hdr-main10-L50-dash-cenc-prk",
144                 # "hevc-hdr-main10-L51-dash-cenc-prk"
145
146                # 'playready-h264mpl30-dash',
147                 #'playready-h264mpl31-dash',
148                 #'playready-h264mpl40-dash',
149                 #'hevc-main10-L41-dash-cenc',
150                 #'hevc-main10-L50-dash-cenc',
151                 #'hevc-main10-L51-dash-cenc',
152
153
154
155                 # Audio
156                 'heaac-2-dash',
157
158                 #subtiltes
159                 'dfxp-ls-sdh',
160                 #'simplesdh',
161                 #'nflx-cmisc',
162
163                 #unkown
164                 'BIF240',
165                 'BIF320'
166             ],
167             'drmSystem': 'widevine',
168             'appId': '14673889385265',
169             'sessionParams': {
170                 'pinCapableClient': False,
171                 'uiplaycontext': 'null'
172             },
173             'sessionId': '14673889385265',
174             'trackId': 0,
175             'flavor': 'PRE_FETCH',
176             'secureUrls': False,
177             'supportPreviewContent': True,
178             'forceClearStreams': False,
179             'languages': ['de-DE'],
180             'clientVersion': '4.0004.899.011',
181             'uiVersion': 'akira'
182         }
183
184         # Check if dolby sound is enabled and add to profles
185         if self.kodi_helper.get_dolby_setting():
186             manifest_request_data['profiles'].append('ddplus-2.0-dash')
187             manifest_request_data['profiles'].append('ddplus-5.1-dash')
188
189         request_data = self.__generate_msl_request_data(manifest_request_data)
190         resp = self.session.post(self.endpoints['manifest'], request_data)
191
192         try:
193             # if the json() does not fail we have an error because the manifest response is a chuncked json response
194             resp.json()
195             self.kodi_helper.log(msg='Error getting Manifest: '+resp.text)
196             return False
197         except ValueError:
198             # json() failed so parse the chunked response
199             self.kodi_helper.log(msg='Got chunked Manifest Response: ' + resp.text)
200             resp = self.__parse_chunked_msl_response(resp.text)
201             self.kodi_helper.log(msg='Parsed chunked Response: ' + json.dumps(resp))
202             data = self.__decrypt_payload_chunk(resp['payloads'][0])
203             return self.__tranform_to_dash(data)
204
205     def get_license(self, challenge, sid):
206         """
207         Requests and returns a license for the given challenge and sid
208         :param challenge: The base64 encoded challenge
209         :param sid: The sid paired to the challengew
210         :return: Base64 representation of the license key or False if no success
211         """
212         license_request_data = {
213             'method': 'license',
214             'licenseType': 'STANDARD',
215             'clientVersion': '4.0004.899.011',
216             'uiVersion': 'akira',
217             'languages': ['de-DE'],
218             'playbackContextId': self.last_playback_context,
219             'drmContextIds': [self.last_drm_context],
220             'challenges': [{
221                 'dataBase64': challenge,
222                 'sessionId': sid
223             }],
224             'clientTime': int(time.time()),
225             'xid': int((int(time.time()) + 0.1612) * 1000)
226
227         }
228         request_data = self.__generate_msl_request_data(license_request_data)
229
230         resp = self.session.post(self.endpoints['license'], request_data)
231
232         try:
233             # If is valid json the request for the licnese failed
234             resp.json()
235             self.kodi_helper.log(msg='Error getting license: '+resp.text)
236             return False
237         except ValueError:
238             # json() failed so we have a chunked json response
239             resp = self.__parse_chunked_msl_response(resp.text)
240             data = self.__decrypt_payload_chunk(resp['payloads'][0])
241             if data['success'] is True:
242                 return data['result']['licenses'][0]['data']
243             else:
244                 self.kodi_helper.log(msg='Error getting license: ' + json.dumps(data))
245                 return False
246
247     def __decrypt_payload_chunk(self, payloadchunk):
248         payloadchunk = json.JSONDecoder().decode(payloadchunk)
249         encryption_envelope = json.JSONDecoder().decode(base64.standard_b64decode(payloadchunk['payload']))
250         # Decrypt the text
251         cipher = AES.new(self.encryption_key, AES.MODE_CBC, base64.standard_b64decode(encryption_envelope['iv']))
252         plaintext = cipher.decrypt(base64.standard_b64decode(encryption_envelope['ciphertext']))
253         # unpad the plaintext
254         plaintext = json.JSONDecoder().decode(Padding.unpad(plaintext, 16))
255         data = plaintext['data']
256
257         # uncompress data if compressed
258         if plaintext['compressionalgo'] == 'GZIP':
259             data = zlib.decompress(base64.standard_b64decode(data), 16 + zlib.MAX_WBITS)
260         else:
261             data = base64.standard_b64decode(data)
262
263         data = json.JSONDecoder().decode(data)[1]['payload']['data']
264         data = base64.standard_b64decode(data)
265         return json.JSONDecoder().decode(data)
266
267
268     def __tranform_to_dash(self, manifest):
269
270         self.save_file(self.kodi_helper.msl_data_path, 'manifest.json', json.dumps(manifest))
271         manifest = manifest['result']['viewables'][0]
272
273         self.last_playback_context = manifest['playbackContextId']
274         self.last_drm_context = manifest['drmContextId']
275
276         #Check for pssh
277         pssh = ''
278         if 'psshb64' in manifest:
279             if len(manifest['psshb64']) >= 1:
280                 pssh = manifest['psshb64'][0]
281
282         seconds = manifest['runtime']/1000
283         init_length = seconds / 2 * 12 + 20*1000
284         duration = "PT"+str(seconds)+".00S"
285
286         root = ET.Element('MPD')
287         root.attrib['xmlns'] = 'urn:mpeg:dash:schema:mpd:2011'
288         root.attrib['xmlns:cenc'] = 'urn:mpeg:cenc:2013'
289         root.attrib['mediaPresentationDuration'] = duration
290
291         period = ET.SubElement(root, 'Period', start='PT0S', duration=duration)
292
293         # One Adaption Set for Video
294         for video_track in manifest['videoTracks']:
295             video_adaption_set = ET.SubElement(period, 'AdaptationSet', mimeType='video/mp4', contentType="video")
296             # Content Protection
297             protection = ET.SubElement(video_adaption_set, 'ContentProtection',
298                           schemeIdUri='urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED')
299             if pssh is not '':
300                 ET.SubElement(protection, 'cenc:pssh').text = pssh
301
302             for downloadable in video_track['downloadables']:
303
304                 codec = 'h264'
305                 if 'hevc' in downloadable['contentProfile']:
306                     codec = 'hevc'
307
308                 hdcp_versions = '0.0'
309                 for hdcp in downloadable['hdcpVersions']:
310                     if hdcp != 'none':
311                         hdcp_versions = hdcp
312
313                 rep = ET.SubElement(video_adaption_set, 'Representation',
314                                     width=str(downloadable['width']),
315                                     height=str(downloadable['height']),
316                                     bandwidth=str(downloadable['bitrate']*1024),
317                                     hdcp=hdcp_versions,
318                                     nflxContentProfile=str(downloadable['contentProfile']),
319                                     codecs=codec,
320                                     mimeType='video/mp4')
321
322                 #BaseURL
323                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
324                 # Init an Segment block
325                 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
326                 ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
327
328
329
330         # Multiple Adaption Set for audio
331         for audio_track in manifest['audioTracks']:
332             audio_adaption_set = ET.SubElement(period, 'AdaptationSet',
333                                                lang=audio_track['bcp47'],
334                                                contentType='audio',
335                                                mimeType='audio/mp4')
336             for downloadable in audio_track['downloadables']:
337                 codec = 'aac'
338                 print downloadable
339                 if downloadable['contentProfile'] == 'ddplus-2.0-dash' or downloadable['contentProfile'] == 'ddplus-5.1-dash':
340                     codec = 'ec-3'
341                 print "codec is: " + codec
342                 rep = ET.SubElement(audio_adaption_set, 'Representation',
343                                     codecs=codec,
344                                     bandwidth=str(downloadable['bitrate']*1024),
345                                     mimeType='audio/mp4')
346
347                 #AudioChannel Config
348                 ET.SubElement(rep, 'AudioChannelConfiguration',
349                               schemeIdUri='urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
350                               value=str(audio_track['channelsCount']))
351
352                 #BaseURL
353                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
354                 # Index range
355                 segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
356                 ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
357
358         # Multiple Adaption Sets for subtiles
359         for text_track in manifest['textTracks']:
360             if 'downloadables' not in text_track or text_track['downloadables'] is None:
361                 continue
362             subtiles_adaption_set = ET.SubElement(period, 'AdaptationSet',
363                                                   lang=text_track['bcp47'],
364                                                   codecs='stpp',
365                                                   contentType='text',
366                                                   mimeType='application/ttml+xml')
367             for downloadable in text_track['downloadables']:
368                 rep = ET.SubElement(subtiles_adaption_set, 'Representation',
369                                     nflxProfile=downloadable['contentProfile']
370                                     )
371                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
372
373
374         xml = ET.tostring(root, encoding='utf-8', method='xml')
375         xml = xml.replace('\n', '').replace('\r', '')
376         return xml
377
378     def __get_base_url(self, urls):
379         for key in urls:
380             return urls[key]
381
382     def __parse_chunked_msl_response(self, message):
383         i = 0
384         opencount = 0
385         closecount = 0
386         header = ""
387         payloads = []
388         old_end = 0
389
390         while i < len(message):
391             if message[i] == '{':
392                 opencount = opencount + 1
393             if message[i] == '}':
394                 closecount = closecount + 1
395             if opencount == closecount:
396                 if header == "":
397                     header = message[:i]
398                     old_end = i + 1
399                 else:
400                     payloads.append(message[old_end:i + 1])
401             i += 1
402
403         return {
404             'header': header,
405             'payloads': payloads
406         }
407
408     def __generate_msl_request_data(self, data):
409         header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
410         header = {
411             'headerdata': base64.standard_b64encode(header_encryption_envelope),
412             'signature': self.__sign(header_encryption_envelope),
413             'mastertoken': self.mastertoken,
414         }
415
416         # Serialize the given Data
417         serialized_data = json.dumps(data)
418         serialized_data = serialized_data.replace('"', '\\"')
419         serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
420
421         compressed_data = self.__compress_data(serialized_data)
422
423         # Create FIRST Payload Chunks
424         first_payload = {
425             "messageid": self.current_message_id,
426             "data": compressed_data,
427             "compressionalgo": "GZIP",
428             "sequencenumber": 1,
429             "endofmsg": True
430         }
431         first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
432         first_payload_chunk = {
433             'payload': base64.standard_b64encode(first_payload_encryption_envelope),
434             'signature': self.__sign(first_payload_encryption_envelope),
435         }
436
437         request_data = json.dumps(header) + json.dumps(first_payload_chunk)
438         return request_data
439
440
441
442     def __compress_data(self, data):
443         # GZIP THE DATA
444         out = StringIO()
445         with gzip.GzipFile(fileobj=out, mode="w") as f:
446             f.write(data)
447         return base64.standard_b64encode(out.getvalue())
448
449
450     def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
451         """
452         Function that generates a MSL header dict
453         :return: The base64 encoded JSON String of the header
454         """
455         self.current_message_id = self.rndm.randint(0, pow(2, 52))
456
457         header_data = {
458             'sender': self.esn,
459             'handshake': is_handshake,
460             'nonreplayable': False,
461             'capabilities': {
462                 'languages': ["en-US"],
463                 'compressionalgos': []
464             },
465             'recipient': 'Netflix',
466             'renewable': True,
467             'messageid': self.current_message_id,
468             'timestamp': 1467733923
469         }
470
471         # Add compression algo if not empty
472         if compressionalgo is not "":
473             header_data['capabilities']['compressionalgos'].append(compressionalgo)
474
475         # If this is a keyrequest act diffrent then other requests
476         if is_key_request:
477             public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER'))
478             header_data['keyrequestdata'] = [{
479                 'scheme': 'ASYMMETRIC_WRAPPED',
480                 'keydata': {
481                     'publickey': public_key,
482                     'mechanism': 'JWK_RSA',
483                     'keypairid': 'superKeyPair'
484                 }
485             }]
486         else:
487             if 'usertoken' in self.tokens:
488                 pass
489             else:
490                 account = self.kodi_helper.get_credentials()
491                 # Auth via email and password
492                 header_data['userauthdata'] = {
493                     'scheme': 'EMAIL_PASSWORD',
494                     'authdata': {
495                         'email': account['email'],
496                         'password': account['password']
497                     }
498                 }
499
500         return json.dumps(header_data)
501
502
503
504     def __encrypt(self, plaintext):
505         """
506         Encrypt the given Plaintext with the encryption key
507         :param plaintext:
508         :return: Serialized JSON String of the encryption Envelope
509         """
510         iv = get_random_bytes(16)
511         encryption_envelope = {
512             'ciphertext': '',
513             'keyid': self.esn + '_' + str(self.sequence_number),
514             'sha256': 'AA==',
515             'iv': base64.standard_b64encode(iv)
516         }
517         # Padd the plaintext
518         plaintext = Padding.pad(plaintext, 16)
519         # Encrypt the text
520         cipher = AES.new(self.encryption_key, AES.MODE_CBC, iv)
521         ciphertext = cipher.encrypt(plaintext)
522         encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
523         return json.dumps(encryption_envelope)
524
525
526     def __sign(self, text):
527         """
528         Calculates the HMAC signature for the given text with the current sign key and SHA256
529         :param text:
530         :return: Base64 encoded signature
531         """
532         signature = HMAC.new(self.sign_key, text, SHA256).digest()
533         return base64.standard_b64encode(signature)
534
535
536     def __perform_key_handshake(self):
537         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
538         request = {
539             'entityauthdata': {
540                 'scheme': 'NONE',
541                 'authdata': {
542                     'identity': self.esn
543                 }
544             },
545             'headerdata': base64.standard_b64encode(header),
546             'signature': '',
547         }
548         self.kodi_helper.log(msg='Key Handshake Request:')
549         self.kodi_helper.log(msg=json.dumps(request))
550
551         resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
552         if resp.status_code == 200:
553             resp = resp.json()
554             if 'errordata' in resp:
555                 self.kodi_helper.log(msg='Key Exchange failed')
556                 self.kodi_helper.log(msg=base64.standard_b64decode(resp['errordata']))
557                 return False
558             self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
559         else:
560             self.kodi_helper.log(msg='Key Exchange failed')
561             self.kodi_helper.log(msg=resp.text)
562
563     def __parse_crypto_keys(self, headerdata):
564         self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
565         # Init Decryption
566         encrypted_encryption_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['encryptionkey'])
567         encrypted_sign_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['hmackey'])
568         cipher_rsa = PKCS1_OAEP.new(self.rsa_key)
569
570         # Decrypt encryption key
571         encryption_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_encryption_key))
572         self.encryption_key = base64key_decode(encryption_key_data['k'])
573
574         # Decrypt sign key
575         sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key))
576         self.sign_key = base64key_decode(sign_key_data['k'])
577
578         self.__save_msl_data()
579         self.handshake_performed = True
580
581     def __load_msl_data(self):
582         msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
583         #Check expire date of the token
584         master_token = json.JSONDecoder().decode(base64.standard_b64decode(msl_data['tokens']['mastertoken']['tokendata']))
585         valid_until = datetime.utcfromtimestamp(int(master_token['expiration']))
586         present = datetime.now()
587         difference = valid_until - present
588         difference = difference.total_seconds() / 60 / 60
589         # If token expires in less then 10 hours or is expires renew it
590         if difference < 10:
591             self.__load_rsa_keys()
592             self.__perform_key_handshake()
593             return
594
595         self.__set_master_token(msl_data['tokens']['mastertoken'])
596         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
597         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
598
599     def __save_msl_data(self):
600         """
601         Saves the keys and tokens in json file
602         :return:
603         """
604         data = {
605             "encryption_key": base64.standard_b64encode(self.encryption_key),
606             'sign_key': base64.standard_b64encode(self.sign_key),
607             'tokens': {
608                 'mastertoken': self.mastertoken
609             }
610         }
611         serialized_data = json.JSONEncoder().encode(data)
612         self.save_file(self.kodi_helper.msl_data_path, 'msl_data.json', serialized_data)
613
614     def __set_master_token(self, master_token):
615         self.mastertoken = master_token
616         self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))['sequencenumber']
617
618     def __load_rsa_keys(self):
619         loaded_key = self.load_file(self.kodi_helper.msl_data_path, 'rsa_key.bin')
620         self.rsa_key = RSA.importKey(loaded_key)
621
622     def __save_rsa_keys(self):
623         self.kodi_helper.log(msg='Save RSA Keys')
624         # Get the DER Base64 of the keys
625         encrypted_key = self.rsa_key.exportKey()
626         self.save_file(self.kodi_helper.msl_data_path, 'rsa_key.bin', encrypted_key)
627
628     @staticmethod
629     def file_exists(msl_data_path, filename):
630         """
631         Checks if a given file exists
632         :param filename: The filename
633         :return: True if so
634         """
635         return os.path.isfile(msl_data_path + filename)
636
637     @staticmethod
638     def save_file(msl_data_path, filename, content):
639         """
640         Saves the given content under given filename
641         :param filename: The filename
642         :param content: The content of the file
643         """
644         with open(msl_data_path + filename, 'w') as file_:
645             file_.write(content)
646             file_.flush()
647
648     @staticmethod
649     def load_file(msl_data_path, filename):
650         """
651         Loads the content of a given filename
652         :param filename: The file to load
653         :return: The content of the file
654         """
655         with open(msl_data_path + filename) as file_:
656             file_content = file_.read()
657         return file_content