Python : Add support for newer OMEMO (Thank you. Syndance);

Python : Fix Ad-Hoc Commands;
SQLite : Fix tagging mechanism.
This commit is contained in:
Schimon Jehudah, Adv. 2024-09-12 15:20:14 +03:00
parent 178f49cb86
commit c050c765dd
8 changed files with 216 additions and 60 deletions

View file

@ -47,7 +47,6 @@ dependencies = [
"feedparser", "feedparser",
"lxml", "lxml",
"python-dateutil", "python-dateutil",
"requests",
"slixmpp", "slixmpp",
"tomli", # Python 3.10 "tomli", # Python 3.10
"tomli_w", "tomli_w",
@ -59,14 +58,7 @@ Repository = "https://git.xmpp-it.net/sch/Slixfeed"
Issues = "https://gitgud.io/sjehuda/slixfeed/issues" Issues = "https://gitgud.io/sjehuda/slixfeed/issues"
[project.optional-dependencies] [project.optional-dependencies]
omemo = [ omemo = ["slixmpp-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",
]
proxy = ["pysocks"] proxy = ["pysocks"]
# [project.readme] # [project.readme]

View file

@ -366,7 +366,7 @@ def create_tables(db_file):
id INTEGER NOT NULL, id INTEGER NOT NULL,
feed_id INTEGER NOT NULL, feed_id INTEGER NOT NULL,
tag_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 UPDATE CASCADE
ON DELETE CASCADE, ON DELETE CASCADE,
FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") FOREIGN KEY ("tag_id") REFERENCES "tags" ("id")

View file

@ -1,2 +1,2 @@
__version__ = '0.1.95' __version__ = '0.1.96'
__version_info__ = (0, 1, 95) __version_info__ = (0, 1, 96)

View file

@ -154,18 +154,23 @@ class XmppChat:
# await compose.message(self, jid_bare, message) # await compose.message(self, jid_bare, message)
if self.omemo_present and self['xep_0384'].is_encrypted(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 = await XmppOmemo.decrypt(
command, omemo_decrypted, retry = await XmppOmemo.decrypt( self, message)
self, message, allow_untrusted)
if retry:
command, omemo_decrypted, retry = await XmppOmemo.decrypt(
self, message, allow_untrusted=True)
else: else:
omemo_decrypted = None omemo_decrypted = None
if message_type == 'groupchat': if message_type == 'groupchat':
command = command[1:] 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]) logger.debug([message_from.full, ':', command])
@ -363,7 +368,7 @@ class XmppChat:
chat_type = await XmppUtilities.get_chat_type(self, jid_bare) chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
if self.omemo_present and encrypted: if self.omemo_present and encrypted:
url_encrypted, omemo_encrypted = await XmppOmemo.encrypt( 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) XmppMessage.send_omemo_oob(self, message_from, url_encrypted, chat_type)
else: else:
XmppMessage.send_oob(self, jid_bare, url, chat_type) XmppMessage.send_oob(self, jid_bare, url, chat_type)
@ -632,7 +637,7 @@ class XmppChat:
encrypted = True if encrypt_omemo else False encrypted = True if encrypt_omemo else False
if self.omemo_present and encrypted and self['xep_0384'].is_encrypted(message): if self.omemo_present and encrypted and self['xep_0384'].is_encrypted(message):
response_encrypted, omemo_encrypted = await XmppOmemo.encrypt( response_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, message_from, response) self, message_from, 'chat', response)
if omemo_decrypted and omemo_encrypted: if omemo_decrypted and omemo_encrypted:
# message_from = message['from'] # message_from = message['from']
# message_type = message['type'] # message_type = message['type']
@ -737,7 +742,7 @@ class XmppChatAction:
if media_url and news_digest: if media_url and news_digest:
if self.omemo_present and encrypt_omemo: if self.omemo_present and encrypt_omemo:
news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt( 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: if self.omemo_present and encrypt_omemo and omemo_encrypted:
XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted) XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted)
else: else:
@ -777,7 +782,7 @@ class XmppChatAction:
# media_url_new = await XmppUpload.start( # media_url_new = await XmppUpload.start(
# self, jid_bare, Path(pathname), filesize, encrypted=encrypted) # self, jid_bare, Path(pathname), filesize, encrypted=encrypted)
media_url_new_encrypted, omemo_encrypted = await XmppOmemo.encrypt( 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: if media_url_new_encrypted and omemo_encrypted:
# NOTE Tested against Gajim. # NOTE Tested against Gajim.
# FIXME This only works with aesgcm URLs, and it does # FIXME This only works with aesgcm URLs, and it does
@ -801,7 +806,7 @@ class XmppChatAction:
if news_digest: if news_digest:
if self.omemo_present and encrypt_omemo: if self.omemo_present and encrypt_omemo:
news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt( 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: if self.omemo_present and encrypt_omemo and omemo_encrypted:
XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted) XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted)
else: else:

View file

@ -145,22 +145,40 @@ class XmppClient(slixmpp.ClientXMPP):
self.register_plugin('xep_0115') # Entity Capabilities self.register_plugin('xep_0115') # Entity Capabilities
self.register_plugin('xep_0122') # Data Forms Validation self.register_plugin('xep_0122') # Data Forms Validation
self.register_plugin('xep_0153') # vCard-Based Avatars 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_0203') # Delayed Delivery
self.register_plugin('xep_0249') # Direct MUC Invitations self.register_plugin('xep_0249') # Direct MUC Invitations
self.register_plugin('xep_0363') # HTTP File Upload 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_0402') # PEP Native Bookmarks
self.register_plugin('xep_0444') # Message Reactions self.register_plugin('xep_0444') # Message Reactions
try: try:
from slixfeed.xmpp.encryption import XmppOmemo
import slixmpp_omemo import slixmpp_omemo
from slixmpp_omemo import PluginCouldNotLoad
self.omemo_present = True self.omemo_present = True
except Exception as e: except Exception as e:
print('Encryption of type OMEMO is not enabled. Reason: ' + str(e)) print('Encryption of type OMEMO is not enabled. Reason: ' + str(e))
self.omemo_present = False 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: if self.omemo_present:
try: try:
self.register_plugin( self.register_plugin(
@ -177,7 +195,7 @@ class XmppClient(slixmpp.ClientXMPP):
except slixmpp.plugins.base.PluginNotFound: except slixmpp.plugins.base.PluginNotFound:
logger.error('Could not load xep_0454. Ensure you have ' logger.error('Could not load xep_0454. Ensure you have '
'\'cryptography\' from extras_require installed.') '\'cryptography\' from extras_require installed.')
"""
# proxy_enabled = config.get_value('accounts', 'XMPP', 'proxy_enabled') # proxy_enabled = config.get_value('accounts', 'XMPP', 'proxy_enabled')
# if proxy_enabled == '1': # if proxy_enabled == '1':
# values = config.get_value('accounts', 'XMPP', [ # values = config.get_value('accounts', 'XMPP', [
@ -870,6 +888,7 @@ class XmppClient(slixmpp.ClientXMPP):
# http://jabber.org/protocol/commands#actions # http://jabber.org/protocol/commands#actions
async def _handle_publish(self, iq, session): async def _handle_publish(self, iq, session):
jid = session['from']
jid_full = session['from'].full jid_full = session['from'].full
function_name = sys._getframe().f_code.co_name function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}' logger.debug('{}: jid_full: {}'
@ -911,6 +930,7 @@ class XmppClient(slixmpp.ClientXMPP):
return session return session
async def _handle_publish_action(self, payload, session): async def _handle_publish_action(self, payload, session):
jid = session['from']
jid_full = session['from'].full jid_full = session['from'].full
function_name = sys._getframe().f_code.co_name function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}' logger.debug('{}: jid_full: {}'
@ -1340,7 +1360,7 @@ class XmppClient(slixmpp.ClientXMPP):
form.add_field(label='Active', form.add_field(label='Active',
ftype='text-single', ftype='text-single',
value=feeds_act) 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', form.add_field(label='Items',
ftype='text-single', ftype='text-single',
value=entries) value=entries)
@ -1350,44 +1370,35 @@ class XmppClient(slixmpp.ClientXMPP):
value=unread) value=unread)
form.add_field(ftype='fixed', form.add_field(ftype='fixed',
label='Options') label='Options')
key_archive = Config.get_setting_value(self, jid_bare, 'archive') key_archive = str(Config.get_setting_value(self, jid_bare, 'archive'))
key_archive = str(key_archive)
form.add_field(label='Archive', form.add_field(label='Archive',
ftype='text-single', ftype='text-single',
value=key_archive) value=key_archive)
key_enabled = Config.get_setting_value(self, jid_bare, 'enabled') key_enabled = str(Config.get_setting_value(self, jid_bare, 'enabled'))
key_enabled = str(key_enabled)
form.add_field(label='Enabled', form.add_field(label='Enabled',
ftype='text-single', ftype='text-single',
value=key_enabled) value=key_enabled)
key_interval = Config.get_setting_value(self, jid_bare, 'interval') key_interval = str(Config.get_setting_value(self, jid_bare, 'interval'))
key_interval = str(key_interval)
form.add_field(label='Interval', form.add_field(label='Interval',
ftype='text-single', ftype='text-single',
value=key_interval) value=key_interval)
key_length = Config.get_setting_value(self, jid_bare, 'length') key_length = str(Config.get_setting_value(self, jid_bare, 'length'))
key_length = str(key_length)
form.add_field(label='Length', form.add_field(label='Length',
ftype='text-single', ftype='text-single',
value=key_length) value=key_length)
key_media = Config.get_setting_value(self, jid_bare, 'media') key_media = str(Config.get_setting_value(self, jid_bare, 'media'))
key_media = str(key_media)
form.add_field(label='Media', form.add_field(label='Media',
ftype='text-single', ftype='text-single',
value=key_media) value=key_media)
key_old = Config.get_setting_value(self, jid_bare, 'old') key_old = str(Config.get_setting_value(self, jid_bare, 'old'))
key_old = str(key_old)
form.add_field(label='Old', form.add_field(label='Old',
ftype='text-single', ftype='text-single',
value=key_old) value=key_old)
key_quantum = Config.get_setting_value(self, jid_bare, 'quantum') key_quantum = str(Config.get_setting_value(self, jid_bare, 'quantum'))
key_quantum = str(key_quantum)
form.add_field(label='Quantum', form.add_field(label='Quantum',
ftype='text-single', ftype='text-single',
value=key_quantum) value=key_quantum)
update_interval = Config.get_setting_value(self, jid_bare, 'interval') update_interval = 60 * Config.get_setting_value(self, jid_bare, 'interval')
update_interval = str(update_interval)
update_interval = 60 * int(update_interval)
last_update_time = sqlite.get_last_update_time(db_file) last_update_time = sqlite.get_last_update_time(db_file)
if last_update_time: if last_update_time:
last_update_time = float(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) chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
if encrypted: if encrypted:
url_encrypted, omemo_encrypted = await XmppOmemo.encrypt( 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) XmppMessage.send_omemo_oob(self, JID(jid_bare), url_encrypted, chat_type)
else: else:
XmppMessage.send_oob(self, jid_bare, url, chat_type) XmppMessage.send_oob(self, jid_bare, url, chat_type)

View file

@ -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 slixfeed.log import Logger
from slixmpp import JID from slixmpp import JID
from slixmpp.exceptions import IqTimeout, IqError from slixmpp.exceptions import IqTimeout, IqError
#from slixmpp.plugins import register_plugin
from slixmpp.stanza import Message from slixmpp.stanza import Message
from slixmpp_omemo import MissingOwnKey, EncryptionPrepareException from slixmpp_omemo import TrustLevel, XEP_0384
from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession from typing import Any, Dict, FrozenSet, Literal, Optional, Union
logger = Logger(__name__) logger = Logger(__name__)
# for task in main_task: # for task in main_task:
# task.cancel() # task.cancel()
@ -46,7 +47,59 @@ logger = Logger(__name__)
class XmppOmemo: 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'] jid = message['from']
try: try:
print('XmppOmemo.decrypt') print('XmppOmemo.decrypt')
@ -124,7 +177,7 @@ class XmppOmemo:
return response, omemo_decrypted, retry return response, omemo_decrypted, retry
async def encrypt(self, jid: JID, message_body): async def _encrypt(self, jid: JID, message_body):
print(jid) print(jid)
print(message_body) print(message_body)
expect_problems = {} # type: Optional[Dict[JID, List[int]]] expect_problems = {} # type: Optional[Dict[JID, List[int]]]
@ -192,3 +245,95 @@ class XmppOmemo:
raise raise
return message_body, omemo_encrypted 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)

View file

@ -41,14 +41,17 @@ class XmppMessage:
def send_omemo(self, jid: JID, chat_type, response_encrypted): def send_omemo(self, jid: JID, chat_type, response_encrypted):
jid_from = str(self.boundjid) if self.is_component else None # jid_from = str(self.boundjid) if self.is_component else None
message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type) # message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type)
eme_ns = 'eu.siacs.conversations.axolotl' # eme_ns = 'eu.siacs.conversations.axolotl'
# message['eme']['namespace'] = eme_ns # message['eme']['namespace'] = eme_ns
# message['eme']['name'] = self['xep_0380'].mechanisms[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['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() message.send()

View file

@ -66,7 +66,7 @@ class XmppUtilities:
access = True if XmppUtilities.is_moderator(self, room, alias) else False access = True if XmppUtilities.is_moderator(self, room, alias) else False
if access: print('Access granted to groupchat moderator ' + alias) if access: print('Access granted to groupchat moderator ' + alias)
else: else:
print('Access granted to chat ' + jid_bare) print('Access granted to chat jid ' + jid_bare)
access = True access = True
return access return access