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)
예제 #2
0
    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'))
예제 #3
0
 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')
        })
예제 #6
0
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
예제 #7
0
    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')
예제 #8
0
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
예제 #9
0
 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
예제 #10
0
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
예제 #11
0
    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'])
예제 #12
0
 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
         }
     }]
예제 #13
0
    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}}]