fix(esn): Fixes ESN/Handshake race condition when user is not valid on Kodi startup
[plugin.video.netflix.git] / resources / lib / MSL.py
index 71f1810a46ce4670732b46f1098688727531111b..d0387972ceeab83ea253a55bf22fb89bc2c748eb 100644 (file)
@@ -1,3 +1,8 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Module: MSL
+# Created on: 26.01.2017
+
 import base64
 import gzip
 import json
 import base64
 import gzip
 import json
@@ -5,6 +10,8 @@ import os
 import pprint
 import random
 from StringIO import StringIO
 import pprint
 import random
 from StringIO import StringIO
+
+from datetime import datetime
 import requests
 import zlib
 
 import requests
 import zlib
 
@@ -13,7 +20,7 @@ from Cryptodome.PublicKey import RSA
 from Cryptodome.Cipher import PKCS1_OAEP
 from Cryptodome.Cipher import AES
 from Cryptodome.Random import get_random_bytes
 from Cryptodome.Cipher import PKCS1_OAEP
 from Cryptodome.Cipher import AES
 from Cryptodome.Random import get_random_bytes
-from Crypto.Hash import HMAC, SHA256
+from Cryptodome.Hash import HMAC, SHA256
 from Cryptodome.Util import Padding
 import xml.etree.ElementTree as ET
 
 from Cryptodome.Util import Padding
 import xml.etree.ElementTree as ET
 
@@ -35,7 +42,8 @@ class MSL:
     last_drm_context = ''
     last_playback_context = ''
     #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
     last_drm_context = ''
     last_playback_context = ''
     #esn = "NFCDCH-LX-CQE0NU6PA5714R25VPLXVU2A193T36"
-    esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
+    #esn = "WWW-BROWSE-D7GW1G4NPXGR1F0X1H3EQGY3V1F5WE"
+    #esn = "NFCDIE-02-DCH84Q2EK3N6VFVQJ0NLRQ27498N0F"
     current_message_id = 0
     session = requests.session()
     rndm = random.SystemRandom()
     current_message_id = 0
     session = requests.session()
     rndm = random.SystemRandom()
@@ -45,13 +53,11 @@ class MSL:
         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
     }
 
         'license': 'http://www.netflix.com/api/msl/NFCDCH-LX/cadmium/license'
     }
 
-    def __init__(self, email, password, kodi_helper):
+    def __init__(self, kodi_helper):
         """
         The Constructor checks for already existing crypto Keys.
         If they exist it will load the existing keys
         """
         """
         The Constructor checks for already existing crypto Keys.
         If they exist it will load the existing keys
         """
-        self.email = email
-        self.password = password
         self.kodi_helper = kodi_helper
         try:
             os.mkdir(self.kodi_helper.msl_data_path)
         self.kodi_helper = kodi_helper
         try:
             os.mkdir(self.kodi_helper.msl_data_path)
@@ -65,13 +71,18 @@ class MSL:
         elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
             self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
             self.__load_rsa_keys()
         elif self.file_exists(self.kodi_helper.msl_data_path, 'rsa_key.bin'):
             self.kodi_helper.log(msg='RSA Keys do already exist load old ones')
             self.__load_rsa_keys()
-            self.__perform_key_handshake()
+            if self.kodi_helper.get_esn():
+                self.__perform_key_handshake()
         else:
             self.kodi_helper.log(msg='Create new RSA Keys')
             # Create new Key Pair and save
             self.rsa_key = RSA.generate(2048)
             self.__save_rsa_keys()
         else:
             self.kodi_helper.log(msg='Create new RSA Keys')
             # Create new Key Pair and save
             self.rsa_key = RSA.generate(2048)
             self.__save_rsa_keys()
-            self.__perform_key_handshake()
+            if self.kodi_helper.get_esn():
+                self.__perform_key_handshake()
+    
+    def perform_key_handshake(self):
+        self.__perform_key_handshake()
 
     def load_manifest(self, viewable_id):
         """
 
     def load_manifest(self, viewable_id):
         """
@@ -84,12 +95,77 @@ class MSL:
             'lookupType': 'PREPARE',
             'viewableIds': [viewable_id],
             'profiles': [
             'lookupType': 'PREPARE',
             'viewableIds': [viewable_id],
             'profiles': [
-                'playready-h264mpl30-dash',
-                'playready-h264mpl31-dash',
+                "playready-h264bpl30-dash",
+                "playready-h264mpl30-dash",
+                "playready-h264mpl31-dash",
+                "playready-h264mpl40-dash",
+                # "hevc-main-L30-dash-cenc",
+                # "hevc-main-L31-dash-cenc",
+                # "hevc-main-L40-dash-cenc",
+                # "hevc-main-L41-dash-cenc",
+                # "hevc-main-L50-dash-cenc",
+                # "hevc-main-L51-dash-cenc",
+                # "hevc-main10-L30-dash-cenc",
+                # "hevc-main10-L31-dash-cenc",
+                # "hevc-main10-L40-dash-cenc",
+                # "hevc-main10-L41-dash-cenc",
+                # "hevc-main10-L50-dash-cenc",
+                # "hevc-main10-L51-dash-cenc",
+                # "hevc-main10-L30-dash-cenc-prk",
+                # "hevc-main10-L31-dash-cenc-prk",
+                # "hevc-main10-L40-dash-cenc-prk",
+                # "hevc-main10-L41-dash-cenc-prk",
+                # "hevc-main-L30-L31-dash-cenc-tl",
+                # "hevc-main-L31-L40-dash-cenc-tl",
+                # "hevc-main-L40-L41-dash-cenc-tl",
+                # "hevc-main-L50-L51-dash-cenc-tl",
+                # "hevc-main10-L30-L31-dash-cenc-tl",
+                # "hevc-main10-L31-L40-dash-cenc-tl",
+                # "hevc-main10-L40-L41-dash-cenc-tl",
+                # "hevc-main10-L50-L51-dash-cenc-tl",
+                # "hevc-dv-main10-L30-dash-cenc",
+                # "hevc-dv-main10-L31-dash-cenc",
+                # "hevc-dv-main10-L40-dash-cenc",
+                # "hevc-dv-main10-L41-dash-cenc",
+                # "hevc-dv-main10-L50-dash-cenc",
+                # "hevc-dv-main10-L51-dash-cenc",
+                # "hevc-dv5-main10-L30-dash-cenc-prk",
+                # "hevc-dv5-main10-L31-dash-cenc-prk",
+                # "hevc-dv5-main10-L40-dash-cenc-prk",
+                # "hevc-dv5-main10-L41-dash-cenc-prk",
+                # "hevc-dv5-main10-L50-dash-cenc-prk",
+                # "hevc-dv5-main10-L51-dash-cenc-prk",
+                # "hevc-hdr-main10-L30-dash-cenc",
+                # "hevc-hdr-main10-L31-dash-cenc",
+                # "hevc-hdr-main10-L40-dash-cenc",
+                # "hevc-hdr-main10-L41-dash-cenc",
+                # "hevc-hdr-main10-L50-dash-cenc",
+                # "hevc-hdr-main10-L51-dash-cenc",
+                # "hevc-hdr-main10-L30-dash-cenc-prk",
+                # "hevc-hdr-main10-L31-dash-cenc-prk",
+                # "hevc-hdr-main10-L40-dash-cenc-prk",
+                # "hevc-hdr-main10-L41-dash-cenc-prk",
+                # "hevc-hdr-main10-L50-dash-cenc-prk",
+                # "hevc-hdr-main10-L51-dash-cenc-prk"
+
+               # 'playready-h264mpl30-dash',
+                #'playready-h264mpl31-dash',
+                #'playready-h264mpl40-dash',
+                #'hevc-main10-L41-dash-cenc',
+                #'hevc-main10-L50-dash-cenc',
+                #'hevc-main10-L51-dash-cenc',
+
+
+
+                # Audio
                 'heaac-2-dash',
                 'heaac-2-dash',
-                'dfxp-ls-sdh',
+
+                #subtiltes
+                #'dfxp-ls-sdh',
                 'simplesdh',
                 'simplesdh',
-                'nflx-cmisc',
+                #'nflx-cmisc',
+
+                #unkown
                 'BIF240',
                 'BIF320'
             ],
                 'BIF240',
                 'BIF320'
             ],
@@ -109,6 +185,12 @@ class MSL:
             'clientVersion': '4.0004.899.011',
             'uiVersion': 'akira'
         }
             'clientVersion': '4.0004.899.011',
             'uiVersion': 'akira'
         }
+
+        # Check if dolby sound is enabled and add to profles
+        if self.kodi_helper.get_dolby_setting():
+            manifest_request_data['profiles'].append('ddplus-2.0-dash')
+            manifest_request_data['profiles'].append('ddplus-5.1-dash')
+
         request_data = self.__generate_msl_request_data(manifest_request_data)
         resp = self.session.post(self.endpoints['manifest'], request_data)
 
         request_data = self.__generate_msl_request_data(manifest_request_data)
         resp = self.session.post(self.endpoints['manifest'], request_data)
 
@@ -203,6 +285,7 @@ class MSL:
                 pssh = manifest['psshb64'][0]
 
         seconds = manifest['runtime']/1000
                 pssh = manifest['psshb64'][0]
 
         seconds = manifest['runtime']/1000
+        init_length = seconds / 2 * 12 + 20*1000
         duration = "PT"+str(seconds)+".00S"
 
         root = ET.Element('MPD')
         duration = "PT"+str(seconds)+".00S"
 
         root = ET.Element('MPD')
@@ -222,18 +305,30 @@ class MSL:
                 ET.SubElement(protection, 'cenc:pssh').text = pssh
 
             for downloadable in video_track['downloadables']:
                 ET.SubElement(protection, 'cenc:pssh').text = pssh
 
             for downloadable in video_track['downloadables']:
+
+                codec = 'h264'
+                if 'hevc' in downloadable['contentProfile']:
+                    codec = 'hevc'
+
+                hdcp_versions = '0.0'
+                for hdcp in downloadable['hdcpVersions']:
+                    if hdcp != 'none':
+                        hdcp_versions = hdcp
+
                 rep = ET.SubElement(video_adaption_set, 'Representation',
                                     width=str(downloadable['width']),
                                     height=str(downloadable['height']),
                                     bandwidth=str(downloadable['bitrate']*1024),
                 rep = ET.SubElement(video_adaption_set, 'Representation',
                                     width=str(downloadable['width']),
                                     height=str(downloadable['height']),
                                     bandwidth=str(downloadable['bitrate']*1024),
-                                    codecs='h264',
+                                    hdcp=hdcp_versions,
+                                    nflxContentProfile=str(downloadable['contentProfile']),
+                                    codecs=codec,
                                     mimeType='video/mp4')
 
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Init an Segment block
                                     mimeType='video/mp4')
 
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Init an Segment block
-                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
-                ET.SubElement(segment_base, 'Initialization', range='0-60000')
+                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
+                ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
 
 
 
 
 
 
@@ -244,8 +339,13 @@ class MSL:
                                                contentType='audio',
                                                mimeType='audio/mp4')
             for downloadable in audio_track['downloadables']:
                                                contentType='audio',
                                                mimeType='audio/mp4')
             for downloadable in audio_track['downloadables']:
+                codec = 'aac'
+                print downloadable
+                if downloadable['contentProfile'] == 'ddplus-2.0-dash' or downloadable['contentProfile'] == 'ddplus-5.1-dash':
+                    codec = 'ec-3'
+                print "codec is: " + codec
                 rep = ET.SubElement(audio_adaption_set, 'Representation',
                 rep = ET.SubElement(audio_adaption_set, 'Representation',
-                                    codecs='aac',
+                                    codecs=codec,
                                     bandwidth=str(downloadable['bitrate']*1024),
                                     mimeType='audio/mp4')
 
                                     bandwidth=str(downloadable['bitrate']*1024),
                                     mimeType='audio/mp4')
 
@@ -257,8 +357,23 @@ class MSL:
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Index range
                 #BaseURL
                 ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
                 # Index range
-                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-60000", indexRangeExact="true")
-                ET.SubElement(segment_base, 'Initialization', range='0-60000')
+                segment_base = ET.SubElement(rep, 'SegmentBase', indexRange="0-"+str(init_length), indexRangeExact="true")
+                ET.SubElement(segment_base, 'Initialization', range='0-'+str(init_length))
+
+        # Multiple Adaption Sets for subtiles
+        for text_track in manifest['textTracks']:
+            if 'downloadables' not in text_track or text_track['downloadables'] is None:
+                continue
+            subtiles_adaption_set = ET.SubElement(period, 'AdaptationSet',
+                                                  lang=text_track['bcp47'],
+                                                  codecs='stpp',
+                                                  contentType='text',
+                                                  mimeType='application/ttml+xml')
+            for downloadable in text_track['downloadables']:
+                rep = ET.SubElement(subtiles_adaption_set, 'Representation',
+                                    nflxProfile=downloadable['contentProfile']
+                                    )
+                ET.SubElement(rep, 'BaseURL').text = self.__get_base_url(downloadable['urls'])
 
 
         xml = ET.tostring(root, encoding='utf-8', method='xml')
 
 
         xml = ET.tostring(root, encoding='utf-8', method='xml')
@@ -296,6 +411,7 @@ class MSL:
         }
 
     def __generate_msl_request_data(self, data):
         }
 
     def __generate_msl_request_data(self, data):
+        self.__load_msl_data()
         header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
         header = {
             'headerdata': base64.standard_b64encode(header_encryption_envelope),
         header_encryption_envelope = self.__encrypt(self.__generate_msl_header())
         header = {
             'headerdata': base64.standard_b64encode(header_encryption_envelope),
@@ -306,7 +422,7 @@ class MSL:
         # Serialize the given Data
         serialized_data = json.dumps(data)
         serialized_data = serialized_data.replace('"', '\\"')
         # Serialize the given Data
         serialized_data = json.dumps(data)
         serialized_data = serialized_data.replace('"', '\\"')
-        serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-11","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
+        serialized_data = '[{},{"headers":{},"path":"/cbp/cadmium-13","payload":{"data":"' + serialized_data + '"},"query":""}]\n'
 
         compressed_data = self.__compress_data(serialized_data)
 
 
         compressed_data = self.__compress_data(serialized_data)
 
@@ -343,9 +459,10 @@ class MSL:
         :return: The base64 encoded JSON String of the header
         """
         self.current_message_id = self.rndm.randint(0, pow(2, 52))
         :return: The base64 encoded JSON String of the header
         """
         self.current_message_id = self.rndm.randint(0, pow(2, 52))
+        esn = self.kodi_helper.get_esn()
 
         header_data = {
 
         header_data = {
-            'sender': self.esn,
+            'sender': esn,
             'handshake': is_handshake,
             'nonreplayable': False,
             'capabilities': {
             'handshake': is_handshake,
             'nonreplayable': False,
             'capabilities': {
@@ -377,12 +494,13 @@ class MSL:
             if 'usertoken' in self.tokens:
                 pass
             else:
             if 'usertoken' in self.tokens:
                 pass
             else:
+                account = self.kodi_helper.get_credentials()
                 # Auth via email and password
                 header_data['userauthdata'] = {
                     'scheme': 'EMAIL_PASSWORD',
                     'authdata': {
                 # Auth via email and password
                 header_data['userauthdata'] = {
                     'scheme': 'EMAIL_PASSWORD',
                     'authdata': {
-                        'email': self.email,
-                        'password': self.password
+                        'email': account['email'],
+                        'password': account['password']
                     }
                 }
 
                     }
                 }
 
@@ -396,10 +514,12 @@ class MSL:
         :param plaintext:
         :return: Serialized JSON String of the encryption Envelope
         """
         :param plaintext:
         :return: Serialized JSON String of the encryption Envelope
         """
+        esn = self.kodi_helper.get_esn()
+
         iv = get_random_bytes(16)
         encryption_envelope = {
             'ciphertext': '',
         iv = get_random_bytes(16)
         encryption_envelope = {
             'ciphertext': '',
-            'keyid': self.esn + '_' + str(self.sequence_number),
+            'keyid': esn + '_' + str(self.sequence_number),
             'sha256': 'AA==',
             'iv': base64.standard_b64encode(iv)
         }
             'sha256': 'AA==',
             'iv': base64.standard_b64encode(iv)
         }
@@ -424,11 +544,13 @@ class MSL:
 
     def __perform_key_handshake(self):
         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
 
     def __perform_key_handshake(self):
         header = self.__generate_msl_header(is_key_request=True, is_handshake=True, compressionalgo="", encrypt=False)
+        esn = self.kodi_helper.get_esn()
+
         request = {
             'entityauthdata': {
                 'scheme': 'NONE',
                 'authdata': {
         request = {
             'entityauthdata': {
                 'scheme': 'NONE',
                 'authdata': {
-                    'identity': self.esn
+                    'identity': esn
                 }
             },
             'headerdata': base64.standard_b64encode(header),
                 }
             },
             'headerdata': base64.standard_b64encode(header),
@@ -469,10 +591,26 @@ class MSL:
 
     def __load_msl_data(self):
         msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
 
     def __load_msl_data(self):
         msl_data = json.JSONDecoder().decode(self.load_file(self.kodi_helper.msl_data_path, 'msl_data.json'))
+        #Check expire date of the token
+        master_token = json.JSONDecoder().decode(base64.standard_b64decode(msl_data['tokens']['mastertoken']['tokendata']))
+        valid_until = datetime.utcfromtimestamp(int(master_token['expiration']))
+        present = datetime.now()
+        difference = valid_until - present
+        difference = difference.total_seconds() / 60 / 60
+        # If token expires in less then 10 hours or is expires renew it
+        if difference < 10:
+            self.__load_rsa_keys()
+            self.__perform_key_handshake()
+            return
+
         self.__set_master_token(msl_data['tokens']['mastertoken'])
         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
 
         self.__set_master_token(msl_data['tokens']['mastertoken'])
         self.encryption_key = base64.standard_b64decode(msl_data['encryption_key'])
         self.sign_key = base64.standard_b64decode(msl_data['sign_key'])
 
+
+    def save_msl_data(self):
+        self.__save_msl_data()
+
     def __save_msl_data(self):
         """
         Saves the keys and tokens in json file
     def __save_msl_data(self):
         """
         Saves the keys and tokens in json file