a5de5384bf1f3cca7372e9a6dce21db64c1dc344
[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         self.__load_msl_data()
410         header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
411         header = {
412             'headerdata': base64.standard_b64encode(header_encryption_envelope),
413             'signature': self.__sign(header_encryption_envelope),
414             'mastertoken': self.mastertoken,
415         }
416
417         # Serialize the given Data
418         serialized_data = json.dumps(data)
419         serialized_data = serialized_data.replace('"', '\\"')
420         serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
421
422         compressed_data = self.__compress_data(serialized_data)
423
424         # Create FIRST Payload Chunks
425         first_payload = {
426             "messageid": self.current_message_id,
427             "data": compressed_data,
428             "compressionalgo": "GZIP",
429             "sequencenumber": 1,
430             "endofmsg": True
431         }
432         first_payload_encryption_envelope = self.__encrypt(json.dumps(first_payload))
433         first_payload_chunk = {
434             'payload': base64.standard_b64encode(first_payload_encryption_envelope),
435             'signature': self.__sign(first_payload_encryption_envelope),
436         }
437
438         request_data = json.dumps(header) + json.dumps(first_payload_chunk)
439         return request_data
440
441
442
443     def __compress_data(self, data):
444         # GZIP THE DATA
445         out = StringIO()
446         with gzip.GzipFile(fileobj=out, mode="w") as f:
447             f.write(data)
448         return base64.standard_b64encode(out.getvalue())
449
450
451     def __generate_msl_header(self, is_handshake=False, is_key_request=False, compressionalgo="GZIP", encrypt=True):
452         """
453         Function that generates a MSL header dict
454         :return: The base64 encoded JSON String of the header
455         """
456         self.current_message_id = self.rndm.randint(0, pow(2, 52))
457         esn = self.kodi_helper.get_esn()
458
459         header_data = {
460             'sender': esn,
461             'handshake': is_handshake,
462             'nonreplayable': False,
463             'capabilities': {
464                 'languages': ["en-US"],
465                 'compressionalgos': []
466             },
467             'recipient': 'Netflix',
468             'renewable': True,
469             'messageid': self.current_message_id,
470             'timestamp': 1467733923
471         }
472
473         # Add compression algo if not empty
474         if compressionalgo is not "":
475             header_data['capabilities']['compressionalgos'].append(compressionalgo)
476
477         # If this is a keyrequest act diffrent then other requests
478         if is_key_request:
479             public_key = base64.standard_b64encode(self.rsa_key.publickey().exportKey(format='DER'))
480             header_data['keyrequestdata'] = [{
481                 'scheme': 'ASYMMETRIC_WRAPPED',
482                 'keydata': {
483                     'publickey': public_key,
484                     'mechanism': 'JWK_RSA',
485                     'keypairid': 'superKeyPair'
486                 }
487             }]
488         else:
489             if 'usertoken' in self.tokens:
490                 pass
491             else:
492                 account = self.kodi_helper.get_credentials()
493                 # Auth via email and password
494                 header_data['userauthdata'] = {
495                     'scheme': 'EMAIL_PASSWORD',
496                     'authdata': {
497                         'email': account['email'],
498                         'password': account['password']
499                     }
500                 }
501
502         return json.dumps(header_data)
503
504
505
506     def __encrypt(self, plaintext):
507         """
508         Encrypt the given Plaintext with the encryption key
509         :param plaintext:
510         :return: Serialized JSON String of the encryption Envelope
511         """
512         esn = self.kodi_helper.get_esn()
513         iv = get_random_bytes(16)
514         encryption_envelope = {
515             'ciphertext': '',
516             'keyid': esn + '_' + str(self.sequence_number),
517             'sha256': 'AA==',
518             'iv': base64.standard_b64encode(iv)
519         }
520         # Padd the plaintext
521         plaintext = Padding.pad(plaintext, 16)
522         # Encrypt the text
523         cipher = AES.new(self.encryption_key, AES.MODE_CBC, iv)
524         ciphertext = cipher.encrypt(plaintext)
525         encryption_envelope['ciphertext'] = base64.standard_b64encode(ciphertext)
526         return json.dumps(encryption_envelope)
527
528
529     def __sign(self, text):
530         """
531         Calculates the HMAC signature for the given text with the current sign key and SHA256
532         :param text:
533         :return: Base64 encoded signature
534         """
535         signature = HMAC.new(self.sign_key, text, SHA256).digest()
536         return base64.standard_b64encode(signature)
537
538
539     def __perform_key_handshake(self):
540         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
541         esn = self.kodi_helper.get_esn()
542         request = {
543             'entityauthdata': {
544                 'scheme': 'NONE',
545                 'authdata': {
546                     'identity': esn
547                 }
548             },
549             'headerdata': base64.standard_b64encode(header),
550             'signature': '',
551         }
552         self.kodi_helper.log(msg='Key Handshake Request:')
553         self.kodi_helper.log(msg=json.dumps(request))
554
555         resp = self.session.post(self.endpoints['manifest'], json.dumps(request, sort_keys=True))
556         if resp.status_code == 200:
557             resp = resp.json()
558             if 'errordata' in resp:
559                 self.kodi_helper.log(msg='Key Exchange failed')
560                 self.kodi_helper.log(msg=base64.standard_b64decode(resp['errordata']))
561                 return False
562             self.__parse_crypto_keys(json.JSONDecoder().decode(base64.standard_b64decode(resp['headerdata'])))
563         else:
564             self.kodi_helper.log(msg='Key Exchange failed')
565             self.kodi_helper.log(msg=resp.text)
566
567     def __parse_crypto_keys(self, headerdata):
568         self.__set_master_token(headerdata['keyresponsedata']['mastertoken'])
569         # Init Decryption
570         encrypted_encryption_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['encryptionkey'])
571         encrypted_sign_key = base64.standard_b64decode(headerdata['keyresponsedata']['keydata']['hmackey'])
572         cipher_rsa = PKCS1_OAEP.new(self.rsa_key)
573
574         # Decrypt encryption key
575         encryption_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_encryption_key))
576         self.encryption_key = base64key_decode(encryption_key_data['k'])
577
578         # Decrypt sign key
579         sign_key_data = json.JSONDecoder().decode(cipher_rsa.decrypt(encrypted_sign_key))
580         self.sign_key = base64key_decode(sign_key_data['k'])
581
582         self.__save_msl_data()
583         self.handshake_performed = True
584
585     def __load_msl_data(self):
586         msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
587         #Check expire date of the token
588         master_token = json.JSONDecoder().decode(base64.standard_b64decode(msl_data['tokens']['mastertoken']['tokendata']))
589         valid_until = datetime.utcfromtimestamp(int(master_token['expiration']))
590         present = datetime.now()
591         difference = valid_until - present
592         difference = difference.total_seconds() / 60 / 60
593         # If token expires in less then 10 hours or is expires renew it
594         if difference < 10:
595             self.__load_rsa_keys()
596             self.__perform_key_handshake()
597             return
598
599         self.__set_master_token(msl_data['tokens']['mastertoken'])
600         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
601         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
602
603
604     def save_msl_data(self):
605         self.__save_msl_data()
606
607     def __save_msl_data(self):
608         """
609         Saves the keys and tokens in json file
610         :return:
611         """
612         data = {
613             "encryption_key": base64.standard_b64encode(self.encryption_key),
614             'sign_key': base64.standard_b64encode(self.sign_key),
615             'tokens': {
616                 'mastertoken': self.mastertoken
617             }
618         }
619         serialized_data = json.JSONEncoder().encode(data)
620         self.save_file(self.kodi_helper.msl_data_path, 'msl_data.json', serialized_data)
621
622     def __set_master_token(self, master_token):
623         self.mastertoken = master_token
624         self.sequence_number = json.JSONDecoder().decode(base64.standard_b64decode(master_token['tokendata']))['sequencenumber']
625
626     def __load_rsa_keys(self):
627         loaded_key = self.load_file(self.kodi_helper.msl_data_path, 'rsa_key.bin')
628         self.rsa_key = RSA.importKey(loaded_key)
629
630     def __save_rsa_keys(self):
631         self.kodi_helper.log(msg='Save RSA Keys')
632         # Get the DER Base64 of the keys
633         encrypted_key = self.rsa_key.exportKey()
634         self.save_file(self.kodi_helper.msl_data_path, 'rsa_key.bin', encrypted_key)
635
636     @staticmethod
637     def file_exists(msl_data_path, filename):
638         """
639         Checks if a given file exists
640         :param filename: The filename
641         :return: True if so
642         """
643         return os.path.isfile(msl_data_path + filename)
644
645     @staticmethod
646     def save_file(msl_data_path, filename, content):
647         """
648         Saves the given content under given filename
649         :param filename: The filename
650         :param content: The content of the file
651         """
652         with open(msl_data_path + filename, 'w') as file_:
653             file_.write(content)
654             file_.flush()
655
656     @staticmethod
657     def load_file(msl_data_path, filename):
658         """
659         Loads the content of a given filename
660         :param filename: The file to load
661         :return: The content of the file
662         """
663         with open(msl_data_path + filename) as file_:
664             file_content = file_.read()
665         return file_content