diff --git a/pyproject.toml b/pyproject.toml index 0651eee..76ef4f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,6 @@ dependencies = [ "feedparser", "lxml", "python-dateutil", - "requests", "slixmpp", "tomli", # Python 3.10 "tomli_w", @@ -59,14 +58,7 @@ Repository = "https://git.xmpp-it.net/sch/Slixfeed" Issues = "https://gitgud.io/sjehuda/slixfeed/issues" [project.optional-dependencies] -omemo = [ - "DoubleRatchet>=0.7.0,<0.8", - "OMEMO>=0.13.0,<0.15", - "protobuf==3.20.3", - "slixmpp-omemo", - "X3DH>=0.5.9,<0.6", - "XEdDSA<0.5,>=0.4.7", -] +omemo = ["slixmpp-omemo"] proxy = ["pysocks"] # [project.readme] diff --git a/slixfeed/sqlite.py b/slixfeed/sqlite.py index a894230..52bd02c 100644 --- a/slixfeed/sqlite.py +++ b/slixfeed/sqlite.py @@ -366,7 +366,7 @@ def create_tables(db_file): id INTEGER NOT NULL, feed_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, - FOREIGN KEY ("feed_id") REFERENCES "feeds" ("id") + FOREIGN KEY ("feed_id") REFERENCES "feeds_properties" ("id") ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") diff --git a/slixfeed/version.py b/slixfeed/version.py index bde8b5b..da736d1 100644 --- a/slixfeed/version.py +++ b/slixfeed/version.py @@ -1,2 +1,2 @@ -__version__ = '0.1.95' -__version_info__ = (0, 1, 95) +__version__ = '0.1.96' +__version_info__ = (0, 1, 96) diff --git a/slixfeed/xmpp/chat.py b/slixfeed/xmpp/chat.py index 35758c7..1ccfab1 100644 --- a/slixfeed/xmpp/chat.py +++ b/slixfeed/xmpp/chat.py @@ -154,18 +154,23 @@ class XmppChat: # await compose.message(self, jid_bare, message) if self.omemo_present and self['xep_0384'].is_encrypted(message): - allow_untrusted=True # Temporary fix. This should be handled by "retry"" - command, omemo_decrypted, retry = await XmppOmemo.decrypt( - self, message, allow_untrusted) - if retry: - command, omemo_decrypted, retry = await XmppOmemo.decrypt( - self, message, allow_untrusted=True) + command, omemo_decrypted = await XmppOmemo.decrypt( + self, message) else: omemo_decrypted = None if message_type == 'groupchat': command = command[1:] - command_lowercase = command.lower() + + if isinstance(command, str): + command_lowercase = command.lower() + elif isinstance(command, Message): + command_lowercase = command['body'].lower() + + # This is a work-around to empty messages that are caused by function + # self.register_handler(CoroutineCallback( of module client.py. + # The code was taken from the cho bot xample of slixmpp-omemo. + #if not command_lowercase: return logger.debug([message_from.full, ':', command]) @@ -363,7 +368,7 @@ class XmppChat: chat_type = await XmppUtilities.get_chat_type(self, jid_bare) if self.omemo_present and encrypted: url_encrypted, omemo_encrypted = await XmppOmemo.encrypt( - self, message_from, url) + self, message_from, 'chat', url) XmppMessage.send_omemo_oob(self, message_from, url_encrypted, chat_type) else: XmppMessage.send_oob(self, jid_bare, url, chat_type) @@ -632,7 +637,7 @@ class XmppChat: encrypted = True if encrypt_omemo else False if self.omemo_present and encrypted and self['xep_0384'].is_encrypted(message): response_encrypted, omemo_encrypted = await XmppOmemo.encrypt( - self, message_from, response) + self, message_from, 'chat', response) if omemo_decrypted and omemo_encrypted: # message_from = message['from'] # message_type = message['type'] @@ -737,7 +742,7 @@ class XmppChatAction: if media_url and news_digest: if self.omemo_present and encrypt_omemo: news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt( - self, jid, news_digest) + self, jid, 'chat', news_digest) if self.omemo_present and encrypt_omemo and omemo_encrypted: XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted) else: @@ -777,7 +782,7 @@ class XmppChatAction: # media_url_new = await XmppUpload.start( # self, jid_bare, Path(pathname), filesize, encrypted=encrypted) media_url_new_encrypted, omemo_encrypted = await XmppOmemo.encrypt( - self, jid, media_url_new) + self, jid, 'chat', media_url_new) if media_url_new_encrypted and omemo_encrypted: # NOTE Tested against Gajim. # FIXME This only works with aesgcm URLs, and it does @@ -801,7 +806,7 @@ class XmppChatAction: if news_digest: if self.omemo_present and encrypt_omemo: news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt( - self, jid, news_digest) + self, jid, 'chat', news_digest) if self.omemo_present and encrypt_omemo and omemo_encrypted: XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted) else: diff --git a/slixfeed/xmpp/client.py b/slixfeed/xmpp/client.py index c05ac45..abebe27 100644 --- a/slixfeed/xmpp/client.py +++ b/slixfeed/xmpp/client.py @@ -145,22 +145,40 @@ class XmppClient(slixmpp.ClientXMPP): self.register_plugin('xep_0115') # Entity Capabilities self.register_plugin('xep_0122') # Data Forms Validation self.register_plugin('xep_0153') # vCard-Based Avatars - self.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping + self.register_plugin('xep_0199', # XMPP Ping + {'keepalive': True}) self.register_plugin('xep_0203') # Delayed Delivery self.register_plugin('xep_0249') # Direct MUC Invitations self.register_plugin('xep_0363') # HTTP File Upload + self.register_plugin('xep_0380') # Explicit Message Encryption self.register_plugin('xep_0402') # PEP Native Bookmarks self.register_plugin('xep_0444') # Message Reactions try: - from slixfeed.xmpp.encryption import XmppOmemo import slixmpp_omemo - from slixmpp_omemo import PluginCouldNotLoad self.omemo_present = True except Exception as e: print('Encryption of type OMEMO is not enabled. Reason: ' + str(e)) self.omemo_present = False + if self.omemo_present: + #from slixmpp.xmlstream.handler import CoroutineCallback + #from slixmpp.xmlstream.matcher import MatchXPath + #self.register_handler(CoroutineCallback( + # 'Messages', + # MatchXPath(f'{{{self.default_ns}}}message'), + # self.on_message # type: ignore[arg-type] + #)) + + from slixfeed.xmpp.encryption import XEP_0384Impl + from slixfeed.xmpp.encryption import XmppOmemo + import slixfeed.xmpp.encryption as slixfeed_xmpp_encryption + from slixmpp.plugins import register_plugin + register_plugin(XEP_0384Impl) + self.register_plugin('xep_0384', # OMEMO Encryption + module=XEP_0384Impl) + + """ if self.omemo_present: try: self.register_plugin( @@ -177,7 +195,7 @@ class XmppClient(slixmpp.ClientXMPP): except slixmpp.plugins.base.PluginNotFound: logger.error('Could not load xep_0454. Ensure you have ' '\'cryptography\' from extras_require installed.') - + """ # proxy_enabled = config.get_value('accounts', 'XMPP', 'proxy_enabled') # if proxy_enabled == '1': # values = config.get_value('accounts', 'XMPP', [ @@ -870,6 +888,7 @@ class XmppClient(slixmpp.ClientXMPP): # http://jabber.org/protocol/commands#actions async def _handle_publish(self, iq, session): + jid = session['from'] jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' @@ -911,6 +930,7 @@ class XmppClient(slixmpp.ClientXMPP): return session async def _handle_publish_action(self, payload, session): + jid = session['from'] jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' @@ -1340,7 +1360,7 @@ class XmppClient(slixmpp.ClientXMPP): form.add_field(label='Active', ftype='text-single', value=feeds_act) - entries = sqlite.get_number_of_items(db_file, 'entries_properties') + entries = str(sqlite.get_number_of_items(db_file, 'entries_properties')) form.add_field(label='Items', ftype='text-single', value=entries) @@ -1350,44 +1370,35 @@ class XmppClient(slixmpp.ClientXMPP): value=unread) form.add_field(ftype='fixed', label='Options') - key_archive = Config.get_setting_value(self, jid_bare, 'archive') - key_archive = str(key_archive) + key_archive = str(Config.get_setting_value(self, jid_bare, 'archive')) form.add_field(label='Archive', ftype='text-single', value=key_archive) - key_enabled = Config.get_setting_value(self, jid_bare, 'enabled') - key_enabled = str(key_enabled) + key_enabled = str(Config.get_setting_value(self, jid_bare, 'enabled')) form.add_field(label='Enabled', ftype='text-single', value=key_enabled) - key_interval = Config.get_setting_value(self, jid_bare, 'interval') - key_interval = str(key_interval) + key_interval = str(Config.get_setting_value(self, jid_bare, 'interval')) form.add_field(label='Interval', ftype='text-single', value=key_interval) - key_length = Config.get_setting_value(self, jid_bare, 'length') - key_length = str(key_length) + key_length = str(Config.get_setting_value(self, jid_bare, 'length')) form.add_field(label='Length', ftype='text-single', value=key_length) - key_media = Config.get_setting_value(self, jid_bare, 'media') - key_media = str(key_media) + key_media = str(Config.get_setting_value(self, jid_bare, 'media')) form.add_field(label='Media', ftype='text-single', value=key_media) - key_old = Config.get_setting_value(self, jid_bare, 'old') - key_old = str(key_old) + key_old = str(Config.get_setting_value(self, jid_bare, 'old')) form.add_field(label='Old', ftype='text-single', value=key_old) - key_quantum = Config.get_setting_value(self, jid_bare, 'quantum') - key_quantum = str(key_quantum) + key_quantum = str(Config.get_setting_value(self, jid_bare, 'quantum')) form.add_field(label='Quantum', ftype='text-single', value=key_quantum) - update_interval = Config.get_setting_value(self, jid_bare, 'interval') - update_interval = str(update_interval) - update_interval = 60 * int(update_interval) + update_interval = 60 * Config.get_setting_value(self, jid_bare, 'interval') last_update_time = sqlite.get_last_update_time(db_file) if last_update_time: last_update_time = float(last_update_time) @@ -2930,7 +2941,7 @@ class XmppClient(slixmpp.ClientXMPP): chat_type = await XmppUtilities.get_chat_type(self, jid_bare) if encrypted: url_encrypted, omemo_encrypted = await XmppOmemo.encrypt( - self, JID(jid_bare), url) + self, JID(jid_bare), 'chat', url) XmppMessage.send_omemo_oob(self, JID(jid_bare), url_encrypted, chat_type) else: XmppMessage.send_oob(self, jid_bare, url, chat_type) diff --git a/slixfeed/xmpp/encryption.py b/slixfeed/xmpp/encryption.py index f3f1154..8497070 100644 --- a/slixfeed/xmpp/encryption.py +++ b/slixfeed/xmpp/encryption.py @@ -23,18 +23,19 @@ TODO """ -from omemo.exceptions import MissingBundleException +import json +from omemo.storage import Just, Maybe, Nothing, Storage +from omemo.types import DeviceInformation, JSONType from slixfeed.log import Logger from slixmpp import JID from slixmpp.exceptions import IqTimeout, IqError +#from slixmpp.plugins import register_plugin from slixmpp.stanza import Message -from slixmpp_omemo import MissingOwnKey, EncryptionPrepareException -from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession - +from slixmpp_omemo import TrustLevel, XEP_0384 +from typing import Any, Dict, FrozenSet, Literal, Optional, Union logger = Logger(__name__) - # for task in main_task: # task.cancel() @@ -46,7 +47,59 @@ logger = Logger(__name__) class XmppOmemo: - async def decrypt(self, message: Message, allow_untrusted: bool = False): + async def decrypt(self, stanza: Message): + + omemo_decrypted = None + + mto = stanza["from"] + mtype = stanza["type"] + + namespace = self['xep_0384'].is_encrypted(stanza) + if namespace is None: + omemo_decrypted = False + response = f"Unencrypted message or unsupported message encryption: {stanza['body']}" + else: + print(f'Message in namespace {namespace} received: {stanza}') + try: + response, device_information = await self['xep_0384'].decrypt_message(stanza) + print(f'Information about sender: {device_information}') + omemo_decrypted = True + except Exception as e: # pylint: disable=broad-exception-caught + response = f'Error {type(e).__name__}: {e}' + + return response, omemo_decrypted + + + async def encrypt( + self, + mto: JID, + mtype: Literal['chat', 'normal'], + mbody: str + ) -> None: + + if isinstance(mbody, str): + reply = self.make_message(mto=mto, mtype=mtype) + reply['body'] = mbody + + reply.set_to(mto) + reply.set_from(self.boundjid) + + # It might be a good idea to strip everything except for the body from the stanza, + # since some things might break when echoed. + message, encryption_errors = await self['xep_0384'].encrypt_message(reply, mto) + + if len(encryption_errors) > 0: + print(f'There were non-critical errors during encryption: {encryption_errors}') + # log.info(f'There were non-critical errors during encryption: {encryption_errors}') + + # for namespace, message in messages.items(): + # message['eme']['namespace'] = namespace + # message['eme']['name'] = self['xep_0380'].mechanisms[namespace] + + return message, True + + + async def _decrypt(self, message: Message, allow_untrusted: bool = False): jid = message['from'] try: print('XmppOmemo.decrypt') @@ -124,7 +177,7 @@ class XmppOmemo: return response, omemo_decrypted, retry - async def encrypt(self, jid: JID, message_body): + async def _encrypt(self, jid: JID, message_body): print(jid) print(message_body) expect_problems = {} # type: Optional[Dict[JID, List[int]]] @@ -192,3 +245,95 @@ class XmppOmemo: raise return message_body, omemo_encrypted + + +class StorageImpl(Storage): + """ + Example storage implementation that stores all data in a single JSON file. + """ + + JSON_FILE = "/home/admin/omemo-echo-client.json" + + def __init__(self) -> None: + super().__init__() + + self.__data: Dict[str, JSONType] = {} + try: + with open(self.JSON_FILE, encoding="utf8") as f: + self.__data = json.load(f) + except Exception: # pylint: disable=broad-exception-caught + pass + + async def _load(self, key: str) -> Maybe[JSONType]: + if key in self.__data: + return Just(self.__data[key]) + + return Nothing() + + async def _store(self, key: str, value: JSONType) -> None: + self.__data[key] = value + with open(self.JSON_FILE, "w", encoding="utf8") as f: + json.dump(self.__data, f) + + async def _delete(self, key: str) -> None: + self.__data.pop(key, None) + with open(self.JSON_FILE, "w", encoding="utf8") as f: + json.dump(self.__data, f) + + +class XEP_0384Impl(XEP_0384): # pylint: disable=invalid-name + """ + Example implementation of the OMEMO plugin for Slixmpp. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=redefined-outer-name + super().__init__(*args, **kwargs) + + # Just the type definition here + self.__storage: Storage + + def plugin_init(self) -> None: + self.__storage = StorageImpl() + + super().plugin_init() + + @property + def storage(self) -> Storage: + return self.__storage + + @property + def _btbv_enabled(self) -> bool: + return True + + async def _devices_blindly_trusted( + self, + blindly_trusted: FrozenSet[DeviceInformation], + identifier: Optional[str] + ) -> None: + log.info(f"[{identifier}] Devices trusted blindly: {blindly_trusted}") + + async def _prompt_manual_trust( + self, + manually_trusted: FrozenSet[DeviceInformation], + identifier: Optional[str] + ) -> None: + # Since BTBV is enabled and we don't do any manual trust adjustments in the example, this method + # should never be called. All devices should be automatically trusted blindly by BTBV. + + # To show how a full implementation could look like, the following code will prompt for a trust + # decision using `input`: + session_mananger = await self.get_session_manager() + + for device in manually_trusted: + while True: + answer = input(f"[{identifier}] Trust the following device? (yes/no) {device}") + if answer in { "yes", "no" }: + await session_mananger.set_trust( + device.bare_jid, + device.identity_key, + TrustLevel.TRUSTED.value if answer == "yes" else TrustLevel.DISTRUSTED.value + ) + break + print("Please answer yes or no.") + +#register_plugin(XEP_0384Impl) diff --git a/slixfeed/xmpp/message.py b/slixfeed/xmpp/message.py index 9773ee0..72b9d2c 100644 --- a/slixfeed/xmpp/message.py +++ b/slixfeed/xmpp/message.py @@ -41,14 +41,17 @@ class XmppMessage: def send_omemo(self, jid: JID, chat_type, response_encrypted): - jid_from = str(self.boundjid) if self.is_component else None - message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type) - eme_ns = 'eu.siacs.conversations.axolotl' + # jid_from = str(self.boundjid) if self.is_component else None + # message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type) + # eme_ns = 'eu.siacs.conversations.axolotl' # message['eme']['namespace'] = eme_ns # message['eme']['name'] = self['xep_0380'].mechanisms[eme_ns] - message['eme'] = {'namespace': eme_ns} # message['eme'] = {'name': self['xep_0380'].mechanisms[eme_ns]} - message.append(response_encrypted) + # message['eme'] = {'namespace': eme_ns} + # message.append(response_encrypted) + for namespace, message in response_encrypted.items(): + message['eme']['namespace'] = namespace + message['eme']['name'] = self['xep_0380'].mechanisms[namespace] message.send() diff --git a/slixfeed/xmpp/utilities.py b/slixfeed/xmpp/utilities.py index aa9aeb6..8f983f1 100644 --- a/slixfeed/xmpp/utilities.py +++ b/slixfeed/xmpp/utilities.py @@ -66,7 +66,7 @@ class XmppUtilities: access = True if XmppUtilities.is_moderator(self, room, alias) else False if access: print('Access granted to groupchat moderator ' + alias) else: - print('Access granted to chat ' + jid_bare) + print('Access granted to chat jid ' + jid_bare) access = True return access