Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: 704d9aa3 by Philipp Hörist at 2019-02-11T19:38:24Z MUC: Parse ofrom and lower handler priority
The handler should run before decryption handlers
- - - - - f45a1278 by Philipp Hörist at 2019-02-11T20:00:18Z OpenPGP: Raise priority on message handler
- - - - - deba7180 by Philipp Hörist at 2019-02-11T20:01:07Z Add OMEMO (XEP-0384) module
- - - - - 4feb9d4a by Philipp Hörist at 2019-02-11T20:30:06Z Simplify AffiliationResult
- - - - - f5e499ce by Philipp Hörist at 2019-02-11T20:30:21Z Simplify MucConfigResult
- - - - - b63d59c2 by Philipp Hörist at 2019-02-11T20:30:47Z Simplify BlockingListResult
- - - - - 7d504905 by Philipp Hörist at 2019-02-11T20:30:59Z Simplify CommonResult
- - - - -
7 changed files:
- nbxmpp/dispatcher_nb.py - nbxmpp/modules/muc.py - + nbxmpp/modules/omemo.py - nbxmpp/modules/openpgp.py - nbxmpp/protocol.py - nbxmpp/structs.py - nbxmpp/util.py
Changes:
===================================== nbxmpp/dispatcher_nb.py ===================================== @@ -64,6 +64,7 @@ from nbxmpp.modules.location import Location from nbxmpp.modules.user_avatar import UserAvatar from nbxmpp.modules.bookmarks import Bookmarks from nbxmpp.modules.openpgp import OpenPGP +from nbxmpp.modules.omemo import OMEMO from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.util import get_properties_struct @@ -195,6 +196,7 @@ class XMPPDispatcher(PlugIn): self._modules['UserAvatar'] = UserAvatar(self._owner) self._modules['Bookmarks'] = Bookmarks(self._owner) self._modules['OpenPGP'] = OpenPGP(self._owner) + self._modules['OMEMO'] = OMEMO(self._owner)
for instance in self._modules.values(): for handler in instance.handlers:
===================================== nbxmpp/modules/muc.py ===================================== @@ -25,6 +25,7 @@ from nbxmpp.protocol import NS_MUC_REQUEST from nbxmpp.protocol import NS_MUC_ADMIN from nbxmpp.protocol import NS_MUC_OWNER from nbxmpp.protocol import NS_CAPTCHA +from nbxmpp.protocol import NS_ADDRESS from nbxmpp.protocol import JID from nbxmpp.protocol import Iq from nbxmpp.protocol import Message @@ -71,7 +72,7 @@ class MUC: StanzaHandler(name='message', callback=self._process_groupchat_message, typ='groupchat', - priority=11), + priority=6), StanzaHandler(name='message', callback=self._process_mediated_invite, typ='normal', @@ -160,11 +161,17 @@ class MUC: def _process_groupchat_message(self, _con, stanza, properties): properties.from_muc = True properties.muc_nickname = properties.jid.getResource() or None - + muc_user = stanza.getTag('x', namespace=NS_MUC_USER) if muc_user is not None: properties.muc_user = self._parse_muc_user(muc_user)
+ addresses = stanza.getTag('addresses', namespace=NS_ADDRESS) + if addresses is not None: + address = addresses.getTag('address', attrs={'type': 'ofrom'}) + if address is not None: + properties.muc_ofrom = JID(address.getAttr('jid')) + @staticmethod def _process_message(_con, stanza, properties): muc_user = stanza.getTag('x', namespace=NS_MUC_USER) @@ -293,18 +300,12 @@ class MUC: iq = Iq(typ='get', to=jid, queryNS=NS_MUC_ADMIN) item = iq.setQuery().setTag('item') item.setAttr('affiliation', affiliation) - return iq, {'affiliation': affiliation} + return iq
@callback - def _affiliation_received(self, stanza, affiliation): + def _affiliation_received(self, stanza): if not isResultNode(stanza): - log.info('Affiliation error: %s %s %s', - stanza.getFrom(), - stanza.getError(), - affiliation) - return AffiliationResult(jid=stanza.getFrom(), - affiliation=affiliation, - error=stanza.getError()) + return raise_error(log.info, stanza)
room_jid = stanza.getFrom() query = stanza.getTag('query', namespace=NS_MUC_ADMIN) @@ -326,12 +327,10 @@ class MUC: if reason: users_dict[jid]['reason'] = reason
- log.info('%s affiliations received from %s: %s', - affiliation, room_jid, users_dict) + log.info('Affiliations received from %s: %s', + room_jid, users_dict)
- return AffiliationResult(jid=room_jid, - affiliation=affiliation, - users=users_dict) + return AffiliationResult(jid=room_jid, users=users_dict)
@call_on_response('_default_response') def destroy(self, room_jid, reason='', jid=''): @@ -365,9 +364,7 @@ class MUC: @callback def _config_received(self, stanza): if not isResultNode(stanza): - log.info('Error: %s', stanza.getError()) - return MucConfigResult(jid=stanza.getFrom(), - error=stanza.getError()) + return raise_error(log.info, stanza)
jid = stanza.getFrom() payload = stanza.getQueryPayload()
===================================== nbxmpp/modules/omemo.py ===================================== @@ -0,0 +1,443 @@ +# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +import logging + +from nbxmpp.protocol import NS_OMEMO_TEMP +from nbxmpp.protocol import NS_OMEMO_TEMP_DL +from nbxmpp.protocol import NS_OMEMO_TEMP_BUNDLE +from nbxmpp.protocol import NS_PUBSUB_EVENT +from nbxmpp.protocol import NS_EME +from nbxmpp.protocol import NS_HINTS +from nbxmpp.protocol import NodeProcessed +from nbxmpp.protocol import Node +from nbxmpp.protocol import isResultNode +from nbxmpp.protocol import StanzaMalformed +from nbxmpp.util import call_on_response +from nbxmpp.util import callback +from nbxmpp.util import b64decode +from nbxmpp.util import b64encode +from nbxmpp.util import raise_error +from nbxmpp.structs import StanzaHandler +from nbxmpp.structs import OMEMOMessage +from nbxmpp.structs import OMEMOBundle +from nbxmpp.modules.pubsub import get_pubsub_request + +log = logging.getLogger('nbxmpp.m.omemo') + + +class OMEMO: + def __init__(self, client): + self._client = client + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_omemo_devicelist, + ns=NS_PUBSUB_EVENT, + priority=16), + StanzaHandler(name='message', + callback=self._process_omemo_message, + ns=NS_OMEMO_TEMP, + priority=7), + ] + + def _process_omemo_message(self, _con, stanza, properties): + try: + properties.omemo = self._parse_omemo_message(stanza) + log.info('Received message') + except StanzaMalformed as error: + log.warning(error) + log.warning(stanza) + return + + @staticmethod + def _parse_omemo_message(stanza): + ''' + <message> + <encrypted xmlns='eu.siacs.conversations.axolotl'> + <header sid='27183'> + <key rid='31415'>BASE64ENCODED...</key> + <key prekey="true" rid='12321'>BASE64ENCODED...</key> + <!-- ... --> + <iv>BASE64ENCODED...</iv> + </header> + <payload>BASE64ENCODED</payload> + </encrypted> + <store xmlns='urn:xmpp:hints'/> + </message> + ''' + encrypted = stanza.getTag('encrypted', namespace=NS_OMEMO_TEMP) + if encrypted is None: + raise StanzaMalformed('No encrypted node found') + + header = encrypted.getTag('header') + if header is None: + raise StanzaMalformed('header node not found') + + try: + sid = int(header.getAttr('sid')) + except Exception as error: + raise StanzaMalformed('sid attr not found') + + iv_node = header.getTag('iv') + try: + iv = b64decode(iv_node.getData(), bytes) + except Exception as error: + raise StanzaMalformed('failed to decode iv: %s' % error) + + payload = None + payload_node = encrypted.getTag('payload') + if payload_node is not None: + try: + payload = b64decode(payload_node.getData(), bytes) + except Exception as error: + raise StanzaMalformed('failed to decode payload: %s' % error) + + key_nodes = header.getTags('key') + if not key_nodes: + raise StanzaMalformed('no keys found') + + keys = {} + for kn in key_nodes: + rid = kn.getAttr('rid') + if rid is None: + raise StanzaMalformed('rid not found') + + prekey = kn.getAttr('prekey') == 'true' + + try: + keys[int(rid)] = (b64decode(kn.getData(), bytes), prekey) + except Exception as error: + raise StanzaMalformed('failed to decode key: %s' % error) + + return OMEMOMessage(sid=sid, iv=iv, keys=keys, payload=payload) + + def _process_omemo_devicelist(self, _con, stanza, properties): + if not properties.is_pubsub_event: + return + + if properties.pubsub_event.node != NS_OMEMO_TEMP_DL: + return + + if properties.pubsub_event.retracted: + # Retracts should not happen and its unclear how we should react + raise NodeProcessed + + if properties.pubsub_event.deleted: + log.info('Devicelist node deleted by %s', properties.jid) + return + + item = properties.pubsub_event.item + try: + devices = self._parse_devicelist(item) + except StanzaMalformed as error: + log.warning(error) + log.warning(stanza) + raise NodeProcessed + + if not devices: + pubsub_event = properties.pubsub_event._replace(empty=True) + log.info('Received OMEMO devicelist: %s - no devices set', + properties.jid) + else: + pubsub_event = properties.pubsub_event._replace(data=devices) + log.info('Received OMEMO devicelist: %s - %s', + properties.jid, devices) + + properties.pubsub_event = pubsub_event + + @staticmethod + def _parse_devicelist(item): + ''' + <items node='eu.siacs.conversations.axolotl.devicelist'> + <item id='current'> + <list xmlns='eu.siacs.conversations.axolotl'> + <device id='12345' /> + <device id='4223' /> + </list> + </item> + </items> + ''' + list_node = item.getTag('list', namespace=NS_OMEMO_TEMP) + if list_node is None: + raise StanzaMalformed('No list node found') + + if not list_node.getChildren(): + return [] + + result = [] + devices_nodes = list_node.getChildren() + for dn in devices_nodes: + _id = dn.getAttr('id') + if _id: + result.append(int(_id)) + + return result + + def set_devicelist(self, devicelist=None): + item = Node('list', attrs={'xmlns': NS_OMEMO_TEMP}) + for device in devicelist: + item.addChild('device').setAttr('id', device) + + log.info('Set devicelist: %s', devicelist) + jid = self._client.get_bound_jid().getBare() + self._client.get_module('PubSub').publish( + jid, NS_OMEMO_TEMP_DL, item, id_='current') + + @call_on_response('_devicelist_received') + def request_devicelist(self, jid=None): + if jid is None: + jid = self._client.get_bound_jid().getBare() + log.info('Request devicelist from: %s', jid) + return get_pubsub_request(jid, NS_OMEMO_TEMP_DL, max_items=1) + + @callback + def _devicelist_received(self, stanza): + if not isResultNode(stanza): + return raise_error(log.info, stanza) + + pubsub_node = stanza.getTag('pubsub') + items_node = pubsub_node.getTag('items') + item = items_node.getTag('item') + + try: + return self._parse_devicelist(item) + except StanzaMalformed as error: + return raise_error(log.warning, stanza, + 'stanza-malformed', error) + + def set_bundle(self, bundle, device_id): + item = self._create_bundle(bundle) + log.info('Set bundle') + + node = '%s:%s' % (NS_OMEMO_TEMP_BUNDLE, device_id) + jid = self._client.get_bound_jid().getBare() + self._client.get_module('PubSub').publish( + jid, node, item, id_='current') + + @staticmethod + def _create_bundle(bundle): + ''' + <publish node='eu.siacs.conversations.axolotl.bundles:31415'> + <item id='current'> + <bundle xmlns='eu.siacs.conversations.axolotl'> + <signedPreKeyPublic signedPreKeyId='1'> + BASE64ENCODED... + </signedPreKeyPublic> + <signedPreKeySignature> + BASE64ENCODED... + </signedPreKeySignature> + <identityKey> + BASE64ENCODED... + </identityKey> + <prekeys> + <preKeyPublic preKeyId='1'> + BASE64ENCODED... + </preKeyPublic> + <preKeyPublic preKeyId='2'> + BASE64ENCODED... + </preKeyPublic> + <preKeyPublic preKeyId='3'> + BASE64ENCODED... + </preKeyPublic> + <!-- ... --> + </prekeys> + </bundle> + </item> + </publish> + ''' + bundle_node = Node('bundle', attrs={'xmlns': NS_OMEMO_TEMP}) + prekey_pub_node = bundle_node.addChild( + 'signedPreKeyPublic', + attrs={'signedPreKeyId': bundle.spk['id']}) + prekey_pub_node.addData(b64encode(bundle.spk['key'])) + + prekey_sig_node = bundle_node.addChild('signedPreKeySignature') + prekey_sig_node.addData(b64encode(bundle.spk_signature)) + + identity_key_node = bundle_node.addChild('identityKey') + identity_key_node.addData(b64encode(bundle.ik)) + + prekeys = bundle_node.addChild('prekeys') + for key in bundle.otpks: + pre_key_public = prekeys.addChild('preKeyPublic', + attrs={'preKeyId': key['id']}) + pre_key_public.addData(b64encode(key['key'])) + return bundle_node + + @call_on_response('_bundle_received') + def request_bundle(self, jid, device_id): + log.info('Request bundle from: %s %s', jid, device_id) + node = '%s:%s' % (NS_OMEMO_TEMP_BUNDLE, device_id) + return get_pubsub_request(jid, node, max_items=1) + + @callback + def _bundle_received(self, stanza): + if not isResultNode(stanza): + return raise_error(log.info, stanza) + + pubsub_node = stanza.getTag('pubsub') + items_node = pubsub_node.getTag('items') + item = items_node.getTag('item') + + try: + return self._parse_bundle(item) + except StanzaMalformed as error: + return raise_error(log.warning, stanza, + 'stanza-malformed', error) + + @staticmethod + def _parse_bundle(item): + ''' + <item id='current'> + <bundle xmlns='eu.siacs.conversations.axolotl'> + <signedPreKeyPublic signedPreKeyId='1'> + BASE64ENCODED... + </signedPreKeyPublic> + <signedPreKeySignature> + BASE64ENCODED... + </signedPreKeySignature> + <identityKey> + BASE64ENCODED... + </identityKey> + <prekeys> + <preKeyPublic preKeyId='1'> + BASE64ENCODED... + </preKeyPublic> + <preKeyPublic preKeyId='2'> + BASE64ENCODED... + </preKeyPublic> + <preKeyPublic preKeyId='3'> + BASE64ENCODED... + </preKeyPublic> + <!-- ... --> + </prekeys> + </bundle> + </item> + ''' + bundle = item.getTag('bundle', namespace=NS_OMEMO_TEMP) + if bundle is None: + raise StanzaMalformed('No bundle node found') + + result = {} + signed_prekey_node = bundle.getTag('signedPreKeyPublic') + try: + result['spk'] = {'key': b64decode(signed_prekey_node.getData(), bytes)} + except Exception as error: + raise StanzaMalformed('Failed to decode ' + 'signedPreKeyPublic: %s' % error) + + signed_prekey_id = signed_prekey_node.getAttr('signedPreKeyId') + try: + result['spk']['id'] = int(signed_prekey_id) + except Exception as error: + raise StanzaMalformed('Invalid signedPreKeyId: %s' % error) + + signed_signature_node = bundle.getTag('signedPreKeySignature') + try: + result['spk_signature'] = b64decode(signed_signature_node.getData(), bytes) + except Exception as error: + raise StanzaMalformed('Failed to decode ' + 'signedPreKeySignature: %s' % error) + + identity_key_node = bundle.getTag('identityKey') + try: + result['ik'] = b64decode(identity_key_node.getData(), bytes) + except Exception as error: + raise StanzaMalformed('Failed to decode ' + 'signedPreKeySignature: %s' % error) + + prekeys = bundle.getTag('prekeys') + if prekeys is None or not prekeys.getChildren(): + raise StanzaMalformed('No prekeys node found') + + result['otpks'] = [] + for prekey in prekeys.getChildren(): + try: + id_ = int(prekey.getAttr('preKeyId')) + except Exception as error: + raise StanzaMalformed('Invalid prekey: %s' % error) + + try: + key = b64decode(prekey.getData(), bytes) + except Exception as error: + raise StanzaMalformed('Failed to decode preKeyPublic: %s' % error) + + result['otpks'].append({'key': key, 'id': id_}) + + return OMEMOBundle(**result) + + +def create_omemo_message(stanza, omemo_message, store_hint=True, + node_whitelist=None): + ''' + <message> + <encrypted xmlns='eu.siacs.conversations.axolotl'> + <header sid='27183'> + <key rid='31415'>BASE64ENCODED...</key> + <key prekey="true" rid='12321'>BASE64ENCODED...</key> + <!-- ... --> + <iv>BASE64ENCODED...</iv> + </header> + <payload>BASE64ENCODED</payload> + </encrypted> + <store xmlns='urn:xmpp:hints'/> + </message> + ''' + + if node_whitelist is not None: + cleanup_stanza(stanza, node_whitelist) + + encrypted = Node('encrypted', attrs={'xmlns': NS_OMEMO_TEMP}) + header = Node('header', attrs={'sid': omemo_message.sid}) + for rid, (key, prekey) in omemo_message.keys.items(): + attrs = {'rid': rid} + if prekey: + attrs['prekey'] = 'true' + child = header.addChild('key', attrs=attrs) + child.addData(b64encode(key)) + + header.addChild('iv').addData(b64encode(omemo_message.iv)) + encrypted.addChild(node=header) + + payload = encrypted.addChild('payload') + payload.addData(b64encode(omemo_message.payload)) + + stanza.addChild(node=encrypted) + + stanza.addChild(node=Node('encryption', attrs={'xmlns': NS_EME, + 'name': 'OMEMO', + 'namespace': NS_OMEMO_TEMP})) + + stanza.setBody("You received a message encrypted with " + "OMEMO but your client doesn't support OMEMO.") + + if store_hint: + stanza.addChild(node=Node('store', attrs={'xmlns': NS_HINTS})) + + +def cleanup_stanza(stanza, node_whitelist): + whitelisted_nodes = [] + for tag, ns in node_whitelist: + node = stanza.getTag(tag, namespace=ns) + if node is not None: + whitelisted_nodes.append(node) + + for node in list(stanza.getChildren()): + stanza.delChild(node) + + for node in whitelisted_nodes: + stanza.addChild(node=node)
===================================== nbxmpp/modules/openpgp.py ===================================== @@ -56,7 +56,7 @@ class OpenPGP: StanzaHandler(name='message', callback=self._process_openpgp_message, ns=NS_OPENPGP, - priority=6), + priority=7), ]
def _process_openpgp_message(self, _con, stanza, properties):
===================================== nbxmpp/protocol.py ===================================== @@ -121,7 +121,6 @@ NS_MUC_CONFIG = NS_MUC + '#roomconfig' NS_MUC_REQUEST = NS_MUC + '#request' NS_NICK = 'http://jabber.org/protocol/nick' # XEP-0172 NS_OFFLINE = 'http://www.jabber.org/jeps/jep-0030.html' # XEP-0013 -NS_OMEMO = 'urn:xmpp:omemo:0' # XEP-0384 NS_PHYSLOC = 'http://jabber.org/protocol/physloc' # XEP-0112 NS_PING = 'urn:xmpp:ping' # XEP-0199 NS_PRESENCE = 'presence' # Jabberd2 @@ -191,8 +190,12 @@ NS_HASHES_BLAKE2B_512 = 'urn:xmpp:hash-function-text-names:id-blake2b512' NS_OPENPGP = 'urn:xmpp:openpgp:0' NS_OPENPGP_PK = 'urn:xmpp:openpgp:0:public-keys' NS_OPENPGP_SK = 'urn:xmpp:openpgp:0:secret-key' +NS_OMEMO_TEMP = 'eu.siacs.conversations.axolotl' +NS_OMEMO_TEMP_DL = 'eu.siacs.conversations.axolotl.devicelist' +NS_OMEMO_TEMP_BUNDLE = 'eu.siacs.conversations.axolotl.bundles' NS_BOOKMARK_CONVERSION = 'urn:xmpp:bookmarks-conversion:0' NS_DOMAIN_BASED_NAME = 'urn:xmpp:domain-based-name:1' +NS_HINTS = 'urn:xmpp:hints'
#xmpp_stream_error_conditions = ''' #bad-format -- -- -- The entity has sent XML that cannot be processed.
===================================== nbxmpp/structs.py ===================================== @@ -16,6 +16,7 @@ # along with this program; If not, see http://www.gnu.org/licenses/.
import time +import random from collections import namedtuple
from nbxmpp.protocol import JID @@ -33,6 +34,9 @@ StanzaHandler = namedtuple('StanzaHandler', 'name callback typ ns xmlns system priority') StanzaHandler.__new__.__defaults__ = ('', '', None, False, 50)
+CommonResult = namedtuple('CommonResult', 'jid') +CommonResult.__new__.__defaults__ = (None,) + InviteData = namedtuple('InviteData', 'muc from_ reason password type continued thread')
@@ -50,6 +54,11 @@ MucUserData.__new__.__defaults__ = (None, None, None, None, None) MucDestroyed = namedtuple('MucDestroyed', 'alternate reason password') MucDestroyed.__new__.__defaults__ = (None, None, None)
+MucConfigResult = namedtuple('MucConfigResult', 'jid form') +MucConfigResult.__new__.__defaults__ = (None,) + +AffiliationResult = namedtuple('AffiliationResult', 'jid users') + EntityCapsData = namedtuple('EntityCapsData', 'hash node ver') EntityCapsData.__new__.__defaults__ = (None, None, None)
@@ -78,10 +87,19 @@ AvatarData.__new__.__defaults__ = (None,) * len(AvatarData._fields) BookmarkData = namedtuple('BookmarkData', 'jid name nick autojoin password') BookmarkData.__new__.__defaults__ = (None, None, None, None)
+BlockingListResult = namedtuple('BlockingListResult', 'blocking_list') + PGPPublicKey = namedtuple('PGPPublicKey', 'jid key date')
PGPKeyMetadata = namedtuple('PGPKeyMetadata', 'jid fingerprint date')
+OMEMOMessage = namedtuple('OMEMOMessage', 'sid iv keys payload') + + +class OMEMOBundle(namedtuple('OMEMOBundle', 'spk spk_signature ik otpks')): + def pick_prekey(self): + return random.SystemRandom().choice(self.otpks) +
class CommonError(namedtuple('CommonError', 'type message')): def __str__(self): @@ -146,6 +164,7 @@ class MessageProperties: self.muc_invite = None self.muc_decline = None self.muc_user = None + self.muc_ofrom = None self.captcha = None self.voice_request = None self.self_message = False @@ -153,12 +172,17 @@ class MessageProperties: self.pubsub = False self.pubsub_event = None self.openpgp = None + self.omemo = None self.encrypted = None
@property def is_encrypted(self): return self.encrypted is not None
+ @property + def is_omemo(self): + return self.omemo is not None + @property def is_openpgp(self): return self.openpgp is not None @@ -348,36 +372,3 @@ class ErrorProperties:
def __str__(self): return '%s %s' % (self.type, self.message) - - -class BaseResult: - @property - def is_error(self): - return self.error is not None - - -class CommonResult(BaseResult, namedtuple('CommonResult', 'jid error')): - pass - -CommonResult.__new__.__defaults__ = (None,) - - -class AffiliationResult(BaseResult, namedtuple('AffiliationResult', - 'jid affiliation users error')): - pass - -AffiliationResult.__new__.__defaults__ = (None, None) - - -class MucConfigResult(BaseResult, namedtuple('MucConfigResult', - 'jid form error')): - pass - -MucConfigResult.__new__.__defaults__ = (None, None) - - -class BlockingListResult(BaseResult, namedtuple('BlockingListResult', - 'blocking_list error')): - pass - -BlockingListResult.__new__.__defaults__ = (None,)
===================================== nbxmpp/util.py ===================================== @@ -36,6 +36,8 @@ log = logging.getLogger('nbxmpp.util')
def b64decode(data, return_type=str): + if not data: + raise ValueError('No data to decode') if isinstance(data, str): data = data.encode() result = base64.b64decode(data) @@ -45,6 +47,8 @@ def b64decode(data, return_type=str):
def b64encode(data, return_type=str): + if not data: + raise ValueError('No data to encode') if isinstance(data, str): data = data.encode() result = base64.b64encode(data)
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/compare/988398bbd01453a314bbe47e2c...