Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- a/src/browser/sat_browser/plugin_sec_otr.py Tue Mar 03 06:51:13 2015 +0100 +++ b/src/browser/sat_browser/plugin_sec_otr.py Tue Mar 03 07:21:50 2015 +0100 @@ -23,10 +23,12 @@ The text messages to display are mostly taken from the Pidgin OTR plugin (GPL 2.0, see http://otr.cypherpunks.ca). """ +from sat.core.log import getLogger +log = getLogger(__name__) + from sat.core.i18n import _, D_ -from sat.core.log import getLogger from sat.core import exceptions -log = getLogger(__name__) +from sat.tools.misc import TriggerManager from constants import Const as C from sat_frontends.tools import jid @@ -39,7 +41,6 @@ PRIVATE_KEY = "PRIVATE KEY" MAIN_MENU = D_('OTR encryption') DIALOG_EOL = "<br />" -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>") AUTH_TRUSTED = D_("Verified") AUTH_UNTRUSTED = D_("Unverified") @@ -72,7 +73,8 @@ AKE_ENCRYPTED = D_(" conversation with {jid} started. Your client is not logging this conversation.") AKE_NOT_ENCRYPTED = D_("ERROR: successfully ake'd with {jid} but the conversation is not encrypted!") END_ENCRYPTED = D_("ERROR: the OTR session ended but the context is still supposedly encrypted!") -END_PLAIN = D_("Your conversation with {jid} is no more or hasn't been encrypted.") +END_PLAIN_NO_MORE = D_("Your conversation with {jid} is no more encrypted.") +END_PLAIN_HAS_NOT = D_("Your conversation with {jid} hasn't been encrypted.") END_FINISHED = D_("{jid} has ended his or her private conversation with you; you should do the same.") KEY_TITLE = D_('Private key') @@ -92,15 +94,13 @@ ACTION_NA_TITLE = D_("Impossible action") ACTION_NA = D_("Your correspondent must be connected to start an OTR conversation with him.") -RESOURCE_ISSUE_TITLE = D_("Security issue") -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}.") DEFAULT_POLICY_FLAGS = { 'ALLOW_V2': True, 'ALLOW_V3': True, 'REQUIRE_ENCRYPTION': False, - 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTrigger before turning this to True - 'WHITESPACE_START_AKE': False, # FIXME: we need to complete messageReceivedTrigger before turning this to True + 'SEND_WHITESPACE_TAG': False, # FIXME: we need to complete sendMessageTg before turning this to True + 'WHITESPACE_START_AKE': False, # FIXME: we need to complete newMessageTg before turning this to True } # list a couple of texts or htmls (untrusted, trusted) for each state @@ -120,6 +120,13 @@ } +unicode = str # FIXME: pyjamas workaround + + +class NotConnectedEntity(Exception): + pass + + class Context(otr.context.Context): def __init__(self, host, account, other_jid): @@ -158,13 +165,13 @@ if not encrypted: if self.state == otr.context.STATE_ENCRYPTED: log.warning(u"Received unencrypted message in an encrypted context (from %(jid)s)" % {'jid': self.peer}) - self.host.newMessageCb(self.peer, RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, self.host.whoami, {}) - self.host.newMessageCb(self.peer, msg, "chat", self.host.whoami, {}) + self.host.newMessageHandler(unicode(self.peer), RECEIVE_PLAIN_IN_ENCRYPTED_CONTEXT, C.MESS_TYPE_INFO, unicode(self.host.whoami), {}) + self.host.newMessageHandler(unicode(self.peer), msg, C.MESS_TYPE_CHAT, unicode(self.host.whoami), {}) def sendMessageCb(self, msg, meta=None): assert isinstance(self.peer, jid.JID) log.debug("message to send%s: %s" % ((' (attached meta data: %s)' % meta) if meta else '', msg)) - self.host.bridge.call('sendMessage', (None, self.host.sendError), self.peer, msg, '', 'chat', {'send_only': 'true'}) + self.host.bridge.call('sendMessage', (None, self.host.sendError), unicode(self.peer), msg, '', C.MESS_TYPE_CHAT, {'send_only': 'true'}) def messageErrorCb(self, error): log.error('error occured: %s' % error) @@ -186,13 +193,13 @@ elif status == otr.context.STATUS_END_OTR: if msg_state == otr.context.STATE_PLAINTEXT: - feedback = END_PLAIN + feedback = END_PLAIN_NO_MORE elif msg_state == otr.context.STATE_ENCRYPTED: log.error(END_ENCRYPTED) elif msg_state == otr.context.STATE_FINISHED: feedback = END_FINISHED - 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)}) + 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)}) def setCurrentTrust(self, new_trust='', act='asked', type_='trust'): log.debug("setCurrentTrust: trust={trust}, act={act}, type={type}".format(type=type_, trust=new_trust, act=act)) @@ -215,7 +222,7 @@ otr.context.Context.setCurrentTrust(self, new_trust) if old_trust != new_trust: feedback = AUTH_STATUS.format(state=(AUTH_TRUSTED if new_trust else AUTH_UNTRUSTED).lower()) - self.host.newMessageCb(self.peer, feedback, C.MESS_TYPE_INFO, self.host.whoami, {'header_info': OTR.getInfoText(self.state, new_trust)}) + self.host.newMessageHandler(unicode(self.peer), feedback, C.MESS_TYPE_INFO, unicode(self.host.whoami), {'header_info': OTR.getInfoText(self.state, new_trust)}) def fingerprintAuthCb(self): """OTR v2 authentication using manual fingerprint comparison""" @@ -327,7 +334,7 @@ self.contexts = {} def startContext(self, other_jid): - assert isinstance(other_jid, jid.JID) + assert isinstance(other_jid, jid.JID) # never start an OTR session with a bare JID # FIXME upstream: apparently pyjamas doesn't implement setdefault well, it ignores JID.__hash__ redefinition #context = self.contexts.setdefault(other_jid, Context(self.host, self.account, other_jid)) if other_jid not in self.contexts: @@ -341,20 +348,30 @@ @param start (bool): start non-existing context if True @return: Context """ + try: + other_jid = self.fixResource(other_jid) + except NotConnectedEntity: + log.debug(u"getContextForUser [%s]: not connected!" % other_jid) + return None log.debug(u"getContextForUser [%s]" % other_jid) - if not other_jid.resource: - log.error("getContextForUser called with a bare jid") - running_sessions = [jid_.bare for jid_ in self.contexts.keys() if self.contexts[jid_].state == otr.context.STATE_ENCRYPTED] - if start or (other_jid in running_sessions): - users_ml = DIALOG_USERS_ML.format(subject=D_("OTR issue in Libervia: getContextForUser called with a bare jid in an encrypted context")) - text = RESOURCE_ISSUE.format(eol=DIALOG_EOL, jid=other_jid, users_ml=users_ml) - dialog.InfoDialog(RESOURCE_ISSUE_TITLE, text, AddStyleName="maxWidthLimit").show() - return None # never start an OTR session with a bare JID if start: return self.startContext(other_jid) else: return self.contexts.get(other_jid, None) + def fixResource(self, other_jid): + """Return the full JID in case the resource of the given JID is missing. + + @param other_jid (jid.JID): JID to check + @return jid.JID + """ + if other_jid.resource: + return other_jid + clist = self.host.contact_list + if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: + raise NotConnectedEntity + return clist.getFullJid(other_jid) + class OTR(object): @@ -362,8 +379,9 @@ log.info(_(u"OTR plugin initialization")) self.host = host self.context_manager = None - self.last_resources = {} self.host.bridge._registerMethods(["skipOTR"]) + self.host.trigger.add("newMessage", self.newMessageTg, priority=TriggerManager.MAX_PRIORITY) + self.host.trigger.add("sendMessage", self.sendMessageTg, priority=TriggerManager.MAX_PRIORITY) @classmethod def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''): @@ -377,20 +395,16 @@ state = OTR_MSG_STATES.keys()[0] return OTR_MSG_STATES[state][1 if trust else 0] - def infoTextCallback(self, other_jid, cb): - """Get the current info text for a conversation and run a callback. + def getInfoTextForUser(self, other_jid): + """Get the current info text for a conversation. @param other_jid (jid.JID): JID of the correspondant - @paam cb (callable): method to be called with the computed info text """ - def gotResource(other_jid): - otrctx = self.context_manager.getContextForUser(other_jid, start=False) - if otrctx is None: - cb(OTR.getInfoText()) - else: - cb(OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust())) - - self.fixResource(other_jid, gotResource) + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None: + return OTR.getInfoText() + else: + return OTR.getInfoText(otrctx.state, otrctx.getCurrentTrust()) def inhibitMenus(self): """Tell the caller which dynamic menus should be inhibited""" @@ -410,112 +424,80 @@ # decrypt it, parse it with otr.crypt.PK.parsePrivateKey(privkey) and # assign it to self.context_manager.account.privkey + # 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) + self.presenceListener = self.onPresenceUpdate + self.host.addListener('presence', self.presenceListener, [C.PROF_KEY_NONE]) + def profileDisconnected(self): for context in self.context_manager.contexts.values(): context.disconnect() + self.host.removeListener('presence', self.presenceListener) - def fixResource(self, jid_, cb): - # FIXME: it's dirty, but libervia doesn't manage resources correctly now, refactoring is planed - if jid_.resource: - self.last_resources[jid_.bare] = jid_.resource - cb(jid_) - elif jid_.bare in self.last_resources: - # FIXME: to be removed: must use new resource system - # jid_.setResource(self.last_resources[jid_.bare]) - cb(jid_) - else: - pass # FIXME: to be removed: must use new resource system - # def gotResource(resource): - # if resource: - # jid_.setResource(resource) - # self.last_resources[jid_.bare] = jid_.resource - # cb(jid_) - # - # self.host.bridge.call('getLastResource', gotResource, jid_) - - def messageReceivedTrigger(self, from_jid, msg, msg_type, to_jid, extra): - if msg_type == C.MESS_TYPE_INFO: + def newMessageTg(self, from_jid, msg, msg_type, to_jid, extra, profile): + if msg_type != C.MESS_TYPE_CHAT: return True tag = otr.proto.checkForOTR(msg) if tag is None or (tag == otr.context.WHITESPACE_TAG and not DEFAULT_POLICY_FLAGS['WHITESPACE_START_AKE']): return True - def decrypt(context): - context.receiveMessage(msg) + other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None: + def confirm(confirm): + if confirm: + self.host.displayWidget(chat.Chat, other_jid) + self.context_manager.startContext(other_jid).receiveMessage(msg) + else: + # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True + pass + key = self.context_manager.account.privkey + question = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM + dialog.ConfirmDialog(confirm, question.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + else: # do not ask for user confirmation if the context exist + otrctx.receiveMessage(msg) - def cb(jid_): - otrctx = self.context_manager.getContextForUser(jid_, start=False) - if otrctx is None: - def confirm(confirm): - if confirm: - self.host.displayWidget(chat.Chat, jid_) - decrypt(self.context_manager.startContext(jid_)) - else: - # FIXME: plain text messages with whitespaces would be lost here when WHITESPACE_START_AKE is True - pass - key = self.context_manager.account.privkey - msg = QUERY_RECEIVED + QUERY_SLOWDOWN + (QUERY_KEY if key else QUERY_NO_KEY) + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() - else: # do not ask if the context exist - decrypt(otrctx) - - other_jid = to_jid if from_jid.bare == self.host.whoami.bare else from_jid - self.fixResource(other_jid, cb) return False # interrupt the main process - def sendMessageTrigger(self, to_jid, msg, msg_type, extra): - def cb(jid_): - otrctx = self.context_manager.getContextForUser(jid_, start=False) - if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: - if otrctx.state == otr.context.STATE_ENCRYPTED: - log.debug(u"encrypting message") - otrctx.sendMessage(msg) - self.host.newMessageCb(self.host.whoami, msg, msg_type, jid_, extra) - else: - feedback = SEND_PLAIN_IN_FINISHED_CONTEXT - dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() + def sendMessageTg(self, to_jid, message, subject, mess_type, extra, callback, errback, profile_key): + if mess_type != C.MESS_TYPE_CHAT: + return True + + otrctx = self.context_manager.getContextForUser(to_jid, start=False) + if otrctx is not None and otrctx.state != otr.context.STATE_PLAINTEXT: + if otrctx.state == otr.context.STATE_ENCRYPTED: + log.debug(u"encrypting message") + otrctx.sendMessage(message) + self.host.newMessageHandler(unicode(self.host.whoami), message, mess_type, unicode(to_jid), extra) else: - log.debug(u"sending message unencrypted") - self.host.bridge.call('sendMessage', (None, self.host.sendError), to_jid, msg, '', msg_type, extra) + feedback = SEND_PLAIN_IN_FINISHED_CONTEXT + dialog.InfoDialog(FINISHED_CONTEXT_TITLE.format(jid=to_jid), feedback, AddStyleName="maxWidthLimit").show() + return False # interrupt the main process - if msg_type == 'groupchat': - return True - self.fixResource(to_jid, cb) - return False # interrupt the main process + log.debug(u"sending message unencrypted") + return True - def presenceReceivedTrigger(self, entity, show, priority, statuses): + def onPresenceUpdate(self, entity, show, priority, statuses, profile): if show == C.PRESENCE_UNAVAILABLE: self.endSession(entity, finish=True) - return True - def endSession(self, other_jid, profile, finish=False): + def endSession(self, other_jid, finish=False): """Finish or disconnect an OTR session - @param other_jid (jid.JID): contact JID + @param other_jid (jid.JID): other JID @param finish: if True, finish the session but do not disconnect it @return: True if the session has been finished or disconnected, False if there was nothing to do """ - def cb(other_jid): - def not_available(): - if not finish: - self.host.newMessageCb(other_jid, END_PLAIN.format(jid=other_jid), C.MESS_TYPE_INFO, self.host.whoami, {}) - - priv_key = self.context_manager.account.privkey - if priv_key is None: - not_available() - return - - otrctx = self.context_manager.getContextForUser(other_jid, start=False) - if otrctx is None: - not_available() - return - if finish: - otrctx.finish() - else: - otrctx.disconnect() - - self.fixResource(other_jid, cb) + # checking for private key existence is not needed, context checking is enough + otrctx = self.context_manager.getContextForUser(other_jid, start=False) + if otrctx is None or otrctx.state == otr.context.STATE_PLAINTEXT: + if not finish: + self.host.newMessageHandler(unicode(other_jid), END_PLAIN_HAS_NOT.format(jid=other_jid), C.MESS_TYPE_INFO, unicode(self.host.whoami), {}) + return + if finish: + otrctx.finish() + else: + otrctx.disconnect() # Menu callbacks @@ -529,38 +511,28 @@ if otrctx: otrctx.sendQueryMessage() - def cb(jid_): - key = self.context_manager.account.privkey - if key is None: - def confirm(confirm): - if confirm: - query(jid_) - msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM - dialog.ConfirmDialog(confirm, msg.format(jid=jid_, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() - else: # on query reception we ask always, if we initiate we just ask the first time - query(jid_) + other_jid = menu_data['jid'] + clist = self.host.contact_list + if clist.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: + dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() + return - try: - other_jid = menu_data['jid'] - contact_list = self.host.contact_list - if contact_list.getCache(other_jid.bare, C.PRESENCE_SHOW) is None: - dialog.InfoDialog(ACTION_NA_TITLE, ACTION_NA, AddStyleName="maxWidthLimit").show() - return - self.fixResource(other_jid, cb) - except KeyError: - log.error(_("jid key is not present !")) + key = self.context_manager.account.privkey + if key is None: + def confirm(confirm): + if confirm: + query(other_jid) + msg = QUERY_SEND + QUERY_SLOWDOWN + QUERY_NO_KEY + QUERY_CONFIRM + dialog.ConfirmDialog(confirm, msg.format(jid=other_jid, eol=DIALOG_EOL), QUERY_TITLE, AddStyleName="maxWidthLimit").show() + else: # on query reception we ask always, if we initiate we just ask the first time + query(other_jid) def _endSession(self, menu_data): """End an OTR session @param menu_data: %(menu_data)s """ - try: - other_jid = menu_data['jid'] - except KeyError: - log.error(_("jid key is not present !")) - return None - self.endSession(other_jid) + self.endSession(menu_data['jid']) def _authenticate(self, menu_data, profile): """Authenticate other user and see our own fingerprint @@ -571,30 +543,20 @@ def not_available(): dialog.InfoDialog(AUTH_TRUST_NA_TITLE, AUTH_TRUST_NA_TXT, AddStyleName="maxWidthLimit").show() - priv_key = self.context_manager.account.privkey - if priv_key is None: + to_jid = menu_data['jid'] + + # checking for private key existence is not needed, context checking is enough + otrctx = self.context_manager.getContextForUser(to_jid, start=False) + if otrctx is None or otrctx.state != otr.context.STATE_ENCRYPTED: not_available() return - - def cb(to_jid): - otrctx = self.context_manager.getContextForUser(to_jid, start=False) - if otrctx is None: - not_available() - return - otr_version = otrctx.getUsedVersion() - if otr_version == otr.context.OTR_VERSION_2: - otrctx.fingerprintAuthCb() - elif otr_version == otr.context.OTR_VERSION_3: - otrctx.smpAuthCb('question', None, 'asked') - else: - not_available() - - try: - to_jid = menu_data['jid'] - self.fixResource(to_jid, cb) - except KeyError: - log.error(_("jid key is not present !")) - return None + otr_version = otrctx.getUsedVersion() + if otr_version == otr.context.OTR_VERSION_2: + otrctx.fingerprintAuthCb() + elif otr_version == otr.context.OTR_VERSION_3: + otrctx.smpAuthCb('question', None, 'asked') + else: + not_available() def _dropPrivkey(self, menu_data, profile): """Drop our private Key @@ -608,21 +570,13 @@ dialog.InfoDialog(KEY_NA_TITLE, KEY_NA_TXT, AddStyleName="maxWidthLimit").show() return - def cb(to_jid): - def dropKey(confirm): - if confirm: - # we end all sessions - for context in self.context_manager.contexts.values(): - context.disconnect() - self.context_manager.contexts.clear() - self.context_manager.account.privkey = None - dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() + def dropKey(confirm): + if confirm: + # we end all sessions + for context in self.context_manager.contexts.values(): + context.disconnect() + self.context_manager.contexts.clear() + self.context_manager.account.privkey = None + dialog.InfoDialog(KEY_TITLE, KEY_DROPPED_TXT, AddStyleName="maxWidthLimit").show() - dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show() - - try: - to_jid = menu_data['jid'] - self.fixResource(to_jid, cb) - except KeyError: - log.error(_("jid key is not present !")) - return None + dialog.ConfirmDialog(dropKey, KEY_DROP_TXT.format(eol=DIALOG_EOL), KEY_DROP_TITLE, AddStyleName="maxWidthLimit").show()