# HG changeset patch # User Goffi # Date 1402151948 -7200 # Node ID abcac1ac27a79cc6fb4b1f86cf193d12c043ce94 # Parent a32ef03d4af0de30d80363a877fb966a022abf64 plugin otr: first draft diff -r a32ef03d4af0 -r abcac1ac27a7 src/plugins/plugin_sec_otr.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/plugins/plugin_sec_otr.py Sat Jun 07 16:39:08 2014 +0200 @@ -0,0 +1,176 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SAT plugin for OTR encryption +# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# XXX: thanks to Darrik L Mazey for his documentation (https://blog.darmasoft.net/2013/06/30/using-pure-python-otr.html) +# this implentation is based on it + +from sat.core.i18n import _ +from sat.core.log import getLogger +from sat.core import exceptions +log = getLogger(__name__) +from twisted.words.protocols.jabber import jid +from twisted.python import failure +import potr + +DEFAULT_POLICY_FLAGS = { + 'ALLOW_V1':False, + 'ALLOW_V2':True, + 'REQUIRE_ENCRYPTION':True, +} + +PLUGIN_INFO = { + "name": "OTR", + "import_name": "OTR", + "type": "SEC", + "protocols": [], + "dependencies": [], + "main": "OTR", + "handler": "no", + "description": _("""Implementation of OTR""") +} + + +PROTOCOL='xmpp' +MMS=1024 + + +class Context(potr.context.Context): + + def __init__(self, host, account, peer): + super(Context, self).__init__(account, peer) + self.host = host + + def getPolicy(self, key): + if key in DEFAULT_POLICY_FLAGS: + return DEFAULT_POLICY_FLAGS[key] + else: + return False + + def inject(self, msg, appdata=None): + to_jid, profile = appdata + assert isinstance(to_jid, jid.JID) + client = self.host.getClient(profile) + log.debug('inject(%s, appdata=%s, to=%s)' % (msg, appdata, to_jid)) + mess_data = {'message': msg, + 'type': 'chat', + 'from': client.jid, + 'to': to_jid, + 'subject': None, + } + self.host.generateMessageXML(mess_data) + client.xmlstream.send(mess_data['xml']) + + def setState(self, state): + super(Context, self).setState(state) + log.debug("setState: %s (self = %s)" % (state, self)) + # TODO: send signal to frontends, maybe a message feedback too + + +class Account(potr.context.Account): + + def __init__(self, account_jid): + global PROTOCOL, MMS + assert isinstance(account_jid, jid.JID) + log.debug("new account: %s" % account_jid) + super(Account, self).__init__(account_jid, PROTOCOL, MMS) + + def loadPrivkey(self): + # TODO + log.debug("loadPrivkey") + return None + + def savePrivkey(self): + # TODO + log.debug("savePrivkey") + + +class ContextManager(object): + + def __init__(self, host, client): + self.host = host + self.account = Account(client.jid) + self.contexts = {} + + def startContext(self, other): + assert isinstance(other, jid.JID) + if not other in self.contexts: + self.contexts[other] = Context(self.host, self.account, other) + return self.contexts[other] + + def getContextForUser(self, other): + log.debug("getContextForUser [%s]" % other) + return self.startContext(other) + + +class OTR(object): + + def __init__(self, host): + log.info(_("OTR plugin initialization")) + self.host = host + self.context_managers = {} + host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) + host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) + + def profileConnected(self, profile): + client = self.host.getClient(profile) + self.context_managers[profile] = ContextManager(self.host, client) + + def _receivedTreatment(self, data, profile): + from_jid = jid.JID(data['from']) + log.debug("_receivedTreatment [from_jid = %s]" % from_jid) + otrctx = self.context_managers[profile].getContextForUser(from_jid) + + encrypted = True + try: + res = otrctx.receiveMessage(data['body'].encode('utf-8'), appdata=(from_jid, profile)) + except potr.context.UnencryptedMessage: + log.warning("Received unencrypted message in an encrypted context") + # TODO: feedback to frontends (either message or popup) + encrypted = False + + if encrypted == False: + return data + else: + if res[0] != None: + # decrypted messages handling. + # receiveMessage() will return a tuple, the first part of which will be the decrypted message + data['body'] = res[0].decode('utf-8') + raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history + else: + raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal) + + def MessageReceivedTrigger(self, message, post_treat, profile): + post_treat.addCallback(self._receivedTreatment, profile) + return True + + def sendMessageTrigger(self, mess_data, pre_xml_treatments, post_xml_treatments, profile): + to_jid = mess_data['to'] + if mess_data['type'] != 'groupchat' and not to_jid.resource: + to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed + otrctx = self.context_managers[profile].getContextForUser(to_jid) + if mess_data['type'] != 'groupchat' and otrctx.state == potr.context.STATE_ENCRYPTED: + log.debug("encrypting message") + otrctx.sendMessage(0, mess_data['message'].encode('utf-8'), appdata=(to_jid, profile)) + client = self.host.getClient(profile) + self.host.sendMessageToBridge(mess_data, client) + return False + else: + log.debug("sending message unencrypted") + return True +