From 1c71637adc1a3103ad18fcd1b6385e9348cb3423 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Thu, 6 Oct 2022 03:57:18 +0300 Subject: [PATCH] Gajim 1.3 support [WIP] --- manifest.ini | 10 ++++----- module.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ otr.py | 34 +++++++++++++++++----------- plugin.py | 39 ++++++++++++++++++++------------ 4 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 module.py diff --git a/manifest.ini b/manifest.ini index 393ab44..fdc2db2 100644 --- a/manifest.ini +++ b/manifest.ini @@ -1,9 +1,9 @@ [info] name: otrplugin short_name: otrplugin -version: 0.3.2 +version: 0.4 description: Off-the-Record encryption -authors: Pavel R -homepage: https://dev.narayana.im/gajim-otrplugin -min_gajim_version: 1.0.3 -max_gajim_version: 1.1.99 +authors: Pavel R , Bohdan Horbeshko +homepage: https://dev.narayana.im/narayana/gajim-otrplugin +min_gajim_version: 1.3.0 +max_gajim_version: 1.3.99 diff --git a/module.py b/module.py new file mode 100644 index 0000000..b4b718c --- /dev/null +++ b/module.py @@ -0,0 +1,63 @@ +## Copyright (C) 2008-2012 Kjell Braden +## Copyright (C) 2019 Pavel R. +## Copyright (C) 2022 Bohdan Horbeshko + +# This file is part of Gajim OTR Plugin. +# +# Gajim OTR Plugin 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; version 3 only. +# +# This software 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 can always obtain full license text at . + +from nbxmpp.structs import StanzaHandler +from gajim.common.modules.base import BaseModule +from gajim.common import app +from .otr import OTR as OTRInstance + +name = 'OTR' +zeroconf = False + +class OTR(BaseModule): + + _nbxmpp_extends = 'OTR' + + def __init__(self, con): + BaseModule.__init__(self, con, plugin=True) + + self.handlers = [ + StanzaHandler(name='message', + callback=self._message_received, + priority=9), + ] + + self.available = True + self.otr = OTRInstance(con.name) + + def activate(self): + """ Method called when the Plugin is activated in the PluginManager + """ + pass + + def deactivate(self): + """ Method called when the Plugin is deactivated in the PluginManager + """ + pass + + def _message_received(self, client, stanza, properties): + if properties.is_omemo or properties.is_openpgp or properties.is_pgp_legacy: return # skip other encryptions + if properties.from_muc: return # skip MUC messages + msgtxt = stanza.getBody() or '' + # if (event.encrypted) or (event.name[0:2] == 'gc') or not (event.msgtxt or '').startswith('?OTR'): return # skip non-OTR messages.. + if not msgtxt.startswith('?OTR'): return # skip non-OTR messages.. + if properties.mam != None: return stanza.setBody('') # skip MAM messages (we can not decrypt OTR out of session).. + if (app.settings.get_contact_setting(self._account,properties.jid.bare,'encryption')!=self._nbxmpp_extends): return # skip all when encryption not set to OTR + self.otr.decrypt(stanza,properties) + +def get_instance(*args, **kwargs): + return OTR(*args, **kwargs), 'OTR' diff --git a/otr.py b/otr.py index 75f09e7..483fd3f 100644 --- a/otr.py +++ b/otr.py @@ -15,9 +15,12 @@ # # You can always obtain full license text at . import os +import string +import random +import itertools +import logging from inspect import signature from gajim.common import const, app, helpers, configpaths -from gajim.session import ChatControlSession from nbxmpp.protocol import Message, JID from potr import context, crypt, proto from .keystore import Keystore @@ -28,7 +31,7 @@ class OTRChannel(context.Context): # this method may be called self.sendMessage() when we need to send some data to our via XMPP def inject(self,msg,appdata=None): stanza = Message(to=self.peer, body=msg.decode(), typ='chat') - stanza.setThread(appdata or ChatControlSession.generate_thread_id(None)) + stanza.setThread(appdata or self.generateThreadId()) self.user.stream.send_stanza(stanza) # this method called on channel state change @@ -46,6 +49,13 @@ class OTRChannel(context.Context): @staticmethod def getPolicy(policy): return OTR.DEFAULT_POLICY.get(policy) + @staticmethod + def generateThreadId(): + return ''.join( + [f(string.ascii_letters) for f in itertools.repeat( + random.choice, 32)] + ) + # OTR instance for Gajim user (Alice) class OTR(context.Account): PROTO = ('XMPP', 1024) @@ -69,10 +79,9 @@ class OTR(context.Account): crypt.InvalidParameterError: "unable to decrypt message (key/signature mismatch)", } - def __init__(self,plugin,account): + def __init__(self,account): super(OTR,self).__init__(account,*OTR.PROTO) - self.plugin = plugin - self.log = plugin.log + self.log = logging.getLogger('gajim.p.otr.otr') self.account = account self.stream = app.connections[account] self.jid = self.stream.get_own_jid() @@ -109,19 +118,18 @@ class OTR(context.Account): for fingerprint,trust in fingerprints.items(): self.keystore.save(jid=peer,fingerprint=fingerprint,trust=trust) # decrypt message - def decrypt(self,event,callback): - peer = event.stanza.getFrom() + def decrypt(self,stanza,properties): + peer = stanza.getFrom() + msgtxt = stanza.getBody() channel, ctl = self.getContext(peer), self.getControl(peer) try: - text, tlvs = channel.receiveMessage(event.msgtxt.encode(),appdata=event.stanza.getThread()) or b'' + text, tlvs = channel.receiveMessage(msgtxt.encode(),appdata=stanza.getThread()) or b'' except (context.UnencryptedMessage,context.NotEncryptedError,context.ErrorReceived,crypt.InvalidParameterError) as e: self.log.error("** got exception while decrypting message: %s" % e) - channel.printl(OTR.STATUS[e].format(msg=event.msgtxt,err=e.args[0].error)) + channel.printl(OTR.STATUS[e].format(msg=msgtxt,err=e.args[0].error)) else: - event.msgtxt = text and text.decode() or "" - event.encrypted = OTR.ENCRYPTION_NAME - event.additional_data["encrypted"] = {"name":OTR.ENCRYPTION_NAME} - callback(event) + event.setBody(text and text.decode() or "") + properties.encrypted = OTR.ENCRYPTION_NAME finally: if channel.mayRetransmit and channel.state and ctl: channel.mayRetransmit = ctl.send_message(channel.lastMessage.decode()) diff --git a/plugin.py b/plugin.py index 7d0b1af..0fd9d35 100644 --- a/plugin.py +++ b/plugin.py @@ -1,5 +1,6 @@ # Copyright (C) 2019 Philipp Hörist # Copyright (C) 2019 Pavel R. +# Copyright (C) 2022 Bohdan Horbeshko # This file is part of Gajim OTR Plugin. # @@ -21,8 +22,20 @@ ERROR = None import logging from gajim.plugins import GajimPlugin from gajim.common import app + +log = logging.getLogger('gajim.p.otr') + try: from .otr import * -except ImportError: ERROR = 'python3-potr is missing' +except ImportError as e: + log.error(e) + ERROR = 'python3-potr is missing' + +if not ERROR: + try: + from . import module + except Exception as error: + log.error(error) + ERROR = 'Error: %s' % error # ... class OTRPlugin(GajimPlugin): @@ -30,17 +43,23 @@ class OTRPlugin(GajimPlugin): # init plugin # def init(self): + self.activatable = (not ERROR) + if ERROR: + self.available_text = (ERROR) + return self.encryption_name = 'OTR' self.description = 'Provides Off-the-Record encryption' - self.activatable = (not ERROR) - self.available_text = (ERROR) self.instances = {} + self.modules = [module] self.getinstance = lambda acct: self.instances.get(acct) or self.instances.setdefault(acct,OTR(self,acct)) self.gui_extension_points = { 'encrypt' + self.encryption_name: (self._encrypt_message, None), - 'decrypt': (self._decrypt_message, None), } + @staticmethod + def get_otr(account): + return app.connections[account].get_module('OTR') + # activate encryption # @staticmethod def activate_encryption(ctl): @@ -54,13 +73,5 @@ class OTRPlugin(GajimPlugin): # encrypt message # def _encrypt_message(self,con,event,callback): if not event.message or event.type_ != 'chat': return # drop empty and non-chat messages - otr = self.getinstance(event.conn.name) - otr.encrypt(event,callback) - - # decrypt message # - def _decrypt_message(self,con,event,callback): - if (event.encrypted) or (event.name[0:2] == 'gc') or not (event.msgtxt or '').startswith('?OTR'): return # skip non-OTR messages.. - if (event.name[0:3] == 'mam'): return setattr(event,'msgtxt','') # skip MAM messages (we can not decrypt OTR out of session).. - if (app.config.get_per('encryption','%s-%s'%(event.conn.name,event.jid),'encryption')!=self.encryption_name): return # skip all when encryption not set to OTR - otr = self.getinstance(event.conn.name) - otr.decrypt(event,callback) + otr = self.get_otr(event.account) + otr.otr.encrypt(event,callback)