Skip to content
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
2 changes: 0 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ charset = utf-8
trim_trailing_whitespace = true
# A file must end with an empty line - this is good for version control systems
insert_final_newline = true
# A line should not have more than this amount of chars (not supported by all plugins)
max_line_length = 100

[*.{py,md,txt}]
indent_size = 4
Expand Down
2 changes: 1 addition & 1 deletion resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ msgid "Install Up Next add-on"
msgstr ""

msgctxt "#30235"
msgid "[WIP] Send/Receive progress and watched status of the videos (works only with the main profile)"
msgid "[WIP] Synchronize the watched status of the videos with Netflix"
msgstr ""

msgctxt "#30236"
Expand Down
7 changes: 6 additions & 1 deletion resources/lib/api/website.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@
# 'ichnaea_log': 'models/serverDefs/data/ICHNAEA_ROOT', can be for XSS attacks?
'api_endpoint_root_url': 'models/serverDefs/data/API_ROOT',
'api_endpoint_url': 'models/playerModel/data/config/ui/initParams/apiUrl',
'request_id': 'models/serverDefs/data/requestId'
'request_id': 'models/serverDefs/data/requestId',
'asset_core': 'models/playerModel/data/config/core/assets/core',
'ui_version': 'models/playerModel/data/config/ui/initParams/uiVersion',
'browser_info_version': 'models/browserInfo/data/version',
'browser_info_os_name': 'models/browserInfo/data/os/name',
'browser_info_os_version': 'models/browserInfo/data/os/version',
}

PAGE_ITEM_ERROR_CODE = 'models/flow/data/fields/errorCode/value'
Expand Down
2 changes: 1 addition & 1 deletion resources/lib/kodi/context_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def generate_context_menu_items(videoid):

if videoid.mediatype in [common.VideoId.MOVIE, common.VideoId.EPISODE]:
# Add menu to allow change manually the watched status when progress manager is enabled
if g.ADDON.getSettingBool('ProgressManager_enabled') and g.LOCAL_DB.get_profile_config('isAccountOwner', False):
if g.ADDON.getSettingBool('ProgressManager_enabled'):
items.insert(0, _ctx_item('change_watched_status', videoid))

return items
Expand Down
14 changes: 5 additions & 9 deletions resources/lib/kodi/infolabels.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,7 @@ def _colorize_title(text, color, remove_color=False):

def _set_progress_status(list_item, video_data, infos):
"""Check and set progress status (watched and resume)"""
if not g.ADDON.getSettingBool('ProgressManager_enabled') or \
not g.LOCAL_DB.get_profile_config('isAccountOwner', False):
# Currently due to a unknown problem, it is not possible to communicate MSL data to the right selected
# profile other than the owner profile
if not g.ADDON.getSettingBool('ProgressManager_enabled'):
return

video_id = video_data['summary']['id']
Expand All @@ -368,11 +365,10 @@ def _set_progress_status(list_item, video_data, infos):
# seem not respect really if a video is watched to the end or this tag have other purposes
# to now, the only way to know if a video is watched is compare the bookmarkPosition with creditsOffset value

if not video_data.get('creditsOffset'):
# NOTE shakti 'creditsOffset' tag not exists on video type 'movie',
# then simulate the default Kodi playcount behaviour (playcountminimumpercent)
watched_threshold = video_data['runtime'] - (video_data['runtime'] / 100 * 90)
else:
# NOTE shakti 'creditsOffset' tag not exists on video type 'movie',
# then simulate the default Kodi playcount behaviour (playcountminimumpercent)
watched_threshold = video_data['runtime'] / 100 * 90
if video_data.get('creditsOffset') and video_data['creditsOffset'] < watched_threshold:
watched_threshold = video_data['creditsOffset']

# NOTE shakti 'bookmarkPosition' tag when it is not set have -1 value
Expand Down
5 changes: 2 additions & 3 deletions resources/lib/navigation/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ def play(videoid):
return
if index_selected == 1:
resume_position = None
elif g.ADDON.getSettingBool('ProgressManager_enabled') and g.LOCAL_DB.get_profile_config('isAccountOwner', False):
elif (g.ADDON.getSettingBool('ProgressManager_enabled') and
videoid.mediatype in [common.VideoId.MOVIE, common.VideoId.EPISODE]):
# To now we have this limits:
# - enabled only if the owner profile is used. Currently due to a unknown problem,
# it is not possible to communicate MSL data to the right selected profile
# - enabled only with items played inside the addon then not Kodi library, need impl. JSON-RPC lib update code
event_data = _get_event_data(videoid)
event_data['videoid'] = videoid.to_dict()
Expand Down
37 changes: 19 additions & 18 deletions resources/lib/services/msl/android_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,27 @@

class AndroidMSLCrypto(MSLBaseCrypto):
"""Crypto handler for Android platforms"""
def __init__(self, msl_data=None): # pylint: disable=super-on-old-class
# pylint: disable=broad-except
def __init__(self):
super(AndroidMSLCrypto, self).__init__()
self.crypto_session = None
self.keyset_id = None
self.key_id = None
self.hmac_key_id = None
try:
self.crypto_session = xbmcdrm.CryptoSession(
'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', 'AES/CBC/NoPadding', 'HmacSHA256')
common.debug('Widevine CryptoSession successful constructed')
except Exception:
except Exception: # pylint: disable=broad-except
import traceback
common.error(traceback.format_exc())
raise MSLError('Failed to construct Widevine CryptoSession')

try:
super(AndroidMSLCrypto, self).__init__(msl_data)
self.keyset_id = base64.standard_b64decode(msl_data['key_set_id'])
self.key_id = base64.standard_b64decode(msl_data['key_id'])
self.hmac_key_id = base64.standard_b64decode(msl_data['hmac_key_id'])
self.crypto_session.RestoreKeys(self.keyset_id)
except Exception:
self.keyset_id = None
self.key_id = None
self.hmac_key_id = None

drm_info = {
'version': self.crypto_session.GetPropertyString('version'),
'system_id': self.crypto_session.GetPropertyString('systemId'),
# 'device_unique_id': self.crypto_session.GetPropertyByteArray('deviceUniqueId')
'hdcp_level': self.crypto_session.GetPropertyString('hdcpLevel'),
'hdcp_level_max': self.crypto_session.GetPropertyString('maxHdcpLevel'),
'hdcp_level': self.crypto_session.GetPropertyString('hdcpLevel'), #WTF perchè non viene fuori??? serve volontario test
'hdcp_level_max': self.crypto_session.GetPropertyString('maxHdcpLevel'), #WTF perchè non viene fuori??? serve volontario test
'security_level': self.crypto_session.GetPropertyString('securityLevel')
}

Expand All @@ -71,10 +64,18 @@ def __init__(self, msl_data=None): # pylint: disable=super-on-old-class
else:
common.warn('Widevine CryptoSession system id not obtained!')
common.debug('Widevine CryptoSession security level: {}', drm_info['security_level'])
common.debug('Widevine CryptoSession current hdcp level', drm_info['hdcp_level'])
common.debug('Widevine CryptoSession max hdcp level supported', drm_info['hdcp_level_max'])
common.debug('Widevine CryptoSession current hdcp level: {}', drm_info['hdcp_level'])
common.debug('Widevine CryptoSession max hdcp level supported: {}', drm_info['hdcp_level_max'])
common.debug('Widevine CryptoSession algorithms: {}', self.crypto_session.GetPropertyString('algorithms'))

def load_crypto_session(self, msl_data=None):
if not msl_data:
return
self.keyset_id = base64.standard_b64decode(msl_data['key_set_id'])
self.key_id = base64.standard_b64decode(msl_data['key_id'])
self.hmac_key_id = base64.standard_b64decode(msl_data['hmac_key_id'])
self.crypto_session.RestoreKeys(self.keyset_id)

def __del__(self):
self.crypto_session = None

Expand Down
59 changes: 48 additions & 11 deletions resources/lib/services/msl/base_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,33 @@

import json
import base64
import time

import resources.lib.common as common


class MSLBaseCrypto(object):
"""Common base class for MSL crypto operations.
Handles mastertoken and sequence number"""
# pylint: disable=too-few-public-methods
def __init__(self, msl_data=None):

def __init__(self):
self._msl_data = None
self.mastertoken = None
self.serial_number = None
self.sequence_number = None
self.renewal_window = None
self.expiration = None

def load_msl_data(self, msl_data=None):
self._msl_data = msl_data if msl_data else {}
if msl_data:
self._set_mastertoken(msl_data['tokens']['mastertoken'])
else:
self.mastertoken = None
self.set_mastertoken(msl_data['tokens']['mastertoken'])

def compare_mastertoken(self, mastertoken):
"""Check if the new mastertoken is different from current due to renew"""
if not self._mastertoken_is_newer_that(mastertoken):
common.debug('MSL mastertoken is changed due to renew')
self._set_mastertoken(mastertoken)
self.set_mastertoken(mastertoken)
self._save_msl_data()

def _mastertoken_is_newer_that(self, mastertoken):
Expand All @@ -48,12 +56,12 @@ def _mastertoken_is_newer_that(self, mastertoken):

def parse_key_response(self, headerdata, save_to_disk):
"""Parse a key response and update crypto keys"""
self._set_mastertoken(headerdata['keyresponsedata']['mastertoken'])
self.set_mastertoken(headerdata['keyresponsedata']['mastertoken'])
self._init_keys(headerdata['keyresponsedata'])
if save_to_disk:
self._save_msl_data()

def _set_mastertoken(self, mastertoken):
def set_mastertoken(self, mastertoken):
"""Set the mastertoken and check it for validity"""
tokendata = json.loads(
base64.standard_b64decode(mastertoken['tokendata'].encode('utf-8')).decode('utf-8'))
Expand All @@ -65,9 +73,9 @@ def _set_mastertoken(self, mastertoken):

def _save_msl_data(self):
"""Save crypto keys and mastertoken to disk"""
msl_data = {'tokens': {'mastertoken': self.mastertoken}}
msl_data.update(self._export_keys())
common.save_file('msl_data.json', json.dumps(msl_data).encode('utf-8'))
self._msl_data['tokens'] = {'mastertoken': self.mastertoken}
self._msl_data.update(self._export_keys())
common.save_file('msl_data.json', json.dumps(self._msl_data).encode('utf-8'))
common.debug('Successfully saved MSL data to disk')

def _init_keys(self, key_response_data):
Expand All @@ -77,3 +85,32 @@ def _init_keys(self, key_response_data):
def _export_keys(self):
"""Export crypto keys to a dict"""
raise NotImplementedError

def get_user_id_token(self, profile_guid):
"""Get a valid the user id token associated to a profile guid"""
if 'user_id_tokens' in self._msl_data:
user_id_token = self._msl_data['user_id_tokens'].get(profile_guid)
if user_id_token and self.is_user_id_token_valid(user_id_token):
return user_id_token
return None

def save_user_id_token(self, profile_guid, user_token_id):
"""Save or update a user id token associated to a profile guid"""
if 'user_id_tokens' not in self._msl_data:
self._msl_data['user_id_tokens'] = {
profile_guid: user_token_id
}
else:
self._msl_data['user_id_tokens'][profile_guid] = user_token_id
self._save_msl_data()

def clear_user_id_tokens(self):
"""Clear all user id tokens"""
self._msl_data.pop('user_id_tokens', None)
self._save_msl_data()

def is_user_id_token_valid(self, user_id_token):
"""Check if user id token is not expired"""
token_data = json.loads(base64.standard_b64decode(user_id_token['tokendata']))
# Subtract 5min as a safety measure
return (token_data['expiration'] - 300) > time.time()
19 changes: 13 additions & 6 deletions resources/lib/services/msl/default_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"""
from __future__ import absolute_import, division, unicode_literals

import json
import base64
import json

try: # Python 3
from Crypto.Random import get_random_bytes
from Crypto.Hash import HMAC, SHA256
Expand All @@ -30,23 +31,29 @@
import resources.lib.common as common

from .base_crypto import MSLBaseCrypto
from .exceptions import MSLError


class DefaultMSLCrypto(MSLBaseCrypto):
"""Crypto Handler for non-Android platforms"""
def __init__(self, msl_data=None): # pylint: disable=super-on-old-class
# pylint: disable=broad-except

def __init__(self):
super(DefaultMSLCrypto, self).__init__()
self.rsa_key = None
self.encryption_key = None
self.sign_key = None

def load_crypto_session(self, msl_data=None):
try:
super(DefaultMSLCrypto, self).__init__(msl_data)
self.encryption_key = base64.standard_b64decode(
msl_data['encryption_key'])
self.sign_key = base64.standard_b64decode(
msl_data['sign_key'])
if not self.encryption_key or not self.sign_key:
raise ValueError('Missing encryption_key or sign_key')
raise MSLError('Missing encryption_key or sign_key')
self.rsa_key = RSA.importKey(
base64.standard_b64decode(msl_data['rsa_key']))
except Exception:
except Exception: # pylint: disable=broad-except
common.debug('Generating new RSA keys')
self.rsa_key = RSA.generate(2048)
self.encryption_key = None
Expand Down
99 changes: 0 additions & 99 deletions resources/lib/services/msl/event_tag_builder.py

This file was deleted.

Loading