Skip to content

Commit

Permalink
Auto generate ESN's
Browse files Browse the repository at this point in the history
  • Loading branch information
CastagnaIT committed Dec 16, 2022
1 parent 60731da commit 8e2bbcf
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 50 deletions.
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

0 comments on commit 8e2bbcf

Please sign in to comment.