Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regenerate ESN workaround #1513

Merged
merged 1 commit into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -1225,3 +1225,11 @@ msgstr ""
msgctxt "#30735"
msgid "About Netflix add-on"
msgstr ""

msgctxt "#30736"
msgid "Automatically generates new ESNs (workaround for 540p limit)"
msgstr ""

msgctxt "#30737"
msgid "WARNING: Do not use the original device ESN of the official app."
msgstr ""
20 changes: 14 additions & 6 deletions resources/lib/kodi/ui/xmldialog_esnwidevine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
SPDX-License-Identifier: MIT
See LICENSES/MIT.md for more information.
"""
import time

import xbmc
import xbmcgui
import xbmcvfs
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand All @@ -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

Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion resources/lib/services/nfsession/msl/msl_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
82 changes: 69 additions & 13 deletions resources/lib/utils/esn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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):
Expand All @@ -134,18 +181,19 @@ 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 += ' '
manufacturer = manufacturer[:5]
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):
Expand Down Expand Up @@ -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())
Loading