# HG changeset patch # User Goffi # Date 1534005256 -7200 # Node ID ba8f3a4a5ac75507223706dfcaf2a74a0418c412 # Parent 557946bf95457939042a5444b72899ae25d4537e plugin chat: e2e encryption improvments: OTR buttons has been replaced with a more generic one, which use new encryption mechanisms to retrieve all current encryption algorithms (+ a button for plain text). "refresh" and "authentify" buttons for OTR are temporarily removed. Encryption state is checked on chat widget startup, and current encryption method is selected (it will appear with a different background in encryption menu). diff -r 557946bf9545 -r ba8f3a4a5ac7 cagou/plugins/plugin_wid_chat.kv --- a/cagou/plugins/plugin_wid_chat.kv Sat Aug 11 19:48:58 2018 +0200 +++ b/cagou/plugins/plugin_wid_chat.kv Sat Aug 11 18:34:16 2018 +0200 @@ -128,20 +128,26 @@ width: dp(30) on_release: TransferMenu(callback=root.onTransferOK).show(self) -: +: size_hint: None, 1 width: dp(30) color: self.getColor() symbol: self.getSymbol() -: +: + group: 'encryption' size_hint: None, None - size: self.texture_size + width: max(self.texture_size[0], self.parent.minimum_width if self.parent else 0) + height: self.texture_size[1] padding: dp(5), dp(10) + color: 0, 0, 0, 1 + bold: True + background_normal: app.expand('{media}/misc/borders/border_filled_black.png') + background_color: app.c_sec if self.selected else app.c_prim_dark -: +: size_hint_x: None - width: start_btn.width + width: self.container.minimum_width auto_width: False canvas.before: Color: @@ -149,16 +155,16 @@ Rectangle: pos: self.pos size: self.size - OtrButton: - size_hint: 1, None - id: start_btn - text: _(u"Start/Refresh encrypted session") - on_release: root.otr_start() - OtrButton: - size_hint: 1, None - text: _(u"Finish encrypted session") - on_release: root.otr_end() - OtrButton: - size_hint: 1, None - text: _(u"Authenticate destinee") - on_release: root.otr_authenticate() + # EncryptionButton: + # size_hint: 1, None + # id: start_btn + # text: _(u"Start/Refresh encrypted session") + # on_release: root.otr_start() + # EncryptionButton: + # size_hint: 1, None + # text: _(u"Finish encrypted session") + # on_release: root.otr_end() + # EncryptionButton: + # size_hint: 1, None + # text: _(u"Authenticate destinee") + # on_release: root.otr_authenticate() diff -r 557946bf9545 -r ba8f3a4a5ac7 cagou/plugins/plugin_wid_chat.py --- a/cagou/plugins/plugin_wid_chat.py Sat Aug 11 19:48:58 2018 +0200 +++ b/cagou/plugins/plugin_wid_chat.py Sat Aug 11 18:34:16 2018 +0200 @@ -34,8 +34,10 @@ from cagou.core.image import Image from cagou.core.common import SymbolButton, JidButton from kivy.uix.dropdown import DropDown +from kivy.uix.button import Button from kivy.core.window import Window from cagou import G +from functools import partial import mimetypes @@ -54,6 +56,13 @@ OTR_STATE_ENCRYPTED = 'encrypted' OTR_STATE_ENCRYPTION = (OTR_STATE_UNENCRYPTED, OTR_STATE_ENCRYPTED) +SYMBOL_UNENCRYPTED = 'lock-open' +SYMBOL_ENCRYPTED = 'lock' +SYMBOL_ENCRYPTED_TRUSTED = 'lock-filled' +COLOR_UNENCRYPTED = (0.4, 0.4, 0.4, 1) +COLOR_ENCRYPTED = (0.4, 0.4, 0.4, 1) +COLOR_ENCRYPTED_TRUSTED = (0.29,0.87,0.0,1) + class MessAvatar(Image): pass @@ -131,7 +140,7 @@ pass -class EncryptionButton(SymbolButton): +class EncryptionMainButton(SymbolButton): def __init__(self, chat, **kwargs): """ @@ -139,9 +148,21 @@ """ self.chat = chat # for now we do a simple ContextMenu as we have only OTR - self.otr_menu = OtrMenu(chat) - super(EncryptionButton, self).__init__(**kwargs) - self.bind(on_release=self.otr_menu.open) + self.encryption_menu = EncryptionMenu(chat) + super(EncryptionMainButton, self).__init__(**kwargs) + self.bind(on_release=self.encryption_menu.open) + + def selectAlgo(self, name): + """Mark an encryption algorithm as selected. + + This will also deselect all other button + @param name(unicode, None): encryption plugin name + None for plain text + """ + buttons = self.encryption_menu.container.children + buttons[-1].selected = name is None + for button in buttons[:-1]: + button.selected = button.text == name def getColor(self): if self.chat.otr_state_encryption == OTR_STATE_UNENCRYPTED: @@ -160,14 +181,78 @@ return 'lock' -class OtrMenu(DropDown): +class EncryptionButton(Button): + selected = properties.BooleanProperty(False) + + +class EncryptionMenu(DropDown): def __init__(self, chat, **kwargs): """ @param chat(Chat): Chat instance """ self.chat = chat - super(OtrMenu, self).__init__(**kwargs) + super(EncryptionMenu, self).__init__(**kwargs) + btn = EncryptionButton( + text=_(u"unencrypted (plain text)"), + on_release=self.unencrypted, + selected=True, + bold=False, + ) + self.add_widget(btn) + for plugin in G.host.encryption_plugins: + btn = EncryptionButton( + text=plugin[u'name'], + on_release=partial(self.startEncryption, plugin=plugin), + ) + self.add_widget(btn) + log.info("added encryption: {}".format(plugin['name'])) + + def messageEncryptionStopCb(self): + log.info(_(u"Session with {destinee} is now in plain text").format( + destinee = self.chat.target)) + + def messageEncryptionStopEb(self, failure_): + msg = _(u"Error while stopping encryption with {destinee}: {reason}").format( + destinee = self.chat.target, + reason = failure_) + log.warning(msg) + G.host.addNote(_(u"encryption problem"), msg, C.XMLUI_DATA_LVL_ERROR) + + def unencrypted(self, button): + self.dismiss() + G.host.bridge.messageEncryptionStop( + unicode(self.chat.target), + self.chat.profile, + callback=partial(self.messageEncryptionStopCb), + errback=partial(self.messageEncryptionStopEb)) + + def messageEncryptionStartCb(self, plugin): + log.info(_(u"Session with {destinee} is now encrypted with {encr_name}").format( + destinee = self.chat.target, + encr_name = plugin['name'])) + + def messageEncryptionStartEb(self, failure_): + msg = _(u"Session can't be encrypted with {destinee}: {reason}").format( + destinee = self.chat.target, + reason = failure_) + log.warning(msg) + G.host.addNote(_(u"encryption problem"), msg, C.XMLUI_DATA_LVL_ERROR) + + def startEncryption(self, button, plugin): + """Request encryption with given plugin for this session + + @param button(EncryptionButton): button which has been pressed + @param plugin(dict): plugin data + """ + self.dismiss() + G.host.bridge.messageEncryptionStart( + unicode(self.chat.target), + plugin['namespace'], + True, + self.chat.profile, + callback=partial(self.messageEncryptionStartCb, plugin=plugin), + errback=partial(self.messageEncryptionStartEb)) def otr_start(self): self.dismiss() @@ -207,8 +292,10 @@ message_input = properties.ObjectProperty() messages_widget = properties.ObjectProperty() - def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): - quick_chat.QuickChat.__init__(self, host, target, type_, nick, occupants, subject, profiles=profiles) + def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, + subject=None, profiles=None): + quick_chat.QuickChat.__init__( + self, host, target, type_, nick, occupants, subject, profiles=profiles) self.otr_state_encryption = OTR_STATE_UNENCRYPTED self.otr_state_trust = OTR_STATE_UNTRUSTED # completion attributes @@ -218,7 +305,7 @@ self._hi_comp_allowed = True cagou_widget.CagouWidget.__init__(self) if type_ == C.CHAT_ONE2ONE: - self.encryption_btn = EncryptionButton(self) + self.encryption_btn = EncryptionMainButton(self) self.headerInputAddExtra(self.encryption_btn) self.header_input.hint_text = u"{}".format(target) self.host.addListener('progressError', self.onProgressError, profiles) @@ -242,7 +329,9 @@ raise NotImplementedError(u"Multi-profiles is not available yet for chat") if target is None: target = G.host.profiles[profiles[0]].whoami - return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, on_existing_widget=C.WIDGET_RECREATE, profiles=profiles) + return G.host.widgets.getOrCreateWidget(cls, target, on_new_widget=None, + on_existing_widget=C.WIDGET_RECREATE, + profiles=profiles) ## header ## @@ -269,14 +358,16 @@ def discoCb(disco): # TODO: check if plugin XEP-0045 is activated if "conference" in [i[0] for i in disco[1]]: - G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, callback=self._mucJoinCb, errback=self._mucJoinEb) + G.host.bridge.mucJoin(unicode(jid_), "", "", self.profile, + callback=self._mucJoinCb, errback=self._mucJoinEb) else: self.changeWidget(jid_) def discoEb(failure): log.warning(u"Disco failure, ignore this text: {}".format(failure)) - G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, errback=discoEb) + G.host.bridge.discoInfos(jid_.domain, self.profile, callback=discoCb, + errback=discoEb) def onHeaderInputCompleted(self, input_wid, completed_text): self._hi_comp_allowed = False @@ -369,7 +460,8 @@ For group chat, note will be added on mention, with a desktop notification if window has not focus. """ - visible_clones = [w for w in G.host.getVisibleList(self.__class__) if w.target == self.target] + visible_clones = [w for w in G.host.getVisibleList(self.__class__) + if w.target == self.target] if len(visible_clones) > 1 and visible_clones.index(self) > 0: # to avoid multiple notifications in case of multiple cloned widgets # we only handle first clone @@ -404,7 +496,8 @@ G.host.messageSend( self.target, {'': input_widget.text}, # TODO: handle language - mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat + mess_type = (C.MESS_TYPE_GROUPCHAT + if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), # TODO: put this in QuickChat profile_key=self.profile ) input_widget.text = '' @@ -444,7 +537,8 @@ G.host.messageSend( self.target, {'': metadata['url']}, - mess_type = C.MESS_TYPE_GROUPCHAT if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, + mess_type = (C.MESS_TYPE_GROUPCHAT + if self.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT), extra = extra, profile_key=profile ) @@ -466,21 +560,35 @@ "", {"ignore_tls_errors": C.BOOL_TRUE}, # FIXME: should not be the default self.profile, - callback = lambda progress_data: self.fileTransferCb(progress_data, cleaning_cb) + callback = lambda progress_data: self.fileTransferCb( + progress_data, cleaning_cb) ) elif transfer_type == C.TRANSFER_SEND: if self.type == C.CHAT_GROUP: log.warning(u"P2P transfer is not possible for group chat") - # TODO: show an error dialog to user, or better hide the send button for MUC + # TODO: show an error dialog to user, or better hide the send button for + # MUC else: jid_ = self.target if not jid_.resource: jid_ = G.host.contact_lists[self.profile].getFullJid(jid_) - G.host.bridge.fileSend(unicode(jid_), file_path, "", "", profile=self.profile) + G.host.bridge.fileSend(unicode(jid_), file_path, "", "", + profile=self.profile) # TODO: notification of sending/failing else: raise log.error(u"transfer of type {} are not handled".format(transfer_type)) + def messageEncryptionStarted(self, plugin_data): + quick_chat.QuickChat.messageEncryptionStarted(self, plugin_data) + self.encryption_btn.symbol = SYMBOL_ENCRYPTED + self.encryption_btn.color = COLOR_ENCRYPTED + self.encryption_btn.selectAlgo(plugin_data[u'name']) + + def messageEncryptionStopped(self, plugin_data): + quick_chat.QuickChat.messageEncryptionStopped(self, plugin_data) + self.encryption_btn.symbol = SYMBOL_UNENCRYPTED + self.encryption_btn.color = COLOR_UNENCRYPTED + self.encryption_btn.selectAlgo(None) def _mucJoinCb(self, joined_data): joined, room_jid_s, occupants, user_nick, subject, profile = joined_data @@ -511,7 +619,8 @@ def onDelete(self, force=False): if force==True: return self._onDelete() - if len(list(G.host.widgets.getWidgets(self.__class__, self.target, profiles=self.profiles))) > 1: + if len(list(G.host.widgets.getWidgets( + self.__class__, self.target, profiles=self.profiles))) > 1: # we don't keep duplicate widgets return self._onDelete() return False