Update the OTR plugin

make it more restrictive and give some hints
This commit is contained in:
mathieui 2018-08-12 12:59:40 +02:00
parent 7d3403d288
commit 31fc9d00f5
No known key found for this signature in database
GPG key ID: C59F84CEEFD616E3
2 changed files with 69 additions and 59 deletions

View file

@ -44,11 +44,26 @@ Install the python module:
You can also use pip in a virtualenv (built-in as pyvenv_ with python since 3.3) You can also use pip in a virtualenv (built-in as pyvenv_ with python since 3.3)
with the requirements.txt at the root of the poezio directory. with the requirements.txt at the root of the poezio directory.
Important details
-----------------
The OTR session is considered for a full JID (e.g. toto@example/**client1**),
but the trust is set with a bare JID (e.g. toto@example). This is important
in the case of Private Chats (in a chatroom), since you cannot always get the
real JID of your contact (or check if the same nick is used by different people).
.. note::
This also means that you cannot have an OTR session in the "common"
conversation tab, which is not locked to a specific JID. After activating
the plugin, you need to open a session with a full JID to be able to use
OTR.
Usage Usage
----- -----
Command added to Conversation Tabs and Private Tabs: Command added to Static Conversation Tabs (opened with ``/message foo@bar/baz`` or
by expanding a contact in the roster) and Private Tabs:
.. glossary:: .. glossary::
@ -161,14 +176,6 @@ Configuration
The :term:`require_encryption`, :term:`decode_xhtml`, :term:`decode_entities` The :term:`require_encryption`, :term:`decode_xhtml`, :term:`decode_entities`
and :term:`log` configuration parameters are tab-specific. and :term:`log` configuration parameters are tab-specific.
Important details
-----------------
The OTR session is considered for a full JID (e.g. toto@example/**client1**),
but the trust is set with a bare JID (e.g. toto@example). This is important
in the case of Private Chats (in a chatroom), since you cannot always get the
real JID of your contact (or check if the same nick is used by different people).
.. _Off The Record messaging: http://wiki.xmpp.org/web/OTR .. _Off The Record messaging: http://wiki.xmpp.org/web/OTR
.. _pyvenv: https://docs.python.org/3/using/scripts.html#pyvenv-creating-virtual-environments .. _pyvenv: https://docs.python.org/3/using/scripts.html#pyvenv-creating-virtual-environments
@ -188,12 +195,13 @@ from potr.context import NotEncryptedError, UnencryptedMessage, ErrorReceived, N
STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account, crypt STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account, crypt
from poezio import common from poezio import common
from poezio import xhtml
from poezio import xdg from poezio import xdg
from poezio import xhtml
from poezio.common import safeJID from poezio.common import safeJID
from poezio.config import config from poezio.config import config
from poezio.plugin import BasePlugin from poezio.plugin import BasePlugin
from poezio.tabs import ConversationTab, DynamicConversationTab, PrivateTab from poezio.roster import roster
from poezio.tabs import StaticConversationTab, PrivateTab
from poezio.theming import get_theme, dump_tuple from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser from poezio.decorators import command_args_parser
from poezio.core.structs import Completion from poezio.core.structs import Completion
@ -234,6 +242,15 @@ MESSAGE_NOT_SENT = _('%(info)sYour message to %(jid_c)s%(jid)s%(info)s was'
'encrypted session.\nWait until it is established or ' 'encrypted session.\nWait until it is established or '
'change your configuration.') 'change your configuration.')
INCOMPATIBLE_TAB = _('%(info)sYour message to %(jid_c)s%(jid)s%(info)s was'
' not sent because your configuration requires an '
'encrypted session and the current tab is a bare-jid '
'one, with which you cannot open or use an OTR session.'
' You need to open a fulljid tab with /message if you '
'want to use OTR.%(help)s')
TAB_HELP_RESOURCE = _('\nChoose the relevant one among the following:%s')
OTR_REQUEST = _('%(info)sOTR request to %(jid_c)s%(jid)s%(info)s sent.') OTR_REQUEST = _('%(info)sOTR request to %(jid_c)s%(jid)s%(info)s sent.')
OTR_OWN_FPR = _('%(info)sYour OTR key fingerprint is ' OTR_OWN_FPR = _('%(info)sYour OTR key fingerprint is '
@ -304,6 +321,7 @@ TRUST_REMOVED = _('%(info)sYou removed %(jid_c)s%(bare_jid)s%(info)s with '
KEY_DROPPED = _('%(info)sPrivate key dropped.') KEY_DROPPED = _('%(info)sPrivate key dropped.')
def hl(tab): def hl(tab):
""" """
Make a tab beep and change its status. Make a tab beep and change its status.
@ -363,10 +381,7 @@ class PoezioContext(Context):
tab = self.core.tabs.by_name(self.peer) tab = self.core.tabs.by_name(self.peer)
if not tab: if not tab:
tab = self.core.tabs.by_name(safeJID(self.peer).bare, tab = None
DynamicConversationTab)
if tab and not tab.locked_resource == safeJID(self.peer).resource:
tab = None
if self.state == STATE_ENCRYPTED: if self.state == STATE_ENCRYPTED:
if newstate == STATE_ENCRYPTED and tab: if newstate == STATE_ENCRYPTED and tab:
log.debug('OTR conversation with %s refreshed', self.peer) log.debug('OTR conversation with %s refreshed', self.peer)
@ -489,7 +504,7 @@ class Plugin(BasePlugin):
self.api.add_event_handler('conversation_say_after', self.on_conversation_say) self.api.add_event_handler('conversation_say_after', self.on_conversation_say)
self.api.add_event_handler('private_say_after', self.on_conversation_say) self.api.add_event_handler('private_say_after', self.on_conversation_say)
ConversationTab.add_information_element('otr', self.display_encryption_status) StaticConversationTab.add_information_element('otr', self.display_encryption_status)
PrivateTab.add_information_element('otr', self.display_encryption_status) PrivateTab.add_information_element('otr', self.display_encryption_status)
self.core.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:otr:0') self.core.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:otr:0')
@ -514,14 +529,14 @@ class Plugin(BasePlugin):
'ask: Start a verification, with a question or not\n' 'ask: Start a verification, with a question or not\n'
'answer: Finish a verification\n') 'answer: Finish a verification\n')
self.api.add_tab_command(ConversationTab, 'otrsmp', self.command_smp, self.api.add_tab_command(StaticConversationTab, 'otrsmp', self.command_smp,
help=smp_desc, usage=smp_usage, short=smp_short, help=smp_desc, usage=smp_usage, short=smp_short,
completion=self.completion_smp) completion=self.completion_smp)
self.api.add_tab_command(PrivateTab, 'otrsmp', self.command_smp, self.api.add_tab_command(PrivateTab, 'otrsmp', self.command_smp,
help=smp_desc, usage=smp_usage, short=smp_short, help=smp_desc, usage=smp_usage, short=smp_short,
completion=self.completion_smp) completion=self.completion_smp)
self.api.add_tab_command(ConversationTab, 'otr', self.command_otr, self.api.add_tab_command(StaticConversationTab, 'otr', self.command_otr,
help=desc, usage=usage, short=shortdesc, help=desc, usage=usage, short=shortdesc,
completion=self.completion_otr) completion=self.completion_otr)
self.api.add_tab_command(PrivateTab, 'otr', self.command_otr, self.api.add_tab_command(PrivateTab, 'otr', self.command_otr,
@ -534,7 +549,7 @@ class Plugin(BasePlugin):
self.core.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:otr:0') self.core.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:otr:0')
ConversationTab.remove_information_element('otr') StaticConversationTab.remove_information_element('otr')
PrivateTab.remove_information_element('otr') PrivateTab.remove_information_element('otr')
def get_context(self, jid): def get_context(self, jid):
@ -734,10 +749,6 @@ class Plugin(BasePlugin):
def find_encrypted_context_with_matching(self, bare_jid): def find_encrypted_context_with_matching(self, bare_jid):
""" """
Find an OTR session from a bare JID. Find an OTR session from a bare JID.
Useful when a dynamic tab unlocks, which would lead to sending
unencrypted messages until it locks again, if we didnt fallback
with this.
""" """
for ctx in self.contexts: for ctx in self.contexts:
if safeJID(ctx).bare == bare_jid and self.contexts[ctx].state == STATE_ENCRYPTED: if safeJID(ctx).bare == bare_jid and self.contexts[ctx].state == STATE_ENCRYPTED:
@ -748,13 +759,8 @@ class Plugin(BasePlugin):
""" """
On message sent On message sent
""" """
if isinstance(tab, DynamicConversationTab) and tab.locked_resource: name = tab.name
jid = safeJID(tab.name) jid = safeJID(tab.name)
jid.resource = tab.locked_resource
name = jid.full
else:
name = tab.name
jid = safeJID(tab.name)
format_dict = { format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID), 'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
@ -762,17 +768,13 @@ class Plugin(BasePlugin):
'jid': name, 'jid': name,
} }
ctx = None ctx = self.find_encrypted_context_with_matching(jid)
default_ctx = self.get_context(name) default_ctx = self.get_context(name)
if isinstance(tab, DynamicConversationTab) and not tab.locked_resource:
log.debug('Unlocked tab %s found, falling back to the first encrypted chat we find.', name)
ctx = self.find_encrypted_context_with_matching(jid.bare)
if ctx is None: if ctx is None:
ctx = default_ctx ctx = default_ctx
if ctx and ctx.state == STATE_ENCRYPTED: if is_relevant(tab) and ctx and ctx.state == STATE_ENCRYPTED:
ctx.sendMessage(0, msg['body'].encode('utf-8')) ctx.sendMessage(0, msg['body'].encode('utf-8'))
if not tab.send_chat_state('active'): if not tab.send_chat_state('active'):
tab.send_chat_state('inactive', always_send=True) tab.send_chat_state('inactive', always_send=True)
@ -787,12 +789,28 @@ class Plugin(BasePlugin):
del msg['body'] del msg['body']
del msg['replace'] del msg['replace']
del msg['html'] del msg['html']
elif ctx and ctx.getPolicy('REQUIRE_ENCRYPTION'): elif is_relevant(tab) and ctx and ctx.getPolicy('REQUIRE_ENCRYPTION'):
tab.add_message(MESSAGE_NOT_SENT % format_dict, typ=0) warning_msg = MESSAGE_NOT_SENT % format_dict
tab.add_message(warning_msg, typ=0)
del msg['body'] del msg['body']
del msg['replace'] del msg['replace']
del msg['html'] del msg['html']
self.otr_start(tab, name, format_dict) self.otr_start(tab, name, format_dict)
elif not is_relevant(tab) and ctx and (
ctx.state == STATE_ENCRYPTED or ctx.getPolicy('REQUIRE_ENCRYPTION')):
contact = roster[tab.name]
res = []
if contact:
res = [resource.jid for resource in contact.resources]
help_msg = ''
if res:
help_msg = TAB_HELP_RESOURCE % ''.join(('\n - /message %s' % jid) for jid in res)
format_dict['help'] = help_msg
warning_msg = INCOMPATIBLE_TAB % format_dict
tab.add_message(warning_msg, typ=0)
del msg['body']
del msg['replace']
del msg['html']
def display_encryption_status(self, jid): def display_encryption_status(self, jid):
""" """
@ -818,10 +836,6 @@ class Plugin(BasePlugin):
action = args.pop(0) action = args.pop(0)
tab = self.api.current_tab() tab = self.api.current_tab()
name = tab.name name = tab.name
if isinstance(tab, DynamicConversationTab) and tab.locked_resource:
name = safeJID(tab.name)
name.resource = tab.locked_resource
name = name.full
format_dict = { format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID), 'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
@ -833,11 +847,6 @@ class Plugin(BasePlugin):
if action == 'end': # close the session if action == 'end': # close the session
context = self.get_context(name) context = self.get_context(name)
context.disconnect() context.disconnect()
if isinstance(tab, DynamicConversationTab):
ctx = self.find_encrypted_context_with_matching(safeJID(name).bare)
while ctx is not None:
ctx.disconnect()
ctx = self.find_encrypted_context_with_matching(safeJID(name).bare)
elif action == 'start' or action == 'refresh': elif action == 'start' or action == 'refresh':
self.otr_start(tab, name, format_dict) self.otr_start(tab, name, format_dict)
elif action == 'ourfpr': elif action == 'ourfpr':
@ -891,13 +900,7 @@ class Plugin(BasePlugin):
secs = self.config.get('timeout', 3) secs = self.config.get('timeout', 3)
def notify_otr_timeout(): def notify_otr_timeout():
tab_name = tab.name tab_name = tab.name
otr = self.get_context(tab_name) otr = self.find_encrypted_context_with_matching(tab_name)
if isinstance(tab, DynamicConversationTab):
if tab.locked_resource:
tab_name = safeJID(tab.name)
tab_name.resource = tab.locked_resource
tab_name = tab_name.full
otr = self.get_context(tab_name)
if otr.state != STATE_ENCRYPTED: if otr.state != STATE_ENCRYPTED:
format_dict['secs'] = secs format_dict['secs'] = secs
text = OTR_NOT_ENABLED % format_dict text = OTR_NOT_ENABLED % format_dict
@ -938,11 +941,6 @@ class Plugin(BasePlugin):
tab = self.api.current_tab() tab = self.api.current_tab()
name = tab.name name = tab.name
if isinstance(tab, DynamicConversationTab) and tab.locked_resource:
name = safeJID(tab.name)
name.resource = tab.locked_resource
name = name.full
format_dict = { format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID), 'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT), 'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
@ -983,3 +981,7 @@ def get_tlv(tlvs, cls):
for tlv in tlvs: for tlv in tlvs:
if isinstance(tlv, cls): if isinstance(tlv, cls):
return tlv return tlv
def is_relevant(tab):
"""Check if a tab should be concerned with OTR"""
return isinstance(tab, (StaticConversationTab, PrivateTab))

View file

@ -409,6 +409,8 @@ class DynamicConversationTab(ConversationTab):
bad idea so it has been removed. bad idea so it has been removed.
Only one DynamicConversationTab can be opened for a given jid. Only one DynamicConversationTab can be opened for a given jid.
""" """
plugin_commands = {}
plugin_keys = {}
def __init__(self, core, jid, resource=None): def __init__(self, core, jid, resource=None):
self.locked_resource = None self.locked_resource = None
@ -418,6 +420,8 @@ class DynamicConversationTab(ConversationTab):
self.register_command( self.register_command(
'unlock', self.unlock_command, shortdesc='Deprecated, do nothing.') 'unlock', self.unlock_command, shortdesc='Deprecated, do nothing.')
self.resize() self.resize()
self.update_commands()
self.update_keys()
def get_info_header(self): def get_info_header(self):
return self.info_header return self.info_header
@ -475,12 +479,16 @@ class StaticConversationTab(ConversationTab):
A conversation tab associated with one Full JID. It cannot be locked to A conversation tab associated with one Full JID. It cannot be locked to
an different resource or unlocked. an different resource or unlocked.
""" """
plugin_commands = {}
plugin_keys = {}
def __init__(self, core, jid): def __init__(self, core, jid):
assert (safeJID(jid).resource) assert (safeJID(jid).resource)
ConversationTab.__init__(self, core, jid) ConversationTab.__init__(self, core, jid)
self.info_header = windows.ConversationInfoWin() self.info_header = windows.ConversationInfoWin()
self.resize() self.resize()
self.update_commands()
self.update_keys()
def get_info_header(self): def get_info_header(self):
return self.info_header return self.info_header