Compare commits

...

7 commits
dev ... master

4 changed files with 116 additions and 19 deletions

39
otr.py
View file

@ -20,10 +20,11 @@ import string
import random import random
import itertools import itertools
import logging import logging
from inspect import signature
from gajim.common import const, app, helpers, configpaths from gajim.common import const, app, helpers, configpaths
from gajim.common.const import EncryptionData from gajim.common.const import EncryptionData
from gajim.common.structs import OutgoingMessage
from nbxmpp.protocol import Message, JID from nbxmpp.protocol import Message, JID
from nbxmpp.simplexml import Node
import pathlib import pathlib
import sys import sys
@ -51,8 +52,8 @@ class OTRChannel(context.Context):
# print some text to chat window # print some text to chat window
def printl(self,line): def printl(self,line):
println = self.user.getControl(self.peer) and self.user.getControl(self.peer).conv_textview.print_conversation_line control = app.window.get_control()
println and println("OTR: "+line,kind='status',name='',tim='',**('jid' in signature(println).parameters and {'jid':None} or {})) control and control.add_info_message("OTR: "+line)
@staticmethod @staticmethod
def getPolicy(policy): return OTR.DEFAULT_POLICY.get(policy) def getPolicy(policy): return OTR.DEFAULT_POLICY.get(policy)
@ -98,8 +99,7 @@ class OTR(context.Account):
# get chat control # get chat control
def getControl(self,peer): def getControl(self,peer):
ctl = app.interface.msg_win_mgr.get_control(peer.getStripped(),self.account) return app.window.get_control()
return ctl
# get OTR context (encrypted dialog between Alice and Bob) # get OTR context (encrypted dialog between Alice and Bob)
def getContext(self,peer): def getContext(self,peer):
@ -107,6 +107,18 @@ class OTR(context.Account):
self.ctxs[peer] = self.ctxs.get(peer) or OTRChannel(self,peer) self.ctxs[peer] = self.ctxs.get(peer) or OTRChannel(self,peer)
return self.ctxs[peer] return self.ctxs[peer]
# factory for Gajim 1.4+
def makeOutgoingMessage(self,message,control,peer):
contact = control.client.get_module('Contacts').get_contact(peer, groupchat=False)
chatstate = control.client.get_module('Chatstate').get_active_chatstate(contact)
return OutgoingMessage(account=self.account,
contact=contact,
message=message,
type_='chat',
chatstate=chatstate,
label=None,
correct_id=None)
# load my private key # load my private key
def loadPrivkey(self): def loadPrivkey(self):
my = self.keystore.load(jid=str(self.jid)) my = self.keystore.load(jid=str(self.jid))
@ -127,7 +139,8 @@ class OTR(context.Account):
# decrypt message # decrypt message
def decrypt(self,stanza,properties): def decrypt(self,stanza,properties):
peer = stanza.getFrom().new_as_bare() sFrom = stanza.getFrom()
peer = sFrom.new_as_bare()
msgtxt = stanza.getBody() msgtxt = stanza.getBody()
channel, ctl = self.getContext(peer), self.getControl(peer) channel, ctl = self.getContext(peer), self.getControl(peer)
try: try:
@ -136,16 +149,21 @@ class OTR(context.Account):
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=msgtxt,err=e.args[0].error)) channel.printl(OTR.STATUS[e].format(msg=msgtxt,err=e.args[0].error))
else: else:
channel.resource = sFrom.resource
stanza.setBody(text and text.decode() or "") stanza.setBody(text and text.decode() or "")
properties.encrypted = EncryptionData({'name': OTR.ENCRYPTION_NAME}) properties.encrypted = EncryptionData({'name': OTR.ENCRYPTION_NAME})
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.client.send_message(self.makeOutgoingMessage(channel.lastMessage.decode(), ctl, peer))
# encrypt message # encrypt message
def encrypt(self,event,callback): def encrypt(self,event,callback):
peer = event.msg_iq.getTo().new_as_bare() peer = event.msg_iq.getTo().new_as_bare()
channel, ctl = self.getContext(peer), event.control channel, ctl = self.getContext(peer), self.getControl(peer)
if event.xhtml: return ctl.send_message(event.message) # skip xhtml messages if not hasattr(channel, 'resource'):
channel.resource = ""
if channel.resource:
peer = peer.new_with(resource=channel.resource)
if event.xhtml: return ctl.client.send_message(self.makeOutgoingMessage(event.message, ctl, peer)) # skip xhtml messages
try: try:
encrypted = channel.sendMessage(context.FRAGMENT_SEND_ALL_BUT_LAST,event.message.encode(),appdata=event.msg_iq.getThread()) or b'' encrypted = channel.sendMessage(context.FRAGMENT_SEND_ALL_BUT_LAST,event.message.encode(),appdata=event.msg_iq.getThread()) or b''
message = (encrypted != self.getDefaultQueryMessage(OTR.DEFAULT_POLICY.get)) and event.message or "" message = (encrypted != self.getDefaultQueryMessage(OTR.DEFAULT_POLICY.get)) and event.message or ""
@ -156,4 +174,7 @@ class OTR(context.Account):
event.msg_iq.setBody(encrypted.decode()) # encrypted data goes here event.msg_iq.setBody(encrypted.decode()) # encrypted data goes here
event.message = message # message that will be displayed in our chat goes here event.message = message # message that will be displayed in our chat goes here
event.encrypted, event.additional_data["encrypted"] = OTR.ENCRYPTION_NAME, {"name":OTR.ENCRYPTION_NAME} # some mandatory encryption flags event.encrypted, event.additional_data["encrypted"] = OTR.ENCRYPTION_NAME, {"name":OTR.ENCRYPTION_NAME} # some mandatory encryption flags
if channel.resource:
event.stanza.addChild('no-copy', namespace='urn:xmpp:hints') # don't send carbons
event.stanza.addChild('no-store', namespace='urn:xmpp:hints') # don't store in MAM
callback(event) callback(event)

21
plugin-manifest.json Normal file
View file

@ -0,0 +1,21 @@
{
"authors": [
"Pavel R <pd@narayana.im>",
"Bohdan Horbeshko <bodqhrohro@gmail.com>"
],
"config_dialog": false,
"description": "Off-the-Record encryption",
"homepage": "https://dev.narayana.im/gajim-otrplugin",
"name": "otrplugin",
"platforms": [
"others",
"linux",
"darwin",
"win32"
],
"requirements": [
"gajim>=1.6,<1.9"
],
"short_name": "otrplugin",
"version": "0.5.2"
}

View file

@ -22,13 +22,17 @@ 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
from gajim.gtk.util import make_menu_item
from gi.repository import Gtk, Gio
log = logging.getLogger('gajim.p.otr') log = logging.getLogger('gajim.p.otr')
try: from Cryptodome import * try: from Cryptodome import *
except ImportError as e: except ImportError as e:
log.error(e) try: from Crypto import *
ERROR = 'Cryptodome is missing' except ImportError as e:
log.error(e)
ERROR = 'Cryptodome is missing'
if not ERROR: if not ERROR:
try: try:
@ -52,8 +56,27 @@ class OTRPlugin(GajimPlugin):
self.modules = [module] self.modules = [module]
self.gui_extension_points = { self.gui_extension_points = {
'encrypt' + self.encryption_name: (self._encrypt_message, None), 'encrypt' + self.encryption_name: (self._encrypt_message, None),
'message_actions_box': (self._message_actions_box_activate, self._message_actions_box_deactivate),
} }
self._menuitem = None
@staticmethod
def _get_grid():
return app.window.get_chat_stack().get_message_action_box()
@staticmethod
def _get_model(grid):
return grid._ui.encryption_menu_button.get_menu_model()
def activate(self):
if app.window is not None and self._menuitem is None:
grid = self._get_grid()
self._actions_hack_activate(grid)
def deactivate(self):
grid = self._get_grid()
self._actions_hack_deactivate(grid)
@staticmethod @staticmethod
def get_otr(account): def get_otr(account):
return app.connections[account].get_module('OTR') return app.connections[account].get_module('OTR')
@ -73,3 +96,25 @@ class OTRPlugin(GajimPlugin):
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.get_otr(event.account) otr = self.get_otr(event.account)
otr.otr.encrypt(event,callback) otr.otr.encrypt(event,callback)
def _message_actions_box_activate(self, grid, box):
if self._menuitem is None:
self._actions_hack_activate(grid)
def _message_actions_box_deactivate(self, grid, box):
if self._menuitem is not None:
self._actions_hack_deactivate(grid)
def _actions_hack_activate(self, grid):
model = grid._ui.encryption_menu_button.get_menu_model()
menuitem = make_menu_item('OTR', 'win.set-encryption', 'OTR')
self._menuitem = menuitem.get_attribute_value(Gio.MENU_ATTRIBUTE_LABEL)
model.append_item(menuitem)
def _actions_hack_deactivate(self, grid):
model = self._get_model(grid)
for i in range(model.get_n_items()):
if model.get_item_attribute_value(i, Gio.MENU_ATTRIBUTE_LABEL, None) == self._menuitem:
model.remove(i)
self._menuitem = None
break

View file

@ -17,14 +17,24 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>. # along with this library. If not, see <http://www.gnu.org/licenses/>.
from Cryptodome import Cipher try:
from Cryptodome.Hash import HMAC as _HMAC from Cryptodome import Cipher
from Cryptodome.Hash import SHA256 as _SHA256 from Cryptodome.Hash import HMAC as _HMAC
from Cryptodome.Hash import SHA as _SHA1 from Cryptodome.Hash import SHA256 as _SHA256
from Cryptodome.PublicKey import DSA from Cryptodome.Hash import SHA as _SHA1
from Cryptodome.Random import random from Cryptodome.PublicKey import DSA
from Cryptodome.Signature import DSS from Cryptodome.Random import random
from Cryptodome.Util import Counter from Cryptodome.Signature import DSS
from Cryptodome.Util import Counter
except ImportError as e:
from Crypto import Cipher
from Crypto.Hash import HMAC as _HMAC
from Crypto.Hash import SHA256 as _SHA256
from Crypto.Hash import SHA as _SHA1
from Crypto.PublicKey import DSA
from Crypto.Random import random
from Crypto.Signature import DSS
from Crypto.Util import Counter
from numbers import Number from numbers import Number