comparison src/plugins/plugin_sec_otr.py @ 1141:7fcafc3206b1

plugin OTR: authentication management + fixed a bug in setState (due to a wrong docstring in potr.context.getCurrentTrust)
author Goffi <goffi@goffi.org>
date Wed, 27 Aug 2014 00:23:14 +0200
parents 768f1f1ef12c
children 2481fa96ac1c
comparison
equal deleted inserted replaced
1140:7f32371568e4 1141:7fcafc3206b1
23 from sat.core.i18n import _, D_ 23 from sat.core.i18n import _, D_
24 from sat.core.constants import Const as C 24 from sat.core.constants import Const as C
25 from sat.core.log import getLogger 25 from sat.core.log import getLogger
26 from sat.core import exceptions 26 from sat.core import exceptions
27 log = getLogger(__name__) 27 log = getLogger(__name__)
28 from sat.tools import xml_tools
28 from twisted.words.protocols.jabber import jid 29 from twisted.words.protocols.jabber import jid
29 from twisted.python import failure 30 from twisted.python import failure
30 from twisted.internet import defer 31 from twisted.internet import defer
31 import potr 32 import potr
32 from sat.memory import persistent 33 from sat.memory import persistent
33 34
34 NS_OTR = "otr_plugin" 35 NS_OTR = "otr_plugin"
35 PRIVATE_KEY = "PRIVATE KEY" 36 PRIVATE_KEY = "PRIVATE KEY"
36 MAIN_MENU = D_('OTR') 37 MAIN_MENU = D_('OTR')
38 AUTH_TXT = D_("To authenticate your correspondent, you need to give your below fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check that the one he give you is the same as below. If there is a mismatch, there can be a spy between you !")
37 39
38 DEFAULT_POLICY_FLAGS = { 40 DEFAULT_POLICY_FLAGS = {
39 'ALLOW_V1':False, 41 'ALLOW_V1':False,
40 'ALLOW_V2':True, 42 'ALLOW_V2':True,
41 'REQUIRE_ENCRYPTION':True, 43 'REQUIRE_ENCRYPTION':True,
52 "description": _("""Implementation of OTR""") 54 "description": _("""Implementation of OTR""")
53 } 55 }
54 56
55 57
56 class Context(potr.context.Context): 58 class Context(potr.context.Context):
57
58 def __init__(self, host, account, other_jid): 59 def __init__(self, host, account, other_jid):
59 super(Context, self).__init__(account, other_jid) 60 super(Context, self).__init__(account, other_jid)
60 self.host = host 61 self.host = host
61 62
62 def getPolicy(self, key): 63 def getPolicy(self, key):
86 87
87 if state == potr.context.STATE_PLAINTEXT: 88 if state == potr.context.STATE_PLAINTEXT:
88 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()} 89 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
89 elif state == potr.context.STATE_ENCRYPTED: 90 elif state == potr.context.STATE_ENCRYPTED:
90 try: 91 try:
91 fingerprint, trusted = self.getCurrentTrust() 92 trusted = self.getCurrentTrust()
92 except TypeError: 93 except TypeError:
93 trusted = False 94 trusted = False
94 trusted_str = _(u"trusted") if trusted else _(u"untrusted") 95 trusted_str = _(u"trusted") if trusted else _(u"untrusted")
95 96
96 if old_state == potr.context.STATE_ENCRYPTED: 97 if old_state == potr.context.STATE_ENCRYPTED:
136 d = self.host.memory.encryptValue(priv_key, self.client.profile) 137 d = self.host.memory.encryptValue(priv_key, self.client.profile)
137 def save_encrypted_key(encrypted_priv_key): 138 def save_encrypted_key(encrypted_priv_key):
138 self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key 139 self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key
139 d.addCallback(save_encrypted_key) 140 d.addCallback(save_encrypted_key)
140 141
142 def loadTrusts(self):
143 trust_data = self.client.otr_data.get('trust', {})
144 for jid_, jid_data in trust_data.iteritems():
145 for fingerprint, trust_level in jid_data.iteritems():
146 log.debug('setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level))
147 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level
148
149 def saveTrusts(self):
150 log.debug("saving trusts for {profile}".format(profile=self.client.profile))
151 log.debug("trusts = {}".format(self.client.otr_data['trust']))
152 self.client.otr_data.force('trust')
153
154 def setTrust(self, other_jid, fingerprint, trustLevel):
155 try:
156 trust_data = self.client.otr_data['trust']
157 except KeyError:
158 trust_data = {}
159 self.client.otr_data['trust'] = trust_data
160 jid_data = trust_data.setdefault(other_jid.full(), {})
161 jid_data[fingerprint] = trustLevel
162 super(Account, self).setTrust(other_jid, fingerprint, trustLevel)
163
141 164
142 class ContextManager(object): 165 class ContextManager(object):
143 166
144 def __init__(self, host, client): 167 def __init__(self, host, client):
145 self.host = host 168 self.host = host
167 self.context_managers = {} 190 self.context_managers = {}
168 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 191 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
169 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000) 192 host.trigger.add("sendMessage", self.sendMessageTrigger, priority=100000)
170 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) 193 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)
171 host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE) 194 host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
195 host.importMenu((MAIN_MENU, D_("Authenticate")), self._authenticate, security_limit=0, help_string=D_("Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
172 196
173 def _fixPotr(self): 197 def _fixPotr(self):
174 # FIXME: potr fix for bad unicode handling 198 # FIXME: potr fix for bad unicode handling
175 # this method monkeypatch it, must be removed when potr 199 # this method monkeypatch it, must be removed when potr
176 # is fixed 200 # is fixed
187 potr.context.Account.getDefaultQueryMessage = getDefaultQueryMessage 211 potr.context.Account.getDefaultQueryMessage = getDefaultQueryMessage
188 212
189 @defer.inlineCallbacks 213 @defer.inlineCallbacks
190 def profileConnected(self, profile): 214 def profileConnected(self, profile):
191 client = self.host.getClient(profile) 215 client = self.host.getClient(profile)
192 self.context_managers[profile] = ContextManager(self.host, client) 216 ctxMng = self.context_managers[profile] = ContextManager(self.host, client)
193 client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile) 217 client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
194 yield client.otr_data.load() 218 yield client.otr_data.load()
195 encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None) 219 encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None)
196 if encrypted_priv_key is not None: 220 if encrypted_priv_key is not None:
197 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile) 221 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile)
198 client.otr_priv_key = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0] 222 client.otr_priv_key = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
199 else: 223 else:
200 client.otr_priv_key = None 224 client.otr_priv_key = None
225 ctxMng.account.loadTrusts()
201 226
202 def _startRefresh(self, menu_data, profile): 227 def _startRefresh(self, menu_data, profile):
203 """Start or refresh an OTR session 228 """Start or refresh an OTR session
204 229
205 @param menu_data: %(menu_data)s 230 @param menu_data: %(menu_data)s
231 log.error(_("jid key is not present !")) 256 log.error(_("jid key is not present !"))
232 return defer.fail(exceptions.DataError) 257 return defer.fail(exceptions.DataError)
233 otrctx = self.context_managers[profile].getContextForUser(to_jid) 258 otrctx = self.context_managers[profile].getContextForUser(to_jid)
234 otrctx.disconnect() 259 otrctx.disconnect()
235 return {} 260 return {}
261
262 def _authenticate(self, menu_data, profile):
263 """Authenticate other user and see our own fingerprint
264
265 @param menu_data: %(menu_data)s
266 @param profile: %(doc_profile)s
267 """
268 try:
269 to_jid = jid.JID(menu_data['jid'])
270 if not to_jid.resource:
271 to_jid.resource = self.host.memory.getLastResource(to_jid, profile) # FIXME: temporary and unsecure, must be changed when frontends are refactored
272 except KeyError:
273 log.error(_("jid key is not present !"))
274 return defer.fail(exceptions.DataError)
275 otrctx = self.context_managers[profile].getContextForUser(to_jid)
276
277 priv_key = otrctx.user.client.otr_priv_key
278
279 if priv_key is None:
280 # we have no private key yet
281 dialog = xml_tools.XMLUI(C.XMLUI_DIALOG,
282 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
283 C.XMLUI_DATA_MESS: _("You have no private key yet, start an OTR conversation to have one"),
284 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_WARNING
285 },
286 title = _("No private key"),
287 )
288 return {'xmlui': dialog.toXml()}
289
290 other_fingerprint = otrctx.getCurrentKey()
291
292 if other_fingerprint is None:
293 # we have a private key, but not the fingerprint of our correspondent
294 dialog = xml_tools.XMLUI(C.XMLUI_DIALOG,
295 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_MESSAGE,
296 C.XMLUI_DATA_MESS: _("Your fingerprint is\n{fingerprint}\n\nStart an OTR conversation to have your correspondent one.").format(fingerprint=priv_key),
297 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_INFO
298 },
299 title = _("Fingerprint"),
300 )
301 return {'xmlui': dialog.toXml()}
302
303 def setTrust(raw_data, profile):
304 # This method is called when authentication form is submited
305 data = xml_tools.XMLUIResult2DataFormResult(raw_data)
306 if data['match'] == 'yes':
307 otrctx.setCurrentTrust('verified')
308 note_msg = _("Your correspondant {correspondent} is now TRUSTED")
309 else:
310 otrctx.setCurrentTrust('')
311 note_msg = _("Your correspondant {correspondent} is now UNTRUSTED")
312 note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = {
313 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
314 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.user.name)}
315 )
316 return {'xmlui': note.toXml()}
317
318 submit_id = self.host.registerCallback(setTrust, with_data=True, one_shot=True)
319 trusted = bool(otrctx.getCurrentTrust())
320
321 xmlui = xml_tools.XMLUI(C.XMLUI_FORM, title=_('Authentication (%s)') % to_jid.full(), submit_id=submit_id)
322 xmlui.addText(_(AUTH_TXT))
323 xmlui.addDivider()
324 xmlui.addText(_("Your own fingerprint is:\n{fingerprint}").format(fingerprint=priv_key))
325 xmlui.addText(_("Your correspondent fingerprint should be:\n{fingerprint}").format(fingerprint=other_fingerprint))
326 xmlui.addDivider('blank')
327 xmlui.changeContainer('pairs')
328 xmlui.addLabel(_('Is your correspondent fingerprint the same as here ?'))
329 xmlui.addList("match", [('yes', _('yes')),('no', _('no'))], ['yes' if trusted else 'no'])
330 return {'xmlui': xmlui.toXml()}
236 331
237 def _receivedTreatment(self, data, profile): 332 def _receivedTreatment(self, data, profile):
238 from_jid = jid.JID(data['from']) 333 from_jid = jid.JID(data['from'])
239 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) 334 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
240 otrctx = self.context_managers[profile].getContextForUser(from_jid) 335 otrctx = self.context_managers[profile].getContextForUser(from_jid)