diff options
-rw-r--r-- | doc/stub/tabs.py | 1 | ||||
-rw-r--r-- | plugins/otr.py | 416 | ||||
-rw-r--r-- | requirements.txt | 1 |
3 files changed, 266 insertions, 152 deletions
diff --git a/doc/stub/tabs.py b/doc/stub/tabs.py index 70794313..cd3dc073 100644 --- a/doc/stub/tabs.py +++ b/doc/stub/tabs.py @@ -5,4 +5,5 @@ class MucTab(ChatTab): pass class ConversationTab(ChatTab): pass class RosterInfoTab(Tab): pass class XMLTab(Tab): pass +class DynamicConversationTab(Tab): pass diff --git a/plugins/otr.py b/plugins/otr.py index 9014a4c6..bf7185a4 100644 --- a/plugins/otr.py +++ b/plugins/otr.py @@ -1,8 +1,5 @@ """ -.. warning:: THE OTR LIB IS IN AN EXPERIMENTAL STATE AND SHOULD NOT BE - CONSIDERED AS ENTIRELY RELIABLE - This plugin implements `Off The Record messaging`_. This is a plugin used to encrypt one-to-one conversation using the OTR @@ -22,53 +19,47 @@ Note that if you are having an encrypted conversation with a contact, you can **not** send XHTML-IM messages to him. They will be removed and be replaced by plain text messages. -Installation and configuration ------------------------------- - -To use the OTR plugin, you must first install libopenotr. +Installation +------------ -If you use Archlinux, there is a `libopenotr-git`_ package on the AUR. +To use the OTR plugin, you must first install pure-python-otr. -If not, then you will have to install it by hand. +You have to install it from the git because a few issues were +found with the python3 compatibility while writing this plugin, +and the fixes did not make it into a stable release yet. -First, clone the repo and go inside the created directory: +Install the python module: .. code-block:: bash - git clone https://github.com/teisenbe/libopenotr.git - cd libopenotr + git clone https://github.com/afflux/pure-python-otr.git + cd pure-python-otr + python3 setup.py install --user -Then run autogen.sh and configure +You can also use pip with the requirements.txt at the root of +the poezio directory. -.. code-block:: bash - sh autogen.sh - ./configure --enable-gaping-security-hole +Usage +----- -(as of now, you *should* have been warned enough that the library is not finished) +Command added to Conversation Tabs and Private Tabs: -Then compile & install the lib: +.. glossary:: -.. code-block:: bash + /otr + **Usage:** ``/otr [start|refresh|end|fpr|ourfpr]`` - make - sudo make install + This command is used to start (or refresh), or end an OTR private session. -Finally, install the python module: + The ``fpr`` command gives you the fingerprint of the key of the remove entity, and + the ``ourfpr`` command gives you the fingerprint of your own key. -.. code-block:: bash - - python3 setup.py build - sudo python3 setup.py install - -Usage ------ To use OTR, make sure the plugin is loaded (if not, then do ``/load otr``). Once you are in a private conversation, you have to do a: - .. code-block:: none /otr start @@ -76,160 +67,281 @@ Once you are in a private conversation, you have to do a: The status of the OTR encryption should appear in the bar between the chat and the input as ``OTR: encrypted``. - Once you’re done, end the OTR session with .. code-block:: none /otr end -Known problems --------------- -Empty messages send when changing status. +Important details +----------------- -.. _Off The Record messaging: http://wiki.xmpp.org/web/OTR -.. _libopenotr-git: https://aur.archlinux.org/packages.php?ID=57957 +The OTR session is considered for a full jid. -""" -import pyotr -from sleekxmpp.xmlstream.stanzabase import JID +.. _Off The Record messaging: http://wiki.xmpp.org/web/OTR +""" +import potr +import theming import logging + log = logging.getLogger(__name__) +import os +from potr.context import NotEncryptedError, UnencryptedMessage, ErrorReceived, NotOTRMessage, STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account from plugin import BasePlugin +from tabs import ConversationTab, DynamicConversationTab, PrivateTab +from common import safeJID + +OTR_DIR = os.path.join(os.getenv('XDG_DATA_HOME') or + '~/.local/share', 'poezio', 'otr') -import tabs -from tabs import ConversationTab +POLICY_FLAGS = { + 'ALLOW_V1':False, + 'ALLOW_V2':True, + 'REQUIRE_ENCRYPTION': False, + 'SEND_TAG': True, + 'WHITESPACE_START_AKE': True, + 'ERROR_START_AKE': True +} + +log = logging.getLogger(__name__) + +class PoezioContext(Context): + def __init__(self, account, peer, xmpp, core): + super(PoezioContext, self).__init__(account, peer) + self.xmpp = xmpp + self.core = core + self.flags = {} + + def getPolicy(self, key): + if key in self.flags: + return self.flags[key] + else: + return False + + def inject(self, msg, appdata=None): + self.xmpp.send_message(mto=self.peer, mbody=msg.decode('ascii'), mtype='chat') + + def setState(self, newstate): + tab = self.core.get_tab_by_name(self.peer) + if not tab: + tab = self.core.get_tab_by_name(safeJID(self.peer).bare, DynamicConversationTab) + if not tab.locked_resource == safeJID(self.peer).resource: + tab = None + if self.state == STATE_ENCRYPTED: + if newstate == STATE_ENCRYPTED: + log.debug('OTR conversation with %s refreshed', self.peer) + if tab: + tab.add_message('Refreshed OTR conversation with %s' % self.peer) + elif newstate == STATE_FINISHED or newstate == STATE_PLAINTEXT: + log.debug('OTR conversation with %s finished', self.peer) + if tab: + tab.add_message('Ended OTR conversation with %s' % self.peer) + else: + if newstate == STATE_ENCRYPTED: + if tab: + tab.add_message('Started OTR conversation with %s' % self.peer) + + log.debug('Set encryption state of %s to %s', self.peer, states[newstate]) + super(PoezioContext, self).setState(newstate) + if tab: + self.core.refresh_window() + self.core.doupdate() + +class PoezioAccount(Account): + + def __init__(self, jid, key_dir): + super(PoezioAccount, self).__init__(jid, 'xmpp', 1024) + self.key_dir = os.path.join(key_dir, jid) + + def load_privkey(self): + try: + with open(self.key_dir + '.key3', 'rb') as keyfile: + return potr.crypt.PK.parsePrivateKey(keyfile.read())[0] + except: + log.error('Error in load_privkey', exc_info=True) + + def save_privkey(self): + log.error('coucou') + try: + with open(self.key_dir + '.key3', 'xb') as keyfile: + keyfile.write(self.getPrivkey().serializePrivateKey()) + except: + log.error('Error in save_privkey', exc_info=True) + + def save_trusts(self): + """TODO""" + pass + + saveTrusts = save_trusts + loadPrivkey = load_privkey + savePrivkey = save_privkey + +states = { + STATE_PLAINTEXT: 'plaintext', + STATE_ENCRYPTED: 'encrypted', + STATE_FINISHED: 'finished', +} class Plugin(BasePlugin): + def init(self): - self.contacts = {} - # a dict of {full-JID: OTR object} - self.api.add_event_handler('conversation_say_after', self.on_conversation_say) - self.api.add_event_handler('conversation_msg', self.on_conversation_msg) + # set the default values from the config + policy = self.config.get('encryption_policy', 'ondemand').lower() + POLICY_FLAGS['REQUIRE_ENCRYPTION'] = (policy == 'always') + allow_v2 = self.config.get('allow_v2', 'true').lower() + POLICY_FLAGS['ALLOW_V2'] = (allow_v2 != 'false') + allow_v1 = self.config.get('allow_v1', 'false').lower() + POLICY_FLAGS['ALLOW_v1'] = (allow_v1 == 'true') + + global OTR_DIR + OTR_DIR = os.path.expanduser(self.config.get('keys_dir', '') or OTR_DIR) + try: + os.makedirs(OTR_DIR) + except FileExistsError: + pass + except: + self.api.information('The OTR-specific folder could not be created' + ' poezio will be unable to save keys and trusts', 'OTR') - self.api.add_tab_command(ConversationTab, 'otr', self.command_otr, - usage='<start|end|fpr>', - help='Start or stop OTR for the current conversation.', - short='Manage OTR status', - completion=self.otr_completion) + self.api.add_event_handler('conversation_msg', self.on_conversation_msg) + self.api.add_event_handler('private_msg', self.on_conversation_msg) + self.api.add_event_handler('conversation_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) + PrivateTab.add_information_element('otr', self.display_encryption_status) + self.account = PoezioAccount(self.core.xmpp.boundjid.bare, OTR_DIR) + self.contexts = {} + self.api.add_tab_command(ConversationTab, 'otr', self.command_otr, + help='coucou', + completion=self.completion_otr) + self.api.add_tab_command(PrivateTab, 'otr', self.command_otr, + help='coucou', + completion=self.completion_otr) def cleanup(self): ConversationTab.remove_information_element('otr') - self.del_tab_command(ConversationTab, 'otr') - - def otr_special(self, tab, typ): - def helper(msg): - tab.add_message('%s: %s' % (typ, msg.decode())) - return helper - - def otr_on_state_change(self, tab): - def helper(old, new): - old = self.otr_state(old) - new = self.otr_state(new) - tab.add_message('OTR state has changed from %s to %s' % (old, new)) - return helper - - def get_otr(self, tab): - if tab not in self.contacts: - self.contacts[tab] = pyotr.OTR(on_error=self.otr_special(tab, 'Error'), on_warn=self.otr_special(tab, 'Warn'), on_state_change=self.otr_on_state_change(tab)) - return self.contacts[tab] - - def on_conversation_say(self, message, tab): - """ - Feed the message through the OTR filter - """ - to = message['to'] - if not message['body']: - # there’s nothing to encrypt if this is a chatstate, for example + PrivateTab.remove_information_element('otr') + + def get_context(self, jid): + jid = safeJID(jid).full + if not jid in self.contexts: + flags = POLICY_FLAGS.copy() + policy = self.config.get_by_tabname('encryption_policy', 'ondemand', jid).lower() + flags['REQUIRE_ENCRYPTION'] = (policy == 'always') + allow_v2 = self.config.get_by_tabname('allow_v2', 'true', jid).lower() + flags['ALLOW_V2'] = (allow_v2 != 'false') + allow_v1 = self.config.get_by_tabname('allow_v1', 'false', jid).lower() + flags['ALLOW_V1'] = (allow_v1 == 'true') + self.contexts[jid] = PoezioContext(self.account, jid, self.core.xmpp, self.core) + self.contexts[jid].flags = flags + return self.contexts[jid] + + def on_conversation_msg(self, msg, tab): + try: + ctx = self.get_context(msg['from']) + txt, tlvs = ctx.receiveMessage(msg["body"].encode('utf-8')) + except UnencryptedMessage as err: + # received an unencrypted message inside an OTR session + tab.add_message('The following message from %s was not encrypted:\n%s' % (msg['from'], err.args[0].decode('utf-8')), + jid=msg['from'], nick_color=theming.get_theme().COLOR_REMOTE_USER, + typ=0) + del msg['body'] + self.core.refresh_window() + return + except ErrorReceived as err: + # Received an OTR error + tab.add_message('Received the following error from %s: %s' % (msg['from'], err.args[0])) + del msg['body'] + self.core.refresh_window() + return + except NotOTRMessage as err: + # ignore non-otr messages + # if we expected an OTR message, we would have + # got an UnencryptedMesssage + return + except NotEncryptedError as err: + tab.add_message('An encrypted message from %s was received but is unreadable, as you are not' + ' currently communicating privately.' % msg['from'], + jid=msg['from'], nick_color=theming.get_theme().COLOR_REMOTE_USER, + typ=0) + del msg['body'] + self.core.refresh_window() return - otr_state = self.get_otr(tab) - # Not sure what to do with xhtml bodies, and I don't like them anyway ;) - del message['xhtml_im'] - say = otr_state.transform_msg(message['body'].encode()) - if say is not None: - message['body'] = say.decode() + except: + return + + # remove xhtml + del msg['html'] + del msg['body'] + + if not txt: + return + if isinstance(tab, PrivateTab): + user = tab.parent_muc.get_user_by_name(msg['from'].resource) else: - del message['body'] + user = None + + body = txt.decode() + tab.add_message(body, nickname=tab.nick, jid=msg['from'], + forced_user=user, typ=0) + self.core.refresh_window() + del msg['body'] - def on_conversation_msg(self, message, tab): + def on_conversation_say(self, msg, tab): """ - Feed the message through the OTR filter + On message sent """ - fro = message['from'] - if not message['body']: - # there’s nothing to decrypt if this is a chatstate, for example - return - otr_state = self.get_otr(tab) - # Not sure what to do with xhtml bodies, and I don't like them anyway ;) - del message['xhtml_im'] - display, reply = otr_state.handle_msg(message['body'].encode()) - #self.core.information('D: {!r}, R: {!r}'.format(display, reply)) - if display is not None: - message['body'] = display.decode() + if isinstance(tab, DynamicConversationTab) and tab.locked_resource: + name = safeJID(tab.name) + name.resource = tab.locked_resource + name = name.full else: - del message['body'] - if reply is not None: - self.otr_say(tab, reply.decode()) - - @staticmethod - def otr_state(state): - if state == pyotr.MSG_STATE_PLAINTEXT: - return 'plaintext' - elif state == pyotr.MSG_STATE_ENCRYPTED: - return 'encrypted' - elif state == pyotr.MSG_STATE_FINISHED: - return 'finished' + name = tab.name + ctx = self.contexts.get(name) + if ctx and ctx.state == STATE_ENCRYPTED: + ctx.sendMessage(0, msg['body'].encode('utf-8')) + # remove everything from the message so that it doesn’t get sent + del msg['body'] + del msg['replace'] + del msg['html'] def display_encryption_status(self, jid): + context = self.get_context(jid) + state = states[context.state] + return ' OTR: %s' % state + + def command_otr(self, arg): """ - Returns the status of encryption for the associated jid. This is to be used - in the ConversationTab’s InfoWin. - """ - tab = self.core.get_tab_by_name(jid, tabs.ConversationTab) - if tab not in self.contacts: - return '' - state = self.otr_state(self.contacts[tab].state) - return ' OTR: %s' % (state,) - - def otr_say(self, tab, line): - msg = self.core.xmpp.make_message(tab.get_name()) - msg['type'] = 'chat' - msg['body'] = line - msg.send() - - def command_otr(self, args): - """ - A command to start or end OTR encryption + /otr [start|refresh|end|fpr|ourfpr] """ - args = args.split() - if not args: - return self.api.run_command("/help otr") - if isinstance(self.api.current_tab(), ConversationTab): - jid = JID(self.api.current_tab().get_name()) - command = args[0] - if command == 'start': - otr_state = self.get_otr(self.api.current_tab()) - self.otr_say(self.api.current_tab(), otr_state.start().decode()) - elif command == 'end': - otr_state = self.get_otr(self.api.current_tab()) - msg = otr_state.end() - if msg is not None: - self.otr_say(self.api.current_tab(), msg.decode()) - elif command == 'fpr': - otr_state = self.get_otr(self.api.current_tab()) - our = otr_state.our_fpr - if our: - our = hex(int.from_bytes(our, 'big'))[2:].ljust(40).upper() - their = otr_state.their_fpr - if their: - their = hex(int.from_bytes(their, 'big'))[2:].ljust(40).upper() - self.api.current_tab().add_message('Your: %s Their: %s' % (our, their)) - self.core.refresh_window() + arg = arg.strip() + tab = self.api.current_tab() + name = tab.name + if isinstance(tab, DynamicConversationTab) and tab.locked_resource: + name = safeJID(tab.name) + name.resource = tab.locked_resource + name = name.full + if arg == 'end': # close the session + context = self.get_context(name) + context.disconnect() + elif arg == 'start' or arg == 'refresh': + otr = self.get_context(name) + self.core.xmpp.send_message(mto=name, mtype='chat', + mbody=self.contexts[name].sendMessage(0, b'?OTRv?').decode()) + elif arg == 'ourfpr': + fpr = self.account.getPrivkey() + self.api.information('Your OTR key fingerprint is %s' % fpr, 'OTR') + elif arg == 'fpr': + tab = self.api.current_tab() + if tab.get_name() in self.contexts: + ctx = self.contexts[tab.get_name()] + self.api.information('The key fingerprint for %s is %s' % (name, ctx.getCurrentKey()) , 'OTR') + + def completion_otr(self, the_input): + return the_input.new_completion(['start', 'fpr', 'ourfpr', 'refresh', 'end'], 1, quotify=True) + - def otr_completion(self, the_input): - return the_input.auto_completion(['start', 'fpr', 'end'], '', quotify=False) diff --git a/requirements.txt b/requirements.txt index b94996ea..c0f28946 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -e git://github.com/fritzy/SleekXMPP.git@develop#egg=SleekXMPP +-e git://github.com/afflux/pure-python-otr.git#egg=potr dnspython3==1.10.0 sphinx==1.2b1 argparse |