libervia-backend/src/plugins/plugin_sec_otr.py

286 lines
12 KiB
Python

#!/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 _, D_
from sat.core.constants import Const as C
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
from twisted.internet import defer
import potr
from sat.memory import persistent
NS_OTR = "otr_plugin"
PRIVATE_KEY = "PRIVATE KEY"
MAIN_MENU = D_('OTR')
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""")
}
class Context(potr.context.Context):
def __init__(self, host, account, other_jid):
super(Context, self).__init__(account, other_jid)
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_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': 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(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, host, client):
log.debug(u"new account: %s" % client.jid)
if not client.jid.resource:
log.warning("Account created without resource")
super(Account, self).__init__(unicode(client.jid), "xmpp", 1024)
self.host = host
self.client = client
def loadPrivkey(self):
log.debug(u"loadPrivkey")
return self.client.otr_priv_key
def savePrivkey(self):
log.debug(u"savePrivkey")
if self.privkey is None:
raise exceptions.InternalError(_("Save is called but privkey is None !"))
priv_key = self.privkey.serializePrivateKey().encode('hex')
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(host, client)
self.contexts = {}
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(u"getContextForUser [%s]" % other)
if not other.resource:
log.warning("getContextForUser called with a bare jid")
return self.startContext(other)
class OTR(object):
def __init__(self, host):
log.info(_(u"OTR plugin initialization"))
self._fixPotr() # FIXME: to be removed when potr will be fixed
self.host = host
self.context_managers = {}
host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE)
host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
def _fixPotr(self):
# FIXME: potr fix for bad unicode handling
# this method monkeypatch it, must be removed when potr
# is fixed
def getDefaultQueryMessage(self, policy):
defaultQuery = '?OTRv{versions}?\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.'
v = '2' if policy('ALLOW_V2') else ''
msg = defaultQuery.format(versions=v)
return msg.encode('ascii')
potr.context.Account.getDefaultQueryMessage = getDefaultQueryMessage
@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.decode('hex'))[0]
else:
client.otr_priv_key = None
def _startRefresh(self, menu_data, profile):
"""Start or refresh an OTR session
@param menu_data: %(menu_data)s
@param profile: %(doc_profile)s
"""
try:
to_jid = jid.JID(menu_data['jid'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except KeyError:
log.error(_("jid key is not present !"))
return defer.fail(exceptions.DataError)
otrctx = self.context_managers[profile].getContextForUser(to_jid)
query = otrctx.sendMessage(0, '?OTRv?')
otrctx.inject(query)
return {}
def _endSession(self, menu_data, profile):
"""End an OTR session
@param menu_data: %(menu_data)s
@param profile: %(doc_profile)s
"""
try:
to_jid = jid.JID(menu_data['jid'])
if not to_jid.resource:
to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
except KeyError:
log.error(_("jid key is not present !"))
return defer.fail(exceptions.DataError)
otrctx = self.context_managers[profile].getContextForUser(to_jid)
otrctx.disconnect()
return {}
def _receivedTreatment(self, data, profile):
from_jid = jid.JID(data['from'])
log.debug(u"_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'))
except potr.context.UnencryptedMessage:
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 not encrypted:
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(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(u"sending message unencrypted")
return True