# HG changeset patch # User Goffi # Date 1403787505 -7200 # Node ID ef7b7dd5c5db7253d4d18b4aa8ffa576b1e68b2a # Parent 4286a19e9e8a06deae5ed300a2637b199ed2efb0 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 diff -r 4286a19e9e8a -r ef7b7dd5c5db src/plugins/plugin_sec_otr.py --- 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