comparison src/plugins/plugin_sec_otr.py @ 2128:aa94f33fd2ad

plugin otr: various improvments: - messageSend trigger now use pre_xml_treatments so it doesn't block other triggers of higher priority - text commands now use a very high priority, as it is local command and should not be blocked in most of cases - new otrState signal, to send state change to frontends - history is not skipped anymore, a future option may change this behaviour - OTR trigger are skipped on groupchat messages - context_manager is now in client instead of being global to plugin - removed fixPotr as it is fixed upstream note triggers should be improved for encryption methods, as skipping an encrypter may break security, but putting it in top priority may break nice features. fix bug 170
author Goffi <goffi@goffi.org>
date Wed, 01 Feb 2017 21:44:24 +0100
parents ca82c97db195
children 6a66c8c5a567
comparison
equal deleted inserted replaced
2127:8717e9cc95c0 2128:aa94f33fd2ad
33 import potr 33 import potr
34 import copy 34 import copy
35 import time 35 import time
36 import uuid 36 import uuid
37 37
38 NS_OTR = "otr_plugin"
39 PRIVATE_KEY = "PRIVATE KEY"
40 OTR_MENU = D_(u'OTR')
41 AUTH_TXT = D_(u"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 gives you is the same as below. If there is a mismatch, there can be a spy between you!")
42 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?")
43 NO_LOG = D_(u"/!\\Your history is not logged anymore, and most of advanced features are disabled !") # FIXME: not used at the moment
44
45 DEFAULT_POLICY_FLAGS = {
46 'ALLOW_V1':False,
47 'ALLOW_V2':True,
48 'REQUIRE_ENCRYPTION':True,
49 }
50
51 38
52 PLUGIN_INFO = { 39 PLUGIN_INFO = {
53 "name": "OTR", 40 "name": "OTR",
54 "import_name": "OTR", 41 "import_name": "OTR",
55 "type": "SEC", 42 "type": "SEC",
57 "dependencies": [], 44 "dependencies": [],
58 "main": "OTR", 45 "main": "OTR",
59 "handler": "no", 46 "handler": "no",
60 "description": _(u"""Implementation of OTR""") 47 "description": _(u"""Implementation of OTR""")
61 } 48 }
49
50 NS_OTR = "otr_plugin"
51 PRIVATE_KEY = "PRIVATE KEY"
52 OTR_MENU = D_(u'OTR')
53 AUTH_TXT = D_(u"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 gives you is the same as below. If there is a mismatch, there can be a spy between you!")
54 DROP_TXT = D_(u"You private key is used to encrypt messages for your correspondent, nobody except you must know it, if you are in doubt, you should drop it!\n\nAre you sure you want to drop your private key?")
55 # NO_LOG_AND = D_(u"/!\\Your history is not logged anymore, and") # FIXME: not used at the moment
56 NO_ADV_FEATURES = D_(u"Most of advanced features are disabled !")
57
58 DEFAULT_POLICY_FLAGS = {
59 'ALLOW_V1':False,
60 'ALLOW_V2':True,
61 'REQUIRE_ENCRYPTION':True,
62 }
63
64 OTR_STATE_TRUSTED = 'trusted'
65 OTR_STATE_UNTRUSTED = 'untrusted'
66 OTR_STATE_UNENCRYPTED = 'unencrypted'
67 OTR_STATE_ENCRYPTED = 'encrypted'
62 68
63 69
64 class Context(potr.context.Context): 70 class Context(potr.context.Context):
65 def __init__(self, host, account, other_jid): 71 def __init__(self, host, account, other_jid):
66 super(Context, self).__init__(account, other_jid) 72 super(Context, self).__init__(account, other_jid)
89 } 95 }
90 self.host.generateMessageXML(mess_data) 96 self.host.generateMessageXML(mess_data)
91 client.xmlstream.send(mess_data['xml']) 97 client.xmlstream.send(mess_data['xml'])
92 98
93 def setState(self, state): 99 def setState(self, state):
100 client = self.user.client
94 old_state = self.state 101 old_state = self.state
95 super(Context, self).setState(state) 102 super(Context, self).setState(state)
96 log.debug(u"setState: %s (old_state=%s)" % (state, old_state)) 103 log.debug(u"setState: %s (old_state=%s)" % (state, old_state))
97 104
98 if state == potr.context.STATE_PLAINTEXT: 105 if state == potr.context.STATE_PLAINTEXT:
99 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()} 106 feedback = _(u"/!\\ conversation with %(other_jid)s is now UNENCRYPTED") % {'other_jid': self.peer.full()}
107 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
100 elif state == potr.context.STATE_ENCRYPTED: 108 elif state == potr.context.STATE_ENCRYPTED:
101 try: 109 try:
102 trusted = self.getCurrentTrust() 110 trusted = self.getCurrentTrust()
103 except TypeError: 111 except TypeError:
104 trusted = False 112 trusted = False
107 if old_state == potr.context.STATE_ENCRYPTED: 115 if old_state == potr.context.STATE_ENCRYPTED:
108 feedback = D_(u"{trusted} OTR conversation with {other_jid} REFRESHED").format( 116 feedback = D_(u"{trusted} OTR conversation with {other_jid} REFRESHED").format(
109 trusted = trusted_str, 117 trusted = trusted_str,
110 other_jid = self.peer.full()) 118 other_jid = self.peer.full())
111 else: 119 else:
112 feedback = D_(u"{trusted} Encrypted OTR conversation started with {other_jid}\n{no_log}").format( 120 feedback = D_(u"{trusted} encrypted OTR conversation started with {other_jid}\n{extra_info}").format(
113 trusted = trusted_str, 121 trusted = trusted_str,
114 other_jid = self.peer.full(), 122 other_jid = self.peer.full(),
115 no_log = NO_LOG) 123 extra_info = NO_ADV_FEATURES)
124 self.host.bridge.otrState(OTR_STATE_ENCRYPTED, self.peer.full(), client.profile)
116 elif state == potr.context.STATE_FINISHED: 125 elif state == potr.context.STATE_FINISHED:
117 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format(other_jid = self.peer.full()) 126 feedback = D_(u"OTR conversation with {other_jid} is FINISHED").format(other_jid = self.peer.full())
127 self.host.bridge.otrState(OTR_STATE_UNENCRYPTED, self.peer.full(), client.profile)
118 else: 128 else:
119 log.error(D_(u"Unknown OTR state")) 129 log.error(D_(u"Unknown OTR state"))
120 return 130 return
121 131
122 client = self.user.client
123 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), 132 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
124 timestamp=time.time(), 133 timestamp=time.time(),
125 from_jid=client.jid.full(), 134 from_jid=client.jid.full(),
126 to_jid=self.peer.full(), 135 to_jid=self.peer.full(),
127 message={u'': feedback}, 136 message={u'': feedback},
128 subject={}, 137 subject={},
129 mess_type=C.MESS_TYPE_INFO, 138 mess_type=C.MESS_TYPE_INFO,
130 extra={}, 139 extra={},
131 profile=client.profile) 140 profile=client.profile)
132 # TODO: send signal to frontends
133 141
134 def disconnect(self): 142 def disconnect(self):
135 """Disconnect the session.""" 143 """Disconnect the session."""
136 if self.state != potr.context.STATE_PLAINTEXT: 144 if self.state != potr.context.STATE_PLAINTEXT:
137 super(Context, self).disconnect() 145 super(Context, self).disconnect()
162 if self.privkey is None: 170 if self.privkey is None:
163 raise exceptions.InternalError(_(u"Save is called but privkey is None !")) 171 raise exceptions.InternalError(_(u"Save is called but privkey is None !"))
164 priv_key = self.privkey.serializePrivateKey().encode('hex') 172 priv_key = self.privkey.serializePrivateKey().encode('hex')
165 d = self.host.memory.encryptValue(priv_key, self.client.profile) 173 d = self.host.memory.encryptValue(priv_key, self.client.profile)
166 def save_encrypted_key(encrypted_priv_key): 174 def save_encrypted_key(encrypted_priv_key):
167 self.client.otr_data[PRIVATE_KEY] = encrypted_priv_key 175 self.client._otr_data[PRIVATE_KEY] = encrypted_priv_key
168 d.addCallback(save_encrypted_key) 176 d.addCallback(save_encrypted_key)
169 177
170 def loadTrusts(self): 178 def loadTrusts(self):
171 trust_data = self.client.otr_data.get('trust', {}) 179 trust_data = self.client._otr_data.get('trust', {})
172 for jid_, jid_data in trust_data.iteritems(): 180 for jid_, jid_data in trust_data.iteritems():
173 for fingerprint, trust_level in jid_data.iteritems(): 181 for fingerprint, trust_level in jid_data.iteritems():
174 log.debug(u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level)) 182 log.debug(u'setting trust for {jid}: [{fingerprint}] = "{trust_level}"'.format(jid=jid_, fingerprint=fingerprint, trust_level=trust_level))
175 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level 183 self.trusts.setdefault(jid.JID(jid_), {})[fingerprint] = trust_level
176 184
177 def saveTrusts(self): 185 def saveTrusts(self):
178 log.debug(u"saving trusts for {profile}".format(profile=self.client.profile)) 186 log.debug(u"saving trusts for {profile}".format(profile=self.client.profile))
179 log.debug(u"trusts = {}".format(self.client.otr_data['trust'])) 187 log.debug(u"trusts = {}".format(self.client._otr_data['trust']))
180 self.client.otr_data.force('trust') 188 self.client._otr_data.force('trust')
181 189
182 def setTrust(self, other_jid, fingerprint, trustLevel): 190 def setTrust(self, other_jid, fingerprint, trustLevel):
183 try: 191 try:
184 trust_data = self.client.otr_data['trust'] 192 trust_data = self.client._otr_data['trust']
185 except KeyError: 193 except KeyError:
186 trust_data = {} 194 trust_data = {}
187 self.client.otr_data['trust'] = trust_data 195 self.client._otr_data['trust'] = trust_data
188 jid_data = trust_data.setdefault(other_jid.full(), {}) 196 jid_data = trust_data.setdefault(other_jid.full(), {})
189 jid_data[fingerprint] = trustLevel 197 jid_data[fingerprint] = trustLevel
190 super(Account, self).setTrust(other_jid, fingerprint, trustLevel) 198 super(Account, self).setTrust(other_jid, fingerprint, trustLevel)
191 199
192 200
211 219
212 class OTR(object): 220 class OTR(object):
213 221
214 def __init__(self, host): 222 def __init__(self, host):
215 log.info(_(u"OTR plugin initialization")) 223 log.info(_(u"OTR plugin initialization"))
216 self._fixPotr() # FIXME: to be removed when potr will be fixed
217 self.host = host 224 self.host = host
218 self.context_managers = {} 225 self.context_managers = {}
219 self.skipped_profiles = set() 226 self.skipped_profiles = set() # FIXME: OTR should not be skipped per profile, this need to be refactored
220 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000) 227 host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
221 host.trigger.add("messageSend", self.messageSendTrigger, priority=100000) 228 host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
222 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis 229 host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis
230 host.bridge.addSignal("otrState", ".plugin", signature='sss') # args: state, destinee_jid, profile
223 host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE) 231 host.importMenu((OTR_MENU, D_(u"Start/Refresh")), self._otrStartRefresh, security_limit=0, help_string=D_(u"Start or refresh an OTR session"), type_=C.MENU_SINGLE)
224 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE) 232 host.importMenu((OTR_MENU, D_(u"End session")), self._otrSessionEnd, security_limit=0, help_string=D_(u"Finish an OTR session"), type_=C.MENU_SINGLE)
225 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE) 233 host.importMenu((OTR_MENU, D_(u"Authenticate")), self._otrAuthenticate, security_limit=0, help_string=D_(u"Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
226 host.importMenu((OTR_MENU, D_(u"Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE) 234 host.importMenu((OTR_MENU, D_(u"Drop private key")), self._dropPrivKey, security_limit=0, type_=C.MENU_SINGLE)
227 host.trigger.add("presenceReceived", self._presenceReceivedTrigger) 235 host.trigger.add("presenceReceived", self._presenceReceivedTrigger)
228 236
229 def _fixPotr(self):
230 # FIXME: potr fix for bad unicode handling
231 # this method monkeypatch it, must be removed when potr
232 # is fixed
233
234 def getDefaultQueryMessage(self, policy):
235 defaultQuery = '?OTRv{versions}?\nI would like to start ' \
236 'an Off-the-Record private conversation. However, you ' \
237 'do not have a plugin to support that.\nSee '\
238 'https://otr.cypherpunks.ca/ for more information.'
239 v = '2' if policy('ALLOW_V2') else ''
240 msg = defaultQuery.format(versions=v)
241 return msg.encode('ascii')
242
243 potr.context.Account.getDefaultQueryMessage = getDefaultQueryMessage
244
245 def _skipOTR(self, profile): 237 def _skipOTR(self, profile):
246 """Tell the backend to not handle OTR for this profile. 238 """Tell the backend to not handle OTR for this profile.
247 239
248 @param profile (str): %(doc_profile)s 240 @param profile (str): %(doc_profile)s
249 """ 241 """
255 @defer.inlineCallbacks 247 @defer.inlineCallbacks
256 def profileConnected(self, profile): 248 def profileConnected(self, profile):
257 if profile in self.skipped_profiles: 249 if profile in self.skipped_profiles:
258 return 250 return
259 client = self.host.getClient(profile) 251 client = self.host.getClient(profile)
260 ctxMng = self.context_managers[profile] = ContextManager(self.host, client) 252 ctxMng = client._otr_context_manager = ContextManager(self.host, client)
261 client.otr_data = persistent.PersistentBinaryDict(NS_OTR, profile) 253 client._otr_data = persistent.PersistentBinaryDict(NS_OTR, profile)
262 yield client.otr_data.load() 254 yield client._otr_data.load()
263 encrypted_priv_key = client.otr_data.get(PRIVATE_KEY, None) 255 encrypted_priv_key = client._otr_data.get(PRIVATE_KEY, None)
264 if encrypted_priv_key is not None: 256 if encrypted_priv_key is not None:
265 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile) 257 priv_key = yield self.host.memory.decryptValue(encrypted_priv_key, profile)
266 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0] 258 ctxMng.account.privkey = potr.crypt.PK.parsePrivateKey(priv_key.decode('hex'))[0]
267 else: 259 else:
268 ctxMng.account.privkey = None 260 ctxMng.account.privkey = None
269 ctxMng.account.loadTrusts() 261 ctxMng.account.loadTrusts()
270 262
271 def profileDisconnected(self, profile): 263 def profileDisconnected(self, profile):
272 try: 264 if profile in self.skipped_profiles:
273 for context in self.context_managers[profile].contexts.values():
274 context.disconnect()
275 del self.context_managers[profile]
276 except KeyError:
277 pass
278 try:
279 self.skipped_profiles.remove(profile) 265 self.skipped_profiles.remove(profile)
280 except KeyError: 266 return
281 pass 267 client = self.host.getClient(profile)
268 for context in client._otr_context_manager.contexts.values():
269 context.disconnect()
270 del client._otr_context_manager
282 271
283 def _otrStartRefresh(self, menu_data, profile): 272 def _otrStartRefresh(self, menu_data, profile):
284 """Start or refresh an OTR session 273 """Start or refresh an OTR session
285 274
286 @param menu_data: %(menu_data)s 275 @param menu_data: %(menu_data)s
300 289
301 @param to_jid(jid.JID): jid to start encrypted session with 290 @param to_jid(jid.JID): jid to start encrypted session with
302 """ 291 """
303 if not to_jid.resource: 292 if not to_jid.resource:
304 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored 293 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
305 otrctx = self.context_managers[client.profile].getContextForUser(to_jid) 294 otrctx = client._otr_context_manager.getContextForUser(to_jid)
306 query = otrctx.sendMessage(0, '?OTRv?') 295 query = otrctx.sendMessage(0, '?OTRv?')
307 otrctx.inject(query) 296 otrctx.inject(query)
308 297
309 def _otrSessionEnd(self, menu_data, profile): 298 def _otrSessionEnd(self, menu_data, profile):
310 """End an OTR session 299 """End an OTR session
323 312
324 def endSession(self, client, to_jid): 313 def endSession(self, client, to_jid):
325 """End an OTR session""" 314 """End an OTR session"""
326 if not to_jid.resource: 315 if not to_jid.resource:
327 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored 316 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
328 otrctx = self.context_managers[client.profile].getContextForUser(to_jid) 317 otrctx = client._otr_context_manager.getContextForUser(to_jid)
329 otrctx.disconnect() 318 otrctx.disconnect()
330 return {} 319 return {}
331 320
332 def _otrAuthenticate(self, menu_data, profile): 321 def _otrAuthenticate(self, menu_data, profile):
333 """End an OTR session 322 """End an OTR session
345 334
346 def authenticate(self, client, to_jid): 335 def authenticate(self, client, to_jid):
347 """Authenticate other user and see our own fingerprint""" 336 """Authenticate other user and see our own fingerprint"""
348 if not to_jid.resource: 337 if not to_jid.resource:
349 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored 338 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
350 ctxMng = self.context_managers[client.profile] 339 ctxMng = client._otr_context_manager
351 otrctx = ctxMng.getContextForUser(to_jid) 340 otrctx = ctxMng.getContextForUser(to_jid)
352 priv_key = ctxMng.account.privkey 341 priv_key = ctxMng.account.privkey
353 342
354 if priv_key is None: 343 if priv_key is None:
355 # we have no private key yet 344 # we have no private key yet
377 366
378 def setTrust(raw_data, profile): 367 def setTrust(raw_data, profile):
379 # This method is called when authentication form is submited 368 # This method is called when authentication form is submited
380 data = xml_tools.XMLUIResult2DataFormResult(raw_data) 369 data = xml_tools.XMLUIResult2DataFormResult(raw_data)
381 if data['match'] == 'yes': 370 if data['match'] == 'yes':
382 otrctx.setCurrentTrust('verified') 371 otrctx.setCurrentTrust(OTR_STATE_TRUSTED)
383 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED") 372 note_msg = _(u"Your correspondent {correspondent} is now TRUSTED")
373 self.host.bridge.otrState(OTR_STATE_TRUSTED, to_jid.full(), client.profile)
384 else: 374 else:
385 otrctx.setCurrentTrust('') 375 otrctx.setCurrentTrust('')
386 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED") 376 note_msg = _(u"Your correspondent {correspondent} is now UNTRUSTED")
377 self.host.bridge.otrState(OTR_STATE_UNTRUSTED, to_jid.full(), client.profile)
387 note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = { 378 note = xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = {
388 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, 379 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
389 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer)} 380 C.XMLUI_DATA_MESS: note_msg.format(correspondent=otrctx.peer)}
390 ) 381 )
391 return {'xmlui': note.toXml()} 382 return {'xmlui': note.toXml()}
417 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored 408 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: temporary and unsecure, must be changed when frontends are refactored
418 except KeyError: 409 except KeyError:
419 log.error(_(u"jid key is not present !")) 410 log.error(_(u"jid key is not present !"))
420 return defer.fail(exceptions.DataError) 411 return defer.fail(exceptions.DataError)
421 412
422 ctxMng = self.context_managers[profile] 413 ctxMng = client._otr_context_manager
423 if ctxMng.account.privkey is None: 414 if ctxMng.account.privkey is None:
424 return {'xmlui': xml_tools.note(_(u"You don't have a private key yet !")).toXml()} 415 return {'xmlui': xml_tools.note(_(u"You don't have a private key yet !")).toXml()}
425 416
426 def dropKey(data, profile): 417 def dropKey(data, profile):
427 if C.bool(data['answer']): 418 if C.bool(data['answer']):
428 # we end all sessions 419 # we end all sessions
429 for context in ctxMng.contexts.values(): 420 for context in ctxMng.contexts.values():
430 context.disconnect() 421 context.disconnect()
431 ctxMng.account.privkey = None 422 ctxMng.account.privkey = None
432 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey will generate a new key, and save it 423 ctxMng.account.getPrivkey() # as account.privkey is None, getPrivkey will generate a new key, and save it
433 return {'xmlui': xml_tools.note(_(u"Your private key has been dropped")).toXml()} 424 return {'xmlui': xml_tools.note(D_(u"Your private key has been dropped")).toXml()}
434 return {} 425 return {}
435 426
436 submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True) 427 submit_id = self.host.registerCallback(dropKey, with_data=True, one_shot=True)
437 428
438 confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_('Confirm private key drop'), dialog_opt = {'type': C.XMLUI_DIALOG_CONFIRM, 'message': _(DROP_TXT)}, submit_id = submit_id) 429 confirm = xml_tools.XMLUI(C.XMLUI_DIALOG, title=_(u'Confirm private key drop'), dialog_opt = {'type': C.XMLUI_DIALOG_CONFIRM, 'message': _(DROP_TXT)}, submit_id = submit_id)
439 return {'xmlui': confirm.toXml()} 430 return {'xmlui': confirm.toXml()}
440 431
441 def _receivedTreatment(self, data, profile): 432 def _receivedTreatment(self, data, client):
442 from_jid = data['from'] 433 from_jid = data['from']
443 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid) 434 log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
444 otrctx = self.context_managers[profile].getContextForUser(from_jid) 435 otrctx = client._otr_context_manager.getContextForUser(from_jid)
445 encrypted = True
446 436
447 try: 437 try:
448 message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict 438 message = data['message'].itervalues().next() # FIXME: Q&D fix for message refactoring, message is now a dict
449 res = otrctx.receiveMessage(message.encode('utf-8')) 439 res = otrctx.receiveMessage(message.encode('utf-8'))
450 except potr.context.UnencryptedMessage: 440 except potr.context.UnencryptedMessage:
441 encrypted = False
451 if otrctx.state == potr.context.STATE_ENCRYPTED: 442 if otrctx.state == potr.context.STATE_ENCRYPTED:
452 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': from_jid.full()}) 443 log.warning(u"Received unencrypted message in an encrypted context (from {jid})".format(
453 client = self.host.getClient(profile) 444 jid = from_jid.full()))
454 445
455 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"), 446 feedback=D_(u"WARNING: received unencrypted data in a supposedly encrypted context"),
456 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), 447 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
457 timestamp=time.time(), 448 timestamp=time.time(),
458 from_jid=from_jid.full(), 449 from_jid=from_jid.full(),
460 message={u'': feedback}, 451 message={u'': feedback},
461 subject={}, 452 subject={},
462 mess_type=C.MESS_TYPE_INFO, 453 mess_type=C.MESS_TYPE_INFO,
463 extra={}, 454 extra={},
464 profile=client.profile) 455 profile=client.profile)
465 encrypted = False
466 except StopIteration: 456 except StopIteration:
467 return data 457 return data
468
469 if not encrypted:
470 return data
471 else: 458 else:
459 encrypted = True
460
461 if encrypted:
472 if res[0] != None: 462 if res[0] != None:
473 # decrypted messages handling. 463 # decrypted messages handling.
474 # receiveMessage() will return a tuple, the first part of which will be the decrypted message 464 # receiveMessage() will return a tuple, the first part of which will be the decrypted message
475 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict 465 data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
476 raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history 466 # TODO: add skip history as an option, but by default we don't skip it
467 # raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
477 else: 468 else:
469 log.warning(u"An encrypted message was expected, but got {}".format(data['message']))
478 raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal) 470 raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal)
479 471 return data
480 def _receivedTreatmentForSkippedProfiles(self, data, profile): 472
473 def _receivedTreatmentForSkippedProfiles(self, data):
481 """This profile must be skipped because the frontend manages OTR itself, 474 """This profile must be skipped because the frontend manages OTR itself,
482 but we still need to check if the message must be stored in history or not""" 475
476 but we still need to check if the message must be stored in history or not
477 """
478 # XXX: FIXME: this should not be done on a per-profile basis, but per-message
483 try: 479 try:
484 message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict 480 message = data['message'].itervalues().next().encode('utf-8') # FIXME: Q&D fix for message refactoring, message is now a dict
485 except StopIteration: 481 except StopIteration:
486 return data 482 return data
487 if message.startswith(potr.proto.OTRTAG): 483 if message.startswith(potr.proto.OTRTAG):
488 raise failure.Failure(exceptions.SkipHistory()) 484 raise failure.Failure(exceptions.SkipHistory())
489 return data 485 return data
490 486
491 def MessageReceivedTrigger(self, client, message_elt, post_treat): 487 def MessageReceivedTrigger(self, client, message_elt, post_treat):
492 profile = client.profile 488 if message_elt.getAttribute('type') == C.MESS_TYPE_GROUPCHAT:
493 if profile in self.skipped_profiles: 489 # OTR is not possible in group chats
494 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile) 490 return True
491 if client.profile in self.skipped_profiles:
492 post_treat.addCallback(self._receivedTreatmentForSkippedProfiles)
495 else: 493 else:
496 post_treat.addCallback(self._receivedTreatment, profile) 494 post_treat.addCallback(self._receivedTreatment, client)
497 return True 495 return True
498 496
499 def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): 497 def _messageSendOTR(self, mess_data, client):
500 profile = client.profile
501 if profile in self.skipped_profiles:
502 return True
503 to_jid = copy.copy(mess_data['to']) 498 to_jid = copy.copy(mess_data['to'])
504 if mess_data['type'] != 'groupchat' and not to_jid.resource: 499 if not to_jid.resource:
505 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: it's dirty, but frontends don't manage resources correctly now, refactoring is planed 500 to_jid.resource = self.host.memory.getMainResource(client, to_jid) # FIXME: full jid may not be known
506 otrctx = self.context_managers[profile].getContextForUser(to_jid) 501 otrctx = client._otr_context_manager.getContextForUser(to_jid)
507 if mess_data['type'] != 'groupchat' and otrctx.state != potr.context.STATE_PLAINTEXT: 502 if otrctx.state != potr.context.STATE_PLAINTEXT:
508 if otrctx.state == potr.context.STATE_ENCRYPTED: 503 if otrctx.state == potr.context.STATE_ENCRYPTED:
509 log.debug(u"encrypting message") 504 log.debug(u"encrypting message")
510 try: 505 try:
511 msg = mess_data['message'][''] 506 msg = mess_data['message']['']
512 except KeyError: 507 except KeyError:
513 try: 508 try:
514 msg = mess_data['message'].itervalues().next() 509 msg = mess_data['message'].itervalues().next()
515 except StopIteration: 510 except StopIteration:
516 log.warning(u"No message found") 511 log.warning(u"No message found")
517 return False 512 for key, value in mess_data['extra'].iteritems():
513 if key.startswith('rich') or key.startswith('xhtml'):
514 log.error(u'received rich content while OTR encryption is activated, cancelling')
515 raise failure.Failure(exceptions.CancelError())
516 return mess_data
518 otrctx.sendMessage(0, msg.encode('utf-8')) 517 otrctx.sendMessage(0, msg.encode('utf-8'))
518 self.host.messageAddToHistory(mess_data, client)
519 self.host.messageSendToBridge(mess_data, client) 519 self.host.messageSendToBridge(mess_data, client)
520 else: 520 else:
521 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.") 521 feedback = D_(u"Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.")
522 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()), 522 self.host.bridge.messageNew(uid=unicode(uuid.uuid4()),
523 timestamp=time.time(), 523 timestamp=time.time(),
526 message={u'': feedback}, 526 message={u'': feedback},
527 subject={}, 527 subject={},
528 mess_type=C.MESS_TYPE_INFO, 528 mess_type=C.MESS_TYPE_INFO,
529 extra={}, 529 extra={},
530 profile=client.profile) 530 profile=client.profile)
531 return False 531 # we stop treatment here for OTR, as message is already sent
532 raise failure.Failure(exceptions.CancelError())
532 else: 533 else:
533 log.debug(u"sending message unencrypted") 534 return mess_data
535
536 def messageSendTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments):
537 if mess_data['type'] == 'groupchat':
534 return True 538 return True
539 if client.profile in self.skipped_profiles: # FIXME: should not be done on a per-profile basis
540 return True
541 pre_xml_treatments.addCallback(self._messageSendOTR, client)
542 return True
535 543
536 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile): 544 def _presenceReceivedTrigger(self, entity, show, priority, statuses, profile):
537 client = self.host.getClient(profile)
538 if show != C.PRESENCE_UNAVAILABLE: 545 if show != C.PRESENCE_UNAVAILABLE:
539 return True 546 return True
547 client = self.host.getClient(profile)
540 if not entity.resource: 548 if not entity.resource:
541 try: 549 try:
542 entity.resource = self.host.memory.getMainResource(client, entity) # FIXME: temporary and unsecure, must be changed when frontends are refactored 550 entity.resource = self.host.memory.getMainResource(client, entity) # FIXME: temporary and unsecure, must be changed when frontends are refactored
543 except exceptions.UnknownEntityError: 551 except exceptions.UnknownEntityError:
544 return True # entity was not connected 552 return True # entity was not connected
545 if entity in self.context_managers[profile].contexts: 553 if entity in client._otr_context_manager.contexts:
546 otrctx = self.context_managers[profile].getContextForUser(entity) 554 otrctx = client._otr_context_manager.getContextForUser(entity)
547 otrctx.disconnect() 555 otrctx.disconnect()
548 return True 556 return True