173 lines
7.4 KiB
Python
173 lines
7.4 KiB
Python
|
## Copyright (C) 2008-2012 Kjell Braden <afflux@pentabarf.de>
|
||
|
## Copyright (C) 2019 Pavel R. <pd at narayana dot im>
|
||
|
#
|
||
|
|
||
|
# 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/>.
|
||
|
|
||
|
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)
|