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