## Copyright (C) 2008-2012 Kjell Braden ## Copyright (C) 2019 Pavel R. # # 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 . import os import time import logging from potr import context, crypt from gajim.common import const, app, helpers, configpaths from gajim.session import ChatControlSession from nbxmpp.protocol import Message, JID from otrplugin.keystore import Keystore # OTR channel class class Channel(context.Context): def __init__(self, account, peer): super(Channel, self).__init__(account, peer) self.getPolicy = OTR.DEFAULT_POLICY.get # default OTR flags self.trustName = peer.getStripped() # peer name self.stream = account.stream # XMPP stream self.ctl = account.getControl(peer.getStripped()) # chat window control self.resend = [] def println(self, line, kind='status', **kwargs): try: self.ctl.conv_textview.print_conversation_line(line, kind=kind, tim=None, jid=None, name='', **kwargs) except TypeError: self.ctl.conv_textview.print_conversation_line(line, kind=kind, tim=None, name='', **kwargs) # gajim git fix return line def inject(self, msg, appdata={}): thread = appdata.get('thread', ChatControlSession.generate_thread_id(None)) stanza = Message(to=self.peer, body=msg.decode(), typ='chat') stanza.setThread(thread) self.stream.send_stanza(stanza) def setState(self, newstate): state, self.state = self.state, newstate if self.getCurrentTrust() is None: self.println("OTR: new fingerprint received [%s]" % self.getCurrentKey()) self.setCurrentTrust(0) if newstate == context.STATE_ENCRYPTED and state != newstate: self.println("OTR: %s conversation started [%s]" % (self.getCurrentTrust() and 'trusted' or '**untrusted**', self.getCurrentKey()) ) elif newstate == context.STATE_FINISHED and state != newstate: # channel closed self.println("OTR: conversation closed.") # OTR class class OTR(context.Account): contextclass = Channel # OTR const ENCRYPTION_NAME = 'OTR' ENCRYPTION_DATA = helpers.AdditionalDataDict({'encrypted':{'name': ENCRYPTION_NAME}}) PROTOCOL = 'XMPP' MMS = 1000 DEFAULT_POLICY = { 'REQUIRE_ENCRYPTION': True, 'ALLOW_V1': False, 'ALLOW_V2': True, 'SEND_TAG': True, 'WHITESPACE_START_AKE': True, 'ERROR_START_AKE': True, } SESSION_START = '?OTRv2?\nI would like to start ' \ 'an Off-the-Record private conversation. However, you ' \ 'do not have a plugin to support that.\nSee '\ 'https://otr.cypherpunks.ca/ for more information.' def __init__(self, account, logger = None): super(OTR, self).__init__(account, OTR.PROTOCOL, OTR.MMS) self.log = logger self.ctxs, self.ctls = {}, {} self.account = account self.stream = app.connections[account] self.jid = self.stream.get_own_jid() self.keystore = Keystore(os.path.join(configpaths.get('MY_DATA'), 'otr_' + self.jid.getStripped() + '.db')) self.loadTrusts() # overload some default methods # def getControl(self, peer): ctrl = self.ctls.setdefault(peer, app.interface.msg_win_mgr.get_control(peer, self.account)) return ctrl def getContext(self, peer): ctx = self.ctxs.setdefault(peer, Channel(self, peer)) ctx = ctx.state == context.STATE_FINISHED and self.ctxs.pop(peer).disconnect() or self.ctxs.setdefault(peer, Channel(self, peer)) return ctx def loadPrivkey(self): my = self.keystore.load({'jid': str(self.jid)}) return crypt.PK.parsePrivateKey(bytes.fromhex(my.privatekey))[0] if my and my.privatekey else None def savePrivkey(self): return self.keystore.save({'jid': self.jid, 'privatekey': self.getPrivkey().serializePrivateKey().hex()}) def loadTrusts(self): for c in self.keystore.load(): self.setTrust(c.jid, c.fingerprint, c.trust) def saveTrusts(self): for jid, keys in self.trusts.items(): for fingerprint, trust in keys.items(): self.keystore.save({'jid': jid, 'fingerprint': fingerprint, 'trust': trust}) # decrypt & receive def _decrypt(self, event, callback): try: peer = event.stanza.getFrom() channel = self.getContext(peer) text, tlvs = channel.receiveMessage(event.msgtxt.encode(), appdata = {'thread': event.stanza.getThread()}) text = text and text.decode() or "" except context.UnencryptedMessage: self.log.error('** got plain text over encrypted channel ** %s' % stanza.getBody()) channel.println("OTR: received plain message [%s]" % event.stanza.getBody()) except context.ErrorReceived as e: self.log.error('** otr error ** %s' % e) channel.println("OTR: received error [%s]" % e) except crypt.InvalidParameterError: self.log.error('** unreadable message **') channel.println("OTR: received unreadable message (session expired?)") except context.NotEncryptedError: self.log.error('** otr session lost **') channel.println("OTR: session lost.") # resent messages after channel open if channel.resend and channel.state == context.STATE_ENCRYPTED: message = channel.resend.pop() channel.sendMessage(**message) channel.println(message['msg'].decode(), kind='outgoing', encrypted=self.ENCRYPTION_NAME, additional_data=self.ENCRYPTION_DATA) event.xhtml = None event.msgtxt = text event.encrypted = self.ENCRYPTION_NAME event.additional_data = self.ENCRYPTION_DATA callback(event) # encrypt & send def _encrypt(self, event, callback): try: peer = event.msg_iq.getTo() channel = self.getContext(peer) session = event.session or ChatControlSession(self.stream, peer, None, 'chat') encrypted = channel.sendMessage(sendPolicy = context.FRAGMENT_SEND_ALL_BUT_LAST, msg = event.message.encode(), appdata = {'thread': session.thread_id}) or b'' except context.NotEncryptedError: self.log.error("** unable to encrypt message **") channel.println('OTR: unable to start conversation') return # resend lost message after session start if encrypted == OTR.SESSION_START.encode(): channel.println('OTR: trying to start encrypted conversation') channel.resend += [{'sendPolicy': context.FRAGMENT_SEND_ALL, 'msg': event.message.encode(), 'appdata': {'thread': session.thread_id}}] event.message = '' event.encrypted = 'OTR' event.additional_data['encrypted'] = {'name': 'OTR'} event.msg_iq.setBody(encrypted.decode()) callback(event)