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