comparison src/browser/sat_browser/plugin_sec_otr.py @ 663:423182fea41c frontends_multi_profiles

browser_side: fixes OTR using the new resource system and proper triggers (send and receive message) or listener (presence update)
author souliane <souliane@mailoo.org>
date Tue, 03 Mar 2015 07:21:50 +0100
parents ebb602d8b3f2
children 8449a5db0602
comparison
equal deleted inserted replaced
662:ebb602d8b3f2 663:423182fea41c
21 """ 21 """
22 This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js. 22 This file is adapted from sat.plugins.plugin.sec_otr. It offers browser-side OTR encryption using otr.js.
23 The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). 23 The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca).
24 """ 24 """
25 25
26 from sat.core.log import getLogger
27 log = getLogger(__name__)
28
26 from sat.core.i18n import _, D_ 29 from sat.core.i18n import _, D_
27 from sat.core.log import getLogger
28 from sat.core import exceptions 30 from sat.core import exceptions
29 log = getLogger(__name__) 31 from sat.tools.misc import TriggerManager
30 32
31 from constants import Const as C 33 from constants import Const as C
32 from sat_frontends.tools import jid 34 from sat_frontends.tools import jid
33 import otrjs_wrapper as otr 35 import otrjs_wrapper as otr
34 import dialog 36 import dialog
37 39
38 NS_OTR = "otr_plugin" 40 NS_OTR = "otr_plugin"
39 PRIVATE_KEY = "PRIVATE KEY" 41 PRIVATE_KEY = "PRIVATE KEY"
40 MAIN_MENU = D_('OTR encryption') 42 MAIN_MENU = D_('OTR encryption')
41 DIALOG_EOL = "<br />" 43 DIALOG_EOL = "<br />"
42 DIALOG_USERS_ML = D_("<a href='mailto:users@salut-a-toi.org?subject={subject}&body=Please give us some hints about how to reproduce the bug (your browser name and version, what you did and what happened)'>users@salut-a-toi.org</a>")
43 44
44 AUTH_TRUSTED = D_("Verified") 45 AUTH_TRUSTED = D_("Verified")
45 AUTH_UNTRUSTED = D_("Unverified") 46 AUTH_UNTRUSTED = D_("Unverified")
46 AUTH_OTHER_TITLE = D_("Authentication of {jid}") 47 AUTH_OTHER_TITLE = D_("Authentication of {jid}")
47 AUTH_US_TITLE = D_("Authentication to {jid}") 48 AUTH_US_TITLE = D_("Authentication to {jid}")
70 QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...') 71 QUERY_ENCRYPTED = D_('Attempting to refresh the OTR conversation with {jid}...')
71 QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...') 72 QUERY_NOT_ENCRYPTED = D_('Attempting to start an OTR conversation with {jid}...')
72 AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") 73 AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.")
73 AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") 74 AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!")
74 END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") 75 END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!")
75 END_PLAIN = D_("Your conversation with {jid} is no more or hasn't been encrypted.") 76 END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.")
77 END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.")
76 END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") 78 END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.")
77 79
78 KEY_TITLE = D_('Private key') 80 KEY_TITLE = D_('Private key')
79 KEY_NA_TITLE = D_("No private key") 81 KEY_NA_TITLE = D_("No private key")
80 KEY_NA_TXT = D_("You don't have any private key yet.") 82 KEY_NA_TXT = D_("You don't have any private key yet.")
90 QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}") 92 QUERY_KEY = D_("You already have a private key, but to start the conversation will still require a couple of seconds.{eol}{eol}")
91 QUERY_CONFIRM = D_("Press OK to start now the encryption.") 93 QUERY_CONFIRM = D_("Press OK to start now the encryption.")
92 94
93 ACTION_NA_TITLE = D_("Impossible action") 95 ACTION_NA_TITLE = D_("Impossible action")
94 ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") 96 ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.")
95 RESOURCE_ISSUE_TITLE = D_("Security issue")
96 RESOURCE_ISSUE = D_("Your correspondent's resource is unknown!{eol}{eol}You should stop any OTR conversation with {jid} to avoid sending him unencrypted messages in an encrypted context.{eol}{eol}Please report the bug to the users mailing list: {users_ml}.")
97 97
98 DEFAULT_POLICY_FLAGS = { 98 DEFAULT_POLICY_FLAGS = {
99 'ALLOW_V2': True, 99 'ALLOW_V2': True,
100 'ALLOW_V3': True, 100 'ALLOW_V3': True,
101 'REQUIRE_ENCRYPTION': False, 101 'REQUIRE_ENCRYPTION': False,
102 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTrigger before turning this to True 102 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True
103 'WHITESPACE_START_AKE': False, # FIXME: we need to complete messageReceivedTrigger before turning this to True 103 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True
104 } 104 }
105 105
106 # list a couple of texts or htmls (untrusted, trusted) for each state 106 # list a couple of texts or htmls (untrusted, trusted) for each state
107 OTR_MSG_STATES = { 107 OTR_MSG_STATES = {
108 otr.context.STATE_PLAINTEXT: [ 108 otr.context.STATE_PLAINTEXT: [
118 '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />' 118 '<img src="media/icons/silk/lock_break.png" /><img src="media/icons/silk/key.png" />'
119 ] 119 ]
120 } 120 }
121 121
122 122
123 unicode = str # FIXME: pyjamas workaround
124
125
126 class NotConnectedEntity(Exception):
127 pass
128
129
123 class Context(otr.context.Context): 130 class Context(otr.context.Context):
124 131
125 def __init__(self, host, account, other_jid): 132 def __init__(self, host, account, other_jid):
126 """ 133 """
127 134
156 log.warning("A plain-text message has been handled by otr.js") 163 log.warning("A plain-text message has been handled by otr.js")
157 log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg)) 164 log.debug("message received (was %s): %s" % ('encrypted' if encrypted else 'plain', msg))
158 if not encrypted: 165 if not encrypted:
159 if self.state == otr.context.STATE_ENCRYPTED: 166 if self.state == otr.context.STATE_ENCRYPTED:
160 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) 167 log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer})
161 self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {}) 168 self.host.newMessageHandler(unicode(self.peer), RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, unicode(self.host.whoami), {})
162 self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {}) 169 self.host.newMessageHandler(unicode(self.peer), msg, C.MESS_TYPE_CHAT, unicode(self.host.whoami), {})
163 170
164 def sendMessageCb(self, msg, meta=None): 171 def sendMessageCb(self, msg, meta=None):
165 assert isinstance(self.peer, jid.JID) 172 assert isinstance(self.peer, jid.JID)
166 log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) 173 log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg))
167 self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer, msg, '', 'chat', {'send_only': 'true'}) 174 self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'})
168 175
169 def messageErrorCb(self, error): 176 def messageErrorCb(self, error):
170 log.error('error occured: %s' % error) 177 log.error('error occured: %s' % error)
171 178
172 def setStateCb(self, msg_state, status): 179 def setStateCb(self, msg_state, status):
184 trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED 191 trusted_str = AUTH_TRUSTED if trust else AUTH_UNTRUSTED
185 feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED 192 feedback = (trusted_str + AKE_ENCRYPTED) if msg_state == otr.context.STATE_ENCRYPTED else AKE_NOT_ENCRYPTED
186 193
187 elif status == otr.context.STATUS_END_OTR: 194 elif status == otr.context.STATUS_END_OTR:
188 if msg_state == otr.context.STATE_PLAINTEXT: 195 if msg_state == otr.context.STATE_PLAINTEXT:
189 feedback = END_PLAIN 196 feedback = END_PLAIN_NO_MORE
190 elif msg_state == otr.context.STATE_ENCRYPTED: 197 elif msg_state == otr.context.STATE_ENCRYPTED:
191 log.error(END_ENCRYPTED) 198 log.error(END_ENCRYPTED)
192 elif msg_state == otr.context.STATE_FINISHED: 199 elif msg_state == otr.context.STATE_FINISHED:
193 feedback = END_FINISHED 200 feedback = END_FINISHED
194 201
195 self.host.newMessageCb(self.peer, feedback.format(jid=other_jid_s), C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(msg_state, trust)}) 202 self.host.newMessageHandler(unicode(self.peer), feedback.format(jid=other_jid_s), C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(msg_state, trust)})
196 203
197 def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): 204 def setCurrentTrust(self, new_trust='', act='asked', type_='trust'):
198 log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) 205 log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act))
199 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer) 206 title = (AUTH_OTHER_TITLE if act == "asked" else AUTH_US_TITLE).format(jid=self.peer)
200 old_trust = self.getCurrentTrust() 207 old_trust = self.getCurrentTrust()
213 if act != "asked": 220 if act != "asked":
214 return 221 return
215 otr.context.Context.setCurrentTrust(self, new_trust) 222 otr.context.Context.setCurrentTrust(self, new_trust)
216 if old_trust != new_trust: 223 if old_trust != new_trust:
217 feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) 224 feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower())
218 self.host.newMessageCb(self.peer, feedback, C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(self.state, new_trust)}) 225 self.host.newMessageHandler(unicode(self.peer), feedback, C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(self.state, new_trust)})
219 226
220 def fingerprintAuthCb(self): 227 def fingerprintAuthCb(self):
221 """OTR v2 authentication using manual fingerprint comparison""" 228 """OTR v2 authentication using manual fingerprint comparison"""
222 priv_key = self.user.privkey 229 priv_key = self.user.privkey
223 230
325 self.host = host 332 self.host = host
326 self.account = Account(host) 333 self.account = Account(host)
327 self.contexts = {} 334 self.contexts = {}
328 335
329 def startContext(self, other_jid): 336 def startContext(self, other_jid):
330 assert isinstance(other_jid, jid.JID) 337 assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID
331 # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition 338 # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition
332 #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) 339 #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid))
333 if other_jid not in self.contexts: 340 if other_jid not in self.contexts:
334 self.contexts[other_jid] = Context(self.host, self.account, other_jid) 341 self.contexts[other_jid] = Context(self.host, self.account, other_jid)
335 return self.contexts[other_jid] 342 return self.contexts[other_jid]
339 346
340 @param other_jid (jid.JID): your correspondent 347 @param other_jid (jid.JID): your correspondent
341 @param start (bool): start non-existing context if True 348 @param start (bool): start non-existing context if True
342 @return: Context 349 @return: Context
343 """ 350 """
351 try:
352 other_jid = self.fixResource(other_jid)
353 except NotConnectedEntity:
354 log.debug(u"getContextForUser [%s]: not connected!" % other_jid)
355 return None
344 log.debug(u"getContextForUser [%s]" % other_jid) 356 log.debug(u"getContextForUser [%s]" % other_jid)
345 if not other_jid.resource:
346 log.error("getContextForUser called with a bare jid")
347 running_sessions = [jid_.bare for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED]
348 if start or (other_jid in running_sessions):
349 users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context"))
350 text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid, users_ml=users_ml)
351 dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show()
352 return None # never start an OTR session with a bare JID
353 if start: 357 if start:
354 return self.startContext(other_jid) 358 return self.startContext(other_jid)
355 else: 359 else:
356 return self.contexts.get(other_jid, None) 360 return self.contexts.get(other_jid, None)
357 361
362 def fixResource(self, other_jid):
363 """Return the full JID in case the resource of the given JID is missing.
364
365 @param other_jid (jid.JID): JID to check
366 @return jid.JID
367 """
368 if other_jid.resource:
369 return other_jid
370 clist = self.host.contact_list
371 if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
372 raise NotConnectedEntity
373 return clist.getFullJid(other_jid)
374
358 375
359 class OTR(object): 376 class OTR(object):
360 377
361 def __init__(self, host): 378 def __init__(self, host):
362 log.info(_(u"OTR plugin initialization")) 379 log.info(_(u"OTR plugin initialization"))
363 self.host = host 380 self.host = host
364 self.context_manager = None 381 self.context_manager = None
365 self.last_resources = {}
366 self.host.bridge._registerMethods(["skipOTR"]) 382 self.host.bridge._registerMethods(["skipOTR"])
383 self.host.trigger.add("newMessage", self.newMessageTg, priority=TriggerManager.MAX_PRIORITY)
384 self.host.trigger.add("sendMessage", self.sendMessageTg, priority=TriggerManager.MAX_PRIORITY)
367 385
368 @classmethod 386 @classmethod
369 def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): 387 def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''):
370 """Get the widget info text for a certain message state and trust. 388 """Get the widget info text for a certain message state and trust.
371 389
375 """ 393 """
376 if not state: 394 if not state:
377 state = OTR_MSG_STATES.keys()[0] 395 state = OTR_MSG_STATES.keys()[0]
378 return OTR_MSG_STATES[state][1 if trust else 0] 396 return OTR_MSG_STATES[state][1 if trust else 0]
379 397
380 def infoTextCallback(self, other_jid, cb): 398 def getInfoTextForUser(self, other_jid):
381 """Get the current info text for a conversation and run a callback. 399 """Get the current info text for a conversation.
382 400
383 @param other_jid (jid.JID): JID of the correspondant 401 @param other_jid (jid.JID): JID of the correspondant
384 @paam cb (callable): method to be called with the computed info text 402 """
385 """ 403 otrctx = self.context_manager.getContextForUser(other_jid, start=False)
386 def gotResource(other_jid): 404 if otrctx is None:
387 otrctx = self.context_manager.getContextForUser(other_jid, start=False) 405 return OTR.getInfoText()
388 if otrctx is None: 406 else:
389 cb(OTR.getInfoText()) 407 return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust())
390 else:
391 cb(OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()))
392
393 self.fixResource(other_jid, gotResource)
394 408
395 def inhibitMenus(self): 409 def inhibitMenus(self):
396 """Tell the caller which dynamic menus should be inhibited""" 410 """Tell the caller which dynamic menus should be inhibited"""
397 return ["OTR"] # menu categories name to inhibit 411 return ["OTR"] # menu categories name to inhibit
398 412
408 self.context_manager = ContextManager(self.host) 422 self.context_manager = ContextManager(self.host)
409 # TODO: retrieve the encrypted private key from a HTML5 persistent storage, 423 # TODO: retrieve the encrypted private key from a HTML5 persistent storage,
410 # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and 424 # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and
411 # assign it to self.context_manager.account.privkey 425 # assign it to self.context_manager.account.privkey
412 426
427 # FIXME: workaround for a pyjamas issue: calling hash on a class method always return a different value if that method is defined directly within the class (with the "def" keyword)
428 self.presenceListener = self.onPresenceUpdate
429 self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE])
430
413 def profileDisconnected(self): 431 def profileDisconnected(self):
414 for context in self.context_manager.contexts.values(): 432 for context in self.context_manager.contexts.values():
415 context.disconnect() 433 context.disconnect()
416 434 self.host.removeListener('presence', self.presenceListener)
417 def fixResource(self, jid_, cb): 435
418 # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed 436 def newMessageTg(self, from_jid, msg, msg_type, to_jid, extra, profile):
419 if jid_.resource: 437 if msg_type != C.MESS_TYPE_CHAT:
420 self.last_resources[jid_.bare] = jid_.resource
421 cb(jid_)
422 elif jid_.bare in self.last_resources:
423 # FIXME: to be removed: must use new resource system
424 # jid_.setResource(self.last_resources[jid_.bare])
425 cb(jid_)
426 else:
427 pass # FIXME: to be removed: must use new resource system
428 # def gotResource(resource):
429 # if resource:
430 # jid_.setResource(resource)
431 # self.last_resources[jid_.bare] = jid_.resource
432 # cb(jid_)
433 #
434 # self.host.bridge.call('getLastResource', gotResource, jid_)
435
436 def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra):
437 if msg_type == C.MESS_TYPE_INFO:
438 return True 438 return True
439 439
440 tag = otr.proto.checkForOTR(msg) 440 tag = otr.proto.checkForOTR(msg)
441 if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): 441 if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']):
442 return True 442 return True
443 443
444 def decrypt(context):
445 context.receiveMessage(msg)
446
447 def cb(jid_):
448 otrctx = self.context_manager.getContextForUser(jid_, start=False)
449 if otrctx is None:
450 def confirm(confirm):
451 if confirm:
452 self.host.displayWidget(chat.Chat, jid_)
453 decrypt(self.context_manager.startContext(jid_))
454 else:
455 # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True
456 pass
457 key = self.context_manager.account.privkey
458 msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM
459 dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
460 else: # do not ask if the context exist
461 decrypt(otrctx)
462
463 other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid 444 other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid
464 self.fixResource(other_jid, cb) 445 otrctx = self.context_manager.getContextForUser(other_jid, start=False)
446 if otrctx is None:
447 def confirm(confirm):
448 if confirm:
449 self.host.displayWidget(chat.Chat, other_jid)
450 self.context_manager.startContext(other_jid).receiveMessage(msg)
451 else:
452 # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True
453 pass
454 key = self.context_manager.account.privkey
455 question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM
456 dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
457 else: # do not ask for user confirmation if the context exist
458 otrctx.receiveMessage(msg)
459
465 return False # interrupt the main process 460 return False # interrupt the main process
466 461
467 def sendMessageTrigger(self, to_jid, msg, msg_type, extra): 462 def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key):
468 def cb(jid_): 463 if mess_type != C.MESS_TYPE_CHAT:
469 otrctx = self.context_manager.getContextForUser(jid_, start=False) 464 return True
470 if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: 465
471 if otrctx.state == otr.context.STATE_ENCRYPTED: 466 otrctx = self.context_manager.getContextForUser(to_jid, start=False)
472 log.debug(u"encrypting message") 467 if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT:
473 otrctx.sendMessage(msg) 468 if otrctx.state == otr.context.STATE_ENCRYPTED:
474 self.host.newMessageCb(self.host.whoami, msg, msg_type, jid_, extra) 469 log.debug(u"encrypting message")
475 else: 470 otrctx.sendMessage(message)
476 feedback = SEND_PLAIN_IN_FINISHED_CONTEXT 471 self.host.newMessageHandler(unicode(self.host.whoami), message, mess_type, unicode(to_jid), extra)
477 dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show()
478 else: 472 else:
479 log.debug(u"sending message unencrypted") 473 feedback = SEND_PLAIN_IN_FINISHED_CONTEXT
480 self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid, msg, '', msg_type, extra) 474 dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show()
481 475 return False # interrupt the main process
482 if msg_type == 'groupchat': 476
483 return True 477 log.debug(u"sending message unencrypted")
484 self.fixResource(to_jid, cb) 478 return True
485 return False # interrupt the main process 479
486 480 def onPresenceUpdate(self, entity, show, priority, statuses, profile):
487 def presenceReceivedTrigger(self, entity, show, priority, statuses):
488 if show == C.PRESENCE_UNAVAILABLE: 481 if show == C.PRESENCE_UNAVAILABLE:
489 self.endSession(entity, finish=True) 482 self.endSession(entity, finish=True)
490 return True 483
491 484 def endSession(self, other_jid, finish=False):
492 def endSession(self, other_jid, profile, finish=False):
493 """Finish or disconnect an OTR session 485 """Finish or disconnect an OTR session
494 486
495 @param other_jid (jid.JID): contact JID 487 @param other_jid (jid.JID): other JID
496 @param finish: if True, finish the session but do not disconnect it 488 @param finish: if True, finish the session but do not disconnect it
497 @return: True if the session has been finished or disconnected, False if there was nothing to do 489 @return: True if the session has been finished or disconnected, False if there was nothing to do
498 """ 490 """
499 def cb(other_jid): 491 # checking for private key existence is not needed, context checking is enough
500 def not_available(): 492 otrctx = self.context_manager.getContextForUser(other_jid, start=False)
501 if not finish: 493 if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT:
502 self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid), C.MESS_TYPE_INFO, self.host.whoami, {}) 494 if not finish:
503 495 self.host.newMessageHandler(unicode(other_jid), END_PLAIN_HAS_NOT.format(jid=other_jid), C.MESS_TYPE_INFO, unicode(self.host.whoami), {})
504 priv_key = self.context_manager.account.privkey 496 return
505 if priv_key is None: 497 if finish:
506 not_available() 498 otrctx.finish()
507 return 499 else:
508 500 otrctx.disconnect()
509 otrctx = self.context_manager.getContextForUser(other_jid, start=False)
510 if otrctx is None:
511 not_available()
512 return
513 if finish:
514 otrctx.finish()
515 else:
516 otrctx.disconnect()
517
518 self.fixResource(other_jid, cb)
519 501
520 # Menu callbacks 502 # Menu callbacks
521 503
522 def _startRefresh(self, menu_data): 504 def _startRefresh(self, menu_data):
523 """Start or refresh an OTR session 505 """Start or refresh an OTR session
527 def query(other_jid): 509 def query(other_jid):
528 otrctx = self.context_manager.getContextForUser(other_jid) 510 otrctx = self.context_manager.getContextForUser(other_jid)
529 if otrctx: 511 if otrctx:
530 otrctx.sendQueryMessage() 512 otrctx.sendQueryMessage()
531 513
532 def cb(jid_): 514 other_jid = menu_data['jid']
533 key = self.context_manager.account.privkey 515 clist = self.host.contact_list
534 if key is None: 516 if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None:
535 def confirm(confirm): 517 dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show()
536 if confirm: 518 return
537 query(jid_) 519
538 msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM 520 key = self.context_manager.account.privkey
539 dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() 521 if key is None:
540 else: # on query reception we ask always, if we initiate we just ask the first time 522 def confirm(confirm):
541 query(jid_) 523 if confirm:
542 524 query(other_jid)
543 try: 525 msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM
544 other_jid = menu_data['jid'] 526 dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show()
545 contact_list = self.host.contact_list 527 else: # on query reception we ask always, if we initiate we just ask the first time
546 if contact_list.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: 528 query(other_jid)
547 dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show()
548 return
549 self.fixResource(other_jid, cb)
550 except KeyError:
551 log.error(_("jid key is not present !"))
552 529
553 def _endSession(self, menu_data): 530 def _endSession(self, menu_data):
554 """End an OTR session 531 """End an OTR session
555 532
556 @param menu_data: %(menu_data)s 533 @param menu_data: %(menu_data)s
557 """ 534 """
558 try: 535 self.endSession(menu_data['jid'])
559 other_jid = menu_data['jid']
560 except KeyError:
561 log.error(_("jid key is not present !"))
562 return None
563 self.endSession(other_jid)
564 536
565 def _authenticate(self, menu_data, profile): 537 def _authenticate(self, menu_data, profile):
566 """Authenticate other user and see our own fingerprint 538 """Authenticate other user and see our own fingerprint
567 539
568 @param menu_data: %(menu_data)s 540 @param menu_data: %(menu_data)s
569 @param profile: %(doc_profile)s 541 @param profile: %(doc_profile)s
570 """ 542 """
571 def not_available(): 543 def not_available():
572 dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() 544 dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show()
573 545
574 priv_key = self.context_manager.account.privkey 546 to_jid = menu_data['jid']
575 if priv_key is None: 547
548 # checking for private key existence is not needed, context checking is enough
549 otrctx = self.context_manager.getContextForUser(to_jid, start=False)
550 if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED:
576 not_available() 551 not_available()
577 return 552 return
578 553 otr_version = otrctx.getUsedVersion()
579 def cb(to_jid): 554 if otr_version == otr.context.OTR_VERSION_2:
580 otrctx = self.context_manager.getContextForUser(to_jid, start=False) 555 otrctx.fingerprintAuthCb()
581 if otrctx is None: 556 elif otr_version == otr.context.OTR_VERSION_3:
582 not_available() 557 otrctx.smpAuthCb('question', None, 'asked')
583 return 558 else:
584 otr_version = otrctx.getUsedVersion() 559 not_available()
585 if otr_version == otr.context.OTR_VERSION_2:
586 otrctx.fingerprintAuthCb()
587 elif otr_version == otr.context.OTR_VERSION_3:
588 otrctx.smpAuthCb('question', None, 'asked')
589 else:
590 not_available()
591
592 try:
593 to_jid = menu_data['jid']
594 self.fixResource(to_jid, cb)
595 except KeyError:
596 log.error(_("jid key is not present !"))
597 return None
598 560
599 def _dropPrivkey(self, menu_data, profile): 561 def _dropPrivkey(self, menu_data, profile):
600 """Drop our private Key 562 """Drop our private Key
601 563
602 @param menu_data: %(menu_data)s 564 @param menu_data: %(menu_data)s
606 if priv_key is None: 568 if priv_key is None:
607 # we have no private key yet 569 # we have no private key yet
608 dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() 570 dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show()
609 return 571 return
610 572
611 def cb(to_jid): 573 def dropKey(confirm):
612 def dropKey(confirm): 574 if confirm:
613 if confirm: 575 # we end all sessions
614 # we end all sessions 576 for context in self.context_manager.contexts.values():
615 for context in self.context_manager.contexts.values(): 577 context.disconnect()
616 context.disconnect() 578 self.context_manager.contexts.clear()
617 self.context_manager.contexts.clear() 579 self.context_manager.account.privkey = None
618 self.context_manager.account.privkey = None 580 dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show()
619 dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() 581
620 582 dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()
621 dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()
622
623 try:
624 to_jid = menu_data['jid']
625 self.fixResource(to_jid, cb)
626 except KeyError:
627 log.error(_("jid key is not present !"))
628 return None