Gajim 1.3 support [WIP]
This commit is contained in:
parent
9cbc6991ae
commit
1c71637adc
10
manifest.ini
10
manifest.ini
|
@ -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
63
module.py
Normal 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
34
otr.py
|
@ -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())
|
||||||
|
|
||||||
|
|
39
plugin.py
39
plugin.py
|
@ -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)
|
|
||||||
|
|
Loading…
Reference in a new issue