def _provide_key_response(self, data): if not data: raise MSLError('Missing key response data') self.keyset_id = self.crypto_session.ProvideKeyResponse(data) # pylint: disable=assignment-from-none if not self.keyset_id: raise MSLError('Widevine CryptoSession provideKeyResponse failed') LOG.debug('Widevine CryptoSession provideKeyResponse successful') LOG.debug('keySetId: {}', self.keyset_id)
def __init__(self): super().__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') LOG.debug('Widevine CryptoSession successful constructed') except Exception as exc: # pylint: disable=broad-except import traceback LOG.error(traceback.format_exc()) raise MSLError('Failed to construct Widevine CryptoSession') from exc 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'), 'security_level': self.crypto_session.GetPropertyString('securityLevel') } if not drm_info['version']: # Possible cases where no data is obtained: # - Device with custom ROM or without Widevine support # - Using Kodi debug build with a InputStream Adaptive release build (yes users do it) raise MSLError('It was not possible to get the data from Widevine CryptoSession.\r\n' 'Your system is not Widevine certified or you have a wrong Kodi version installed.') G.LOCAL_DB.set_value('drm_system_id', drm_info['system_id'], TABLE_SESSION) G.LOCAL_DB.set_value('drm_security_level', drm_info['security_level'], TABLE_SESSION) G.LOCAL_DB.set_value('drm_hdcp_level', drm_info['hdcp_level'], TABLE_SESSION) LOG.debug('Widevine version: {}', drm_info['version']) if drm_info['system_id']: LOG.debug('Widevine CryptoSession system id: {}', drm_info['system_id']) else: LOG.warn('Widevine CryptoSession system id not obtained!') LOG.debug('Widevine CryptoSession security level: {}', drm_info['security_level']) wv_force_sec_lev = G.LOCAL_DB.get_value('widevine_force_seclev', WidevineForceSecLev.DISABLED, table=TABLE_SESSION) if wv_force_sec_lev != WidevineForceSecLev.DISABLED: LOG.warn('Widevine security level is forced to {} by user settings!', wv_force_sec_lev) LOG.debug('Widevine CryptoSession current hdcp level: {}', drm_info['hdcp_level']) LOG.debug('Widevine CryptoSession max hdcp level supported: {}', drm_info['hdcp_level_max']) LOG.debug('Widevine CryptoSession algorithms: {}', self.crypto_session.GetPropertyString('algorithms'))
def perform_key_handshake(self): """Perform a key handshake and initialize crypto keys""" esn = get_esn() if not esn: LOG.error('Cannot perform key handshake, missing ESN') return False LOG.info('Performing key handshake with ESN: {}', common.censure(esn) if len(esn) > 50 else esn) try: header, _ = _process_json_response( self._post(ENDPOINTS['manifest'], self.handshake_request(esn))) header_data = self.decrypt_header_data(header['headerdata'], False) self.crypto.parse_key_response(header_data, esn, True) except MSLError as exc: if exc.err_number == 207006 and common.get_system_platform( ) == 'android': msg = ( 'Request failed validation during key exchange\r\n' 'To try to solve this problem read the Wiki FAQ on add-on GitHub.' ) raise MSLError(msg) from exc raise # Delete all the user id tokens (are correlated to the previous mastertoken) self.crypto.clear_user_id_tokens() LOG.debug('Key handshake successful') return True
def sign(self, message): """Sign a message""" signature = self.crypto_session.Sign( bytearray(self.hmac_key_id), bytearray(message.encode('utf-8'))) if not signature: raise MSLError('Widevine CryptoSession sign failed!') return base64.standard_b64encode(signature).decode('utf-8')
def encrypt(self, plaintext, esn): # pylint: disable=unused-argument """ Encrypt the given Plaintext with the encryption key :param plaintext: :return: Serialized JSON String of the encryption Envelope """ from os import urandom init_vector = bytearray(urandom(16)) # Add PKCS5Padding pad = 16 - len(plaintext) % 16 padded_data = plaintext + ''.join([chr(pad)] * pad) encrypted_data = self.crypto_session.Encrypt( bytearray(self.key_id), bytearray(padded_data.encode('utf-8')), init_vector) if not encrypted_data: raise MSLError('Widevine CryptoSession encrypt failed!') return json.dumps({ 'version': 1, 'ciphertext': base64.standard_b64encode(encrypted_data).decode('utf-8'), 'sha256': 'AA==', 'keyid': base64.standard_b64encode(self.key_id).decode('utf-8'), # 'cipherspec' : 'AES/CBC/PKCS5Padding', 'iv': base64.standard_b64encode(init_vector).decode('utf-8') })
def _process_json_response(response): """Execute a post request and expect a JSON response""" try: return _raise_if_error(response.json()) except ValueError as exc: raise MSLError('Expected JSON response, got {}'.format( response.text)) from exc
def decrypt(self, init_vector, ciphertext): """Decrypt a ciphertext""" decrypted_data = self.crypto_session.Decrypt(self.key_id, ciphertext, init_vector) if not decrypted_data: raise MSLError('Widevine CryptoSession decrypt failed!') # remove PKCS5Padding pad = decrypted_data[len(decrypted_data) - 1] return decrypted_data[:-pad].decode('utf-8')
def _process_json_response(response): """Processes the response data by returning header and payloads in JSON format and check for possible MSL error""" try: data = json.loads('[' + response.replace('}{', '},{') + ']') # On 'data' list the first dict is always the header or the error payloads = [msg_part for msg_part in data if 'payload' in msg_part] return _raise_if_error(data[0]), payloads except ValueError as exc: LOG.error('Unable to load json data {}', response) raise MSLError('Unable to load json data') from exc
def load_crypto_session(self, msl_data=None): try: 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 MSLError('Missing encryption_key or sign_key') self.rsa_key = RSA.importKey( base64.standard_b64decode(msl_data['rsa_key'])) except Exception: # pylint: disable=broad-except LOG.debug('Generating new RSA keys') self.rsa_key = RSA.generate(2048) self.encryption_key = None self.sign_key = None
def _raise_if_error(decoded_response): raise_error = False # Catch a manifest/chunk error if any(key in decoded_response for key in ['error', 'errordata']): raise_error = True # Catch a license error if 'result' in decoded_response and isinstance( decoded_response.get('result'), list): if 'error' in decoded_response['result'][0]: raise_error = True if raise_error: LOG.error('Full MSL error information:') LOG.error(json.dumps(decoded_response)) raise MSLError(_get_error_details(decoded_response)) return decoded_response
def get_license(self, license_data): """ Requests and returns a license for the given challenge and sid :param license_data: The license data provided by isa :return: Base64 representation of the license key or False unsuccessful """ LOG.debug('Requesting license') challenge, sid = license_data.decode('utf-8').split('!') sid = base64.standard_b64decode(sid).decode('utf-8') timestamp = int(time.time() * 10000) xid = str(timestamp + 1610) params = [{ 'drmSessionId': sid, 'clientTime': int(timestamp / 10000), 'challengeBase64': challenge, 'xid': xid }] self.manifest_challenge = challenge endpoint_url = ENDPOINTS['license'] + create_req_params( 0, 'prefetch/license') try: response = self.msl_requests.chunked_request( endpoint_url, self.msl_requests.build_request_data(self.last_license_url, params, 'drmSessionId'), get_esn()) except MSLError as exc: if exc.err_number == '1044' and common.get_system_platform( ) == 'android': msg = ( 'This title is not available to watch instantly. Please try another title.\r\n' 'To try to solve this problem you can force "Widevine L3" from the add-on Expert settings.\r\n' 'More info in the Wiki FAQ on add-on GitHub.') raise MSLError(msg) from exc raise # This xid must be used also for each future Event request, until playback stops G.LOCAL_DB.set_value('xid', xid, TABLE_SESSION) self.licenses_xid.insert(0, xid) self.licenses_session_id.insert(0, sid) self.licenses_release_url.insert( 0, response[0]['links']['releaseLicense']['href']) if self.msl_requests.msl_switch_requested: self.msl_requests.msl_switch_requested = False self.bind_events() return base64.standard_b64decode(response[0]['licenseResponseBase64'])
def key_request_data(self): """Return a key request dict""" # No key update supported -> remove existing keys self.crypto_session.RemoveKeys() _key_request = self.crypto_session.GetKeyRequest( # pylint: disable=assignment-from-none bytes([10, 122, 0, 108, 56, 43]), 'application/xml', True, {}) if not _key_request: raise MSLError('Widevine CryptoSession getKeyRequest failed!') LOG.debug('Widevine CryptoSession getKeyRequest successful. Size: {}', len(_key_request)) _key_request = base64.standard_b64encode(_key_request).decode('utf-8') return [{ 'scheme': 'WIDEVINE', 'keydata': { 'keyrequest': _key_request } }]
def key_request_data(self): """Return a key request dict""" # No key update supported -> remove existing keys self.crypto_session.RemoveKeys() key_request = self.crypto_session.GetKeyRequest( # pylint: disable=assignment-from-none bytes([10, 122, 0, 108, 56, 43]), 'application/xml', True, dict()) if not key_request: raise MSLError('Widevine CryptoSession getKeyRequest failed!') LOG.debug('Widevine CryptoSession getKeyRequest successful. Size: {}', len(key_request)) # Save the key request (challenge data) required for manifest requests # Todo: to be implemented if/when it becomes mandatory key_request = base64.standard_b64encode(key_request).decode('utf-8') # G.LOCAL_DB.set_value('drm_session_challenge', key_request, TABLE_SESSION) return [{'scheme': 'WIDEVINE', 'keydata': {'keyrequest': key_request}}]