diff src/plugins/plugin_sec_otr.py @ 1055:abcac1ac27a7

plugin otr: first draft
author Goffi <goffi@goffi.org>
date Sat, 07 Jun 2014 16:39:08 +0200
parents
children ef7b7dd5c5db
line wrap: on
line diff
--- /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 <http://www.gnu.org/licenses/>.
+
+# 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
+