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