diff src/plugins/plugin_sec_otr.py @ 1095:ef7b7dd5c5db

plugin OTR: various improvments: - otr_data is now saved between sessions - private key is encrypted before saving - state change is detected (unencrypted/started/refreshed/finished) and (un)trusted (trusting is not implemented yet) - user feedback - fixed unencrypted message in an encrypted context warning - fixed inject msg encoding - minor refactoring
author Goffi <goffi@goffi.org>
date Thu, 26 Jun 2014 14:58:25 +0200
parents abcac1ac27a7
children 8def4a3f55c2
line wrap: on
line diff
--- a/src/plugins/plugin_sec_otr.py	Thu Jun 26 00:05:25 2014 +0200
+++ b/src/plugins/plugin_sec_otr.py	Thu Jun 26 14:58:25 2014 +0200
@@ -26,7 +26,12 @@
 log = getLogger(__name__)
 from twisted.words.protocols.jabber import jid
 from twisted.python import failure
+from twisted.internet import defer
 import potr
+from sat.memory import persistent
+
+NS_OTR = "otr_plugin"
+PRIVATE_KEY = "PRIVATE KEY"
 
 DEFAULT_POLICY_FLAGS = {
   'ALLOW_V1':False,
@@ -46,14 +51,10 @@
 }
 
 
-PROTOCOL='xmpp'
-MMS=1024
-
-
 class Context(potr.context.Context):
 
-    def __init__(self, host, account, peer):
-        super(Context, self).__init__(account, peer)
+    def __init__(self, host, account, other_jid):
+        super(Context, self).__init__(account, other_jid)
         self.host = host
 
     def getPolicy(self, key):
@@ -62,89 +63,136 @@
         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))
+    def inject(self, msg_str, appdata=None):
+        assert isinstance(self.peer, jid.JID)
+        msg = msg_str.decode('utf-8')
+        client = self.user.client
+        log.debug(u'inject(%s, appdata=%s, to=%s)' % (msg, appdata, self.peer))
         mess_data = {'message': msg,
                      'type': 'chat',
                      'from': client.jid,
-                      'to': to_jid,
+                      'to': self.peer,
                       'subject': None,
                     }
         self.host.generateMessageXML(mess_data)
         client.xmlstream.send(mess_data['xml'])
 
     def setState(self, state):
+        old_state = 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
+        log.debug(u"setState: %s (old_state=%s) " % (state, old_state))
+
+        if state == potr.context.STATE_PLAINTEXT:
+            feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
+        elif state == potr.context.STATE_ENCRYPTED:
+            try:
+                fingerprint, trusted = self.getCurrentTrust()
+            except TypeError:
+                trusted = False
+            trusted_str = _(u"trusted") if trusted else _(u"untrusted")
+
+            if old_state == potr.context.STATE_ENCRYPTED:
+                feedback = _(u"%(trusted)s OTR conversation with %(other_jid)s REFRESHED") % {'trusted': trusted_str, 'other_jid': self.peer.full()}
+            else:
+                feedback = _(u"%(trusted)s Encrypted OTR conversation started with %(other_jid)s") % {'trusted': trusted_str, 'other_jid': self.peer.full()}
+        elif state == potr.context.STATE_FINISHED:
+            feedback = _(u"OTR conversation with %(other_jid)s is FINISHED") % {'other_jid': self.peer.full()}
+        else:
+            log.error(_(u"Unknown OTR state"))
+            return
+
+        client = self.user.client
+        # FIXME: newMessage should manage system message, so they don't appear as coming from the contact
+        self.host.bridge.newMessage(client.jid.full(),
+                                    feedback,
+                                    mess_type="headline",
+                                    to_jid=self.peer.full(),
+                                    extra={},
+                                    profile=client.profile)
+        # TODO: send signal to frontends
 
 
 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 __init__(self, host, client):
+        log.debug(u"new account: %s" % client.jid)
+        super(Account, self).__init__(client.jid, "xmpp", 1024)
+        self.host = host
+        self.client = client
 
     def loadPrivkey(self):
-        # TODO
-        log.debug("loadPrivkey")
-        return None
+        log.debug(u"loadPrivkey")
+        return self.client.otr_priv_key
 
     def savePrivkey(self):
-        # TODO
-        log.debug("savePrivkey")
+        log.debug(u"savePrivkey")
+        priv_key = self.getPrivkey().serializePrivateKey()
+        d = self.host.memory.encryptValue(priv_key, self.client.profile)
+        def save_encrypted_key(encrypted_priv_key):
+            self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key
+        d.addCallback(save_encrypted_key)
 
 
 class ContextManager(object):
 
     def __init__(self, host, client):
         self.host = host
-        self.account = Account(client.jid)
+        self.account = Account(host, client)
         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 startContext(self, other_jid):
+        assert isinstance(other_jid, jid.JID)
+        context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid))
+        return context
 
     def getContextForUser(self, other):
-        log.debug("getContextForUser [%s]" % other)
+        log.debug(u"getContextForUser [%s]" % other)
         return self.startContext(other)
 
 
 class OTR(object):
 
     def __init__(self, host):
-        log.info(_("OTR plugin initialization"))
+        log.info(_(u"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)
 
+    @defer.inlineCallbacks
     def profileConnected(self, profile):
         client = self.host.getClient(profile)
         self.context_managers[profile] = ContextManager(self.host, client)
+        client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
+        yield client.otr_data.load()
+        encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None)
+        if encrypted_priv_key is not None:
+            priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile)
+            client.otr_priv_key = potr.crypt.PK.parsePrivateKey(priv_key)[0]
+        else:
+            client.otr_priv_key = None
 
     def _receivedTreatment(self, data, profile):
         from_jid = jid.JID(data['from'])
-        log.debug("_receivedTreatment [from_jid = %s]" % from_jid)
+        log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
         otrctx = self.context_managers[profile].getContextForUser(from_jid)
+        encrypted = True
 
-        encrypted = True
         try:
-            res = otrctx.receiveMessage(data['body'].encode('utf-8'), appdata=(from_jid, profile))
+            res = otrctx.receiveMessage(data['body'].encode('utf-8'))
         except potr.context.UnencryptedMessage:
-            log.warning("Received unencrypted message in an encrypted context")
-            # TODO: feedback to frontends (either message or popup)
+            if otrctx.state == potr.context.STATE_ENCRYPTED:
+                log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()})
+                client = self.host.getClient(profile)
+                self.host.bridge.newMessage(from_jid.full(),
+                                            _(u"WARNING: received unencrypted data in a supposedly encrypted context"),
+                                            mess_type="headline", # FIXME: add message type for internal informations
+                                            to_jid=client.jid.full(),
+                                            extra={},
+                                            profile=client.profile)
             encrypted = False
 
-        if encrypted == False:
+        if not encrypted:
             return data
         else:
             if res[0] != None:
@@ -165,12 +213,12 @@
             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))
+            log.debug(u"encrypting message")
+            otrctx.sendMessage(0, mess_data['message'].encode('utf-8'))
             client = self.host.getClient(profile)
             self.host.sendMessageToBridge(mess_data, client)
             return False
         else:
-            log.debug("sending message unencrypted")
+            log.debug(u"sending message unencrypted")
             return True