From 2c4b1c55c7c86f36ff34cbefe4f0a384fad473d7 Mon Sep 17 00:00:00 2001 From: CastagnaIT Date: Sun, 11 Dec 2022 13:13:38 +0100 Subject: [PATCH] Auto generate ESN's --- .../resource.language.en_gb/strings.po | 4 + .../lib/kodi/ui/xmldialog_esnwidevine.py | 20 +++-- .../lib/services/nfsession/msl/msl_handler.py | 4 +- resources/lib/utils/esn.py | 82 ++++++++++++++++--- .../plugin-video-netflix-ESN-Widevine.xml | 72 ++++++++++------ 5 files changed, 139 insertions(+), 43 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 4bc8822a6..d9f4020d0 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1225,3 +1225,7 @@ msgstr "" msgctxt "#30735" msgid "About Netflix add-on" msgstr "" + +msgctxt "#30736" +msgid "Automatically generates new ESNs (workaround for 540p limit)" +msgstr "" diff --git a/resources/lib/kodi/ui/xmldialog_esnwidevine.py b/resources/lib/kodi/ui/xmldialog_esnwidevine.py index 83e68ff31..8d5942bb4 100644 --- a/resources/lib/kodi/ui/xmldialog_esnwidevine.py +++ b/resources/lib/kodi/ui/xmldialog_esnwidevine.py @@ -7,6 +7,8 @@ SPDX-License-Identifier: MIT See LICENSES/MIT.md for more information. """ +import time + import xbmc import xbmcgui import xbmcvfs @@ -55,6 +57,8 @@ def onInit(self): self.getControl(40002).setLabel(common.get_local_string(30605).format(WidevineForceSecLev.L3_4445)) # Set the current ESN to Label self.getControl(30000).setLabel(self.esn) + # Set [Auto generate ESN] radio button value + self.getControl(40100).setSelected(G.LOCAL_DB.get_value('esn_auto_generate', True)) # Set the current Widevine security level to the radio buttons self.getControl(self.WV_SECLEV_MAP_BTN[self.wv_force_sec_lev]).setSelected(True) # Hide force L3 on non-android systems (L1 is currently supported only to android) @@ -88,6 +92,11 @@ def onClick(self, controlId): G.LOCAL_DB.set_value('widevine_force_seclev', self.wv_sec_lev_new or self.wv_force_sec_lev, TABLE_SESSION) + # Reset ESN timestamp to prevent to replace the stored ESN immediately + G.LOCAL_DB.set_value('esn_timestamp', int(time.time())) + # Update value for auto generate ESN + is_checked = self.getControl(40100).isSelected() + G.LOCAL_DB.set_value('esn_auto_generate', is_checked) # Delete manifests cache, to prevent possible problems in relation to previous ESN used from resources.lib.common.cache_utils import CACHE_MANIFESTS G.CACHE.clear([CACHE_MANIFESTS]) @@ -105,14 +114,13 @@ def onAction(self, action): self._revert_changes() self.close() - def _esn_checks(self): - """Sanity checks for custom ESN""" - esn = self.esn_new or self.esn + def _esn_checks(self, esn): + """Sanity checks for ESN""" if self.is_android: - if not esn.startswith(('NFANDROID1-PRV-', 'NFANDROID2-PRV-')) or len(esn.split('-')) < 5: + if not esn.startswith(('NFANDROID1-PRV-', 'NFANDROID2-PRV-')) or esn.count('-') < 5: return False else: - if len(esn.split('-')) != 3 or len(esn) != 40: + if esn.count('-') != 3 or len(esn) != 40: return False return True @@ -138,7 +146,7 @@ def _update_esn_label(self): def _change_esn(self): esn_custom = ui.ask_for_input(common.get_local_string(30602), self.esn_new or self.esn) if esn_custom: - if not self._esn_checks(): + if not self._esn_checks(esn_custom): # Wrong custom ESN format type ui.show_ok_dialog(common.get_local_string(30600), common.get_local_string(30608)) else: diff --git a/resources/lib/services/nfsession/msl/msl_handler.py b/resources/lib/services/nfsession/msl/msl_handler.py index 5906d9921..1350dc466 100644 --- a/resources/lib/services/nfsession/msl/msl_handler.py +++ b/resources/lib/services/nfsession/msl/msl_handler.py @@ -19,7 +19,7 @@ from resources.lib.common.exceptions import MSLError from resources.lib.database.db_utils import TABLE_SESSION from resources.lib.globals import G -from resources.lib.utils.esn import get_esn, set_esn +from resources.lib.utils.esn import get_esn, set_esn, regen_esn from resources.lib.utils.logging import LOG, measure_exec_time_decorator from .converter import convert_to_dash from .events_handler import EventsHandler @@ -94,6 +94,8 @@ def get_manifest(self, viewable_id, challenge, sid): # When the add-on is installed from scratch or you logout the account the ESN will be empty if not esn: esn = set_esn() + else: + esn = regen_esn(esn) manifest = self._get_manifest(viewable_id, esn, challenge, sid) except MSLError as exc: if 'Email or password is incorrect' in str(exc): diff --git a/resources/lib/utils/esn.py b/resources/lib/utils/esn.py index 11ed10c07..dd0acb5fc 100644 --- a/resources/lib/utils/esn.py +++ b/resources/lib/utils/esn.py @@ -7,7 +7,8 @@ SPDX-License-Identifier: MIT See LICENSES/MIT.md for more information. """ -from re import sub +import time +import re from resources.lib.database.db_utils import TABLE_SESSION from resources.lib.globals import G @@ -51,6 +52,41 @@ def set_website_esn(esn): G.LOCAL_DB.set_value('website_esn', esn, TABLE_SESSION) +def regen_esn(esn): + """ + Regenerate the ESN on the basis of the existing one, + to preserve possible user customizations, + this method will only be executed every 20 hours. + """ + # From the beginning of December 2022 if you are using an ESN for more than about 20 hours + # Netflix limits the resolution to 540p. The reasons behind this are unknown, there are no changes on website + # or Android apps. Moreover, if you set the full-length ESN of android app on the add-on, also the original app + # will be downgraded to 540p without any kind of message. + if not G.LOCAL_DB.get_value('esn_auto_generate', True): + return esn + from resources.lib.common.device_utils import get_system_platform + ts_now = int(time.time()) + ts_esn = G.LOCAL_DB.get_value('esn_timestamp', default_value=0) + # When an ESN has been used for more than 20 hours ago, generate a new ESN + if ts_esn == 0 or ts_now - ts_esn > 72000: + if get_system_platform() == 'android': + if esn[-1] == '-': + # We have a partial ESN without last 64 chars, so generate and add the 64 chars + esn += _create_id64chars() + elif re.search(r'-[0-9]+-[A-Z0-9]{64}', esn): + # Replace last 64 chars with the new generated one + esn = esn[:-64] + _create_id64chars() + else: + LOG.warn('ESN format not recognized, will be reset with a new ESN') + esn = generate_android_esn() + else: + esn = generate_esn(esn[:-30]) + set_esn(esn) + G.LOCAL_DB.set_value('esn_timestamp', ts_now) + LOG.debug('The ESN has been regenerated (540p workaround).') + return esn + + def generate_android_esn(wv_force_sec_lev=None): """Generate an ESN if on android or return the one from user_data""" from resources.lib.common.device_utils import get_system_platform @@ -63,15 +99,25 @@ def generate_android_esn(wv_force_sec_lev=None): return None -def generate_esn(prefix=''): - """Generate a random ESN""" - # For possibles prefixes see website, are based on browser user agent - import random - esn = prefix +def generate_esn(init_part=None): + """ + Generate a random ESN + :param init_part: Specify the initial part to be used e.g. "NFCDCH-02-", + if not set will be obtained from the last retrieved from the website + :return: The generated ESN + """ + # The initial part of the ESN e.g. "NFCDCH-02-" depends on the web browser used and then the user agent, + # refer to website to know all types available. + if not init_part: + esn_w_split = get_website_esn().split('-', 2) + if len(esn_w_split) != 3: + raise Exception('Cannot generate ESN due to unexpected website ESN') + init_part = '-'.join(esn_w_split[:2]) + '-' + esn = init_part possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + from secrets import choice for _ in range(0, 30): - esn += random.choice(possible) - LOG.debug('Generated random ESN: {}', esn) + esn += choice(possible) return esn @@ -110,8 +156,9 @@ def _generate_esn_android(props, wv_force_sec_lev): model = model[:45].strip() prod = manufacturer + model - prod = sub(r'[^A-Za-z0-9=-]', '=', prod) - return 'NFANDROID1-PRV-' + device_category + sec_lev + prod + '-' + system_id + '-' + prod = re.sub(r'[^A-Za-z0-9=-]', '=', prod) + + return 'NFANDROID1-PRV-' + device_category + sec_lev + prod + '-' + system_id + '-' + _create_id64chars() def _generate_esn_android_tv(props, wv_force_sec_lev): @@ -134,7 +181,7 @@ def _generate_esn_android_tv(props, wv_force_sec_lev): if not model_group: model_group = '0' - model_group = sub(r'[^A-Za-z0-9=-]', '=', model_group) + model_group = re.sub(r'[^A-Za-z0-9=-]', '=', model_group) if len(manufacturer) < 5: manufacturer += ' ' @@ -142,10 +189,11 @@ def _generate_esn_android_tv(props, wv_force_sec_lev): model = model[:45].strip() prod = manufacturer + model - prod = sub(r'[^A-Za-z0-9=-]', '=', prod) + prod = re.sub(r'[^A-Za-z0-9=-]', '=', prod) _, system_id = _get_drm_info(wv_force_sec_lev) - return 'NFANDROID2-PRV-' + model_group + '-' + prod + '-' + system_id + '-' + + return 'NFANDROID2-PRV-' + model_group + '-' + prod + '-' + system_id + '-' + _create_id64chars() def _get_drm_info(wv_force_sec_lev): @@ -192,3 +240,11 @@ def _get_android_system_props(): except OSError: LOG.error('Cannot get "getprop" data due to system error.') return {} + +def _create_id64chars(): + # The Android full length ESN include to the end a hashed ID of 64 chars, + # this value is created from the android app by using the Widevine "deviceUniqueId" property value + # hashed in various ways, not knowing the correct formula, we create a random value. + # Starting from 12/2022 this value is mandatory to obtain HD resolutions + from secrets import token_hex + return re.sub(r'[^A-Za-z0-9=-]', '=', token_hex(32).upper()) diff --git a/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml b/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml index 937df892d..39856fd27 100644 --- a/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml +++ b/resources/skins/default/1080i/plugin-video-netflix-ESN-Widevine.xml @@ -21,7 +21,7 @@ 1520 - 490 + 600 0 @@ -75,7 +75,7 @@ 10 80 1200 - 400 + 510 buttons/dialogbutton-nofo.png stretch @@ -85,7 +85,7 @@ 0 0 1200 - 400 + 550 30010 true 10090 @@ -142,7 +142,7 @@ center center no - 40000 + 40100 30011 @@ -164,7 +164,7 @@ center center no - 40000 + 40100 30010 30012 @@ -187,16 +187,45 @@ center center no - 40000 + 40100 30011 40020 + + Radiobutton to auto generate ESN's + radiobutton + 65 + 310 + 920 + 110 + true + myfocustexture.png + mynormaltexture.png + buttons/radio-button-on.png + buttons/radio-button-on.png + buttons/radio-button-off.png + buttons/radio-button-off.png + + font12 + FFFFFFFF + red + 80FFFFFF + left + center + 4 + 5 + false + 30010 + 40000 + 40020 + + Widevine security level - 310 + 430 65 - 310 + 430 65 1000 110 @@ -214,17 +243,16 @@ Widevine force sec.lev. DISABLED radiobutton 65 - 350 + 470 300 110 true - red myfocustexture.png mynormaltexture.png buttons/radio-button-on-sm.png - buttons/radio-button-on-sm.png + buttons/radio-button-on-sm.png buttons/radio-button-off-sm.png - buttons/radio-button-off-sm.png + buttons/radio-button-off-sm.png font12 FFFFFFFF @@ -235,24 +263,23 @@ 4 5 false - 30010 + 40100 40001 Widevine force sec.lev. L3 radiobutton 375 - 350 + 470 300 110 true - red myfocustexture.png mynormaltexture.png buttons/radio-button-on-sm.png - buttons/radio-button-on-sm.png + buttons/radio-button-on-sm.png buttons/radio-button-off-sm.png - buttons/radio-button-off-sm.png + buttons/radio-button-off-sm.png @@ -271,17 +298,16 @@ Widevine force sec.lev. L3 (ID 4445) radiobutton 685 - 350 + 470 300 110 true - red myfocustexture.png mynormaltexture.png buttons/radio-button-on-sm.png - buttons/radio-button-on-sm.png + buttons/radio-button-on-sm.png buttons/radio-button-off-sm.png - buttons/radio-button-off-sm.png + buttons/radio-button-off-sm.png