Gajim 1.3 support [WIP]

This commit is contained in:
Bohdan Horbeshko 2022-10-06 03:57:18 +03:00
parent 9cbc6991ae
commit 1c71637adc
4 changed files with 114 additions and 32 deletions

View file

@ -1,9 +1,9 @@
[info] [info]
name: otrplugin name: otrplugin
short_name: otrplugin short_name: otrplugin
version: 0.3.2 version: 0.4
description: Off-the-Record encryption description: Off-the-Record encryption
authors: Pavel R <pd@narayana.im> authors: Pavel R <pd@narayana.im>, Bohdan Horbeshko <bodqhrohro@gmail.com>
homepage: https://dev.narayana.im/gajim-otrplugin homepage: https://dev.narayana.im/narayana/gajim-otrplugin
min_gajim_version: 1.0.3 min_gajim_version: 1.3.0
max_gajim_version: 1.1.99 max_gajim_version: 1.3.99

63
module.py Normal file
View file

@ -0,0 +1,63 @@
## Copyright (C) 2008-2012 Kjell Braden <afflux@pentabarf.de>
## Copyright (C) 2019 Pavel R. <pd at narayana dot im>
## Copyright (C) 2022 Bohdan Horbeshko <bodqhrohro@gmail.com>
# 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 <http://www.gnu.org/licenses/>.
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'

34
otr.py
View file

@ -15,9 +15,12 @@
# #
# You can always obtain full license text at <http://www.gnu.org/licenses/>. # You can always obtain full license text at <http://www.gnu.org/licenses/>.
import os import os
import string
import random
import itertools
import logging
from inspect import signature from inspect import signature
from gajim.common import const, app, helpers, configpaths from gajim.common import const, app, helpers, configpaths
from gajim.session import ChatControlSession
from nbxmpp.protocol import Message, JID from nbxmpp.protocol import Message, JID
from potr import context, crypt, proto from potr import context, crypt, proto
from .keystore import Keystore 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 <peer> via XMPP # this method may be called self.sendMessage() when we need to send some data to our <peer> via XMPP
def inject(self,msg,appdata=None): def inject(self,msg,appdata=None):
stanza = Message(to=self.peer, body=msg.decode(), typ='chat') 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) self.user.stream.send_stanza(stanza)
# this method called on channel state change # this method called on channel state change
@ -46,6 +49,13 @@ class OTRChannel(context.Context):
@staticmethod @staticmethod
def getPolicy(policy): return OTR.DEFAULT_POLICY.get(policy) 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) # OTR instance for Gajim user (Alice)
class OTR(context.Account): class OTR(context.Account):
PROTO = ('XMPP', 1024) PROTO = ('XMPP', 1024)
@ -69,10 +79,9 @@ class OTR(context.Account):
crypt.InvalidParameterError: "unable to decrypt message (key/signature mismatch)", 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) super(OTR,self).__init__(account,*OTR.PROTO)
self.plugin = plugin self.log = logging.getLogger('gajim.p.otr.otr')
self.log = plugin.log
self.account = account self.account = account
self.stream = app.connections[account] self.stream = app.connections[account]
self.jid = self.stream.get_own_jid() 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) for fingerprint,trust in fingerprints.items(): self.keystore.save(jid=peer,fingerprint=fingerprint,trust=trust)
# decrypt message # decrypt message
def decrypt(self,event,callback): def decrypt(self,stanza,properties):
peer = event.stanza.getFrom() peer = stanza.getFrom()
msgtxt = stanza.getBody()
channel, ctl = self.getContext(peer), self.getControl(peer) channel, ctl = self.getContext(peer), self.getControl(peer)
try: 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: except (context.UnencryptedMessage,context.NotEncryptedError,context.ErrorReceived,crypt.InvalidParameterError) as e:
self.log.error("** got exception while decrypting message: %s" % 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: else:
event.msgtxt = text and text.decode() or "" event.setBody(text and text.decode() or "")
event.encrypted = OTR.ENCRYPTION_NAME properties.encrypted = OTR.ENCRYPTION_NAME
event.additional_data["encrypted"] = {"name":OTR.ENCRYPTION_NAME}
callback(event)
finally: finally:
if channel.mayRetransmit and channel.state and ctl: channel.mayRetransmit = ctl.send_message(channel.lastMessage.decode()) if channel.mayRetransmit and channel.state and ctl: channel.mayRetransmit = ctl.send_message(channel.lastMessage.decode())

View file

@ -1,5 +1,6 @@
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2019 Pavel R. <pd at narayana dot im> # Copyright (C) 2019 Pavel R. <pd at narayana dot im>
# Copyright (C) 2022 Bohdan Horbeshko <bodqhrohro@gmail.com>
# This file is part of Gajim OTR Plugin. # This file is part of Gajim OTR Plugin.
# #
@ -21,8 +22,20 @@ ERROR = None
import logging import logging
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.common import app from gajim.common import app
log = logging.getLogger('gajim.p.otr')
try: from .otr import * 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): class OTRPlugin(GajimPlugin):
@ -30,17 +43,23 @@ class OTRPlugin(GajimPlugin):
# init plugin # # init plugin #
def init(self): def init(self):
self.activatable = (not ERROR)
if ERROR:
self.available_text = (ERROR)
return
self.encryption_name = 'OTR' self.encryption_name = 'OTR'
self.description = 'Provides Off-the-Record encryption' self.description = 'Provides Off-the-Record encryption'
self.activatable = (not ERROR)
self.available_text = (ERROR)
self.instances = {} self.instances = {}
self.modules = [module]
self.getinstance = lambda acct: self.instances.get(acct) or self.instances.setdefault(acct,OTR(self,acct)) self.getinstance = lambda acct: self.instances.get(acct) or self.instances.setdefault(acct,OTR(self,acct))
self.gui_extension_points = { self.gui_extension_points = {
'encrypt' + self.encryption_name: (self._encrypt_message, None), '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 # # activate encryption #
@staticmethod @staticmethod
def activate_encryption(ctl): def activate_encryption(ctl):
@ -54,13 +73,5 @@ class OTRPlugin(GajimPlugin):
# encrypt message # # encrypt message #
def _encrypt_message(self,con,event,callback): def _encrypt_message(self,con,event,callback):
if not event.message or event.type_ != 'chat': return # drop empty and non-chat messages if not event.message or event.type_ != 'chat': return # drop empty and non-chat messages
otr = self.getinstance(event.conn.name) otr = self.get_otr(event.account)
otr.encrypt(event,callback) otr.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)