2013-04-13 20:33:06 +00:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
This plugin implements `Off The Record messaging`_.
|
|
|
|
|
|
|
|
|
|
This is a plugin used to encrypt one-to-one conversation using the OTR
|
|
|
|
|
encryption method. You can use it if you want good privacy, deniability,
|
2013-08-07 18:55:21 +00:00
|
|
|
|
authentication, and strong secrecy. Without this encryption, your messages
|
|
|
|
|
are encrypted **at least** from your client (poezio) to your server. The
|
|
|
|
|
message is decrypted by your server and you cannot control the encryption
|
|
|
|
|
method of your messages from your server to your contact’s server (unless
|
|
|
|
|
you are your own server’s administrator), nor from your contact’s server
|
|
|
|
|
to your contact’s client.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
This plugin does end-to-end encryption. This means that **only** your contact can
|
|
|
|
|
decrypt your messages, and it is fully encrypted during **all** its travel
|
|
|
|
|
through the internet.
|
|
|
|
|
|
|
|
|
|
Note that if you are having an encrypted conversation with a contact, you can
|
2013-08-07 18:55:21 +00:00
|
|
|
|
**not** send XHTML-IM messages to him (or correct messages, or anything more than
|
|
|
|
|
raw text). They will be removed and be replaced by plain text messages.
|
|
|
|
|
|
|
|
|
|
This is a limitation of the OTR protocol, and it will never be fixed. Some clients
|
|
|
|
|
like Pidgin-OTR try do do magic stuff with html unescaping inside the OTR body, and
|
|
|
|
|
it is not pretty.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
Installation
|
|
|
|
|
------------
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
To use the OTR plugin, you must first install pure-python-otr.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
Install the python module:
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
.. code-block:: bash
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
git clone https://github.com/afflux/pure-python-otr.git
|
|
|
|
|
cd pure-python-otr
|
|
|
|
|
python3 setup.py install --user
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
You can also use pip with the requirements.txt at the root of
|
|
|
|
|
the poezio directory.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
Usage
|
|
|
|
|
-----
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
Command added to Conversation Tabs and Private Tabs:
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
.. glossary::
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
/otr
|
2013-08-07 18:55:21 +00:00
|
|
|
|
**Usage:** ``/otr [start|refresh|end|fpr|ourfpr|trust|untrust]``
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
This command is used to manage an OTR private session.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
- The ``start`` (or ``refresh``) command starts or refreshs a private OTR session
|
|
|
|
|
- The ``end`` command ends a private OTR session
|
2014-02-18 23:39:51 +00:00
|
|
|
|
- The ``fpr`` command gives you the fingerprint of the key of the remote entity
|
2013-08-07 18:55:21 +00:00
|
|
|
|
- The ``ourfpr`` command gives you the fingerprint of your own key
|
|
|
|
|
- The ``trust`` command marks the current remote key as trusted for the current remote JID
|
|
|
|
|
- The ``untrust`` command removes that trust
|
2014-01-29 15:41:57 +00:00
|
|
|
|
- Finally, the ``drop`` command is used if you want to delete your private key (not recoverable).
|
|
|
|
|
|
|
|
|
|
.. warning::
|
|
|
|
|
|
2014-05-18 13:11:32 +00:00
|
|
|
|
With ``drop``, the private key is only removed from the filesystem,
|
|
|
|
|
*NOT* with multiple rewrites in a secure manner, you should do that
|
|
|
|
|
yourself if you want to be sure.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
To use OTR, make sure the plugin is loaded (if not, then do ``/load otr``).
|
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
A simple workflow looks like this:
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
.. code-block:: none
|
|
|
|
|
|
|
|
|
|
/otr start
|
|
|
|
|
|
|
|
|
|
The status of the OTR encryption should appear in the bar between the chat and
|
|
|
|
|
the input as ``OTR: encrypted``.
|
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
Then you use ``fpr``/``ourfpr`` to check the fingerprints, and confirm your respective
|
|
|
|
|
identities out-of-band.
|
|
|
|
|
|
|
|
|
|
You can then use
|
|
|
|
|
|
|
|
|
|
.. code-block:: none
|
|
|
|
|
|
|
|
|
|
/otr trust
|
|
|
|
|
|
|
|
|
|
To set the key as trusted, which will be shown when you start or refresh a conversation
|
|
|
|
|
(the trust status will be in a bold font and if the key is untrusted, the remote fingerprint
|
|
|
|
|
will be shown).
|
|
|
|
|
|
2013-04-13 20:33:06 +00:00
|
|
|
|
Once you’re done, end the OTR session with
|
|
|
|
|
|
|
|
|
|
.. code-block:: none
|
|
|
|
|
|
|
|
|
|
/otr end
|
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
Files
|
|
|
|
|
-----
|
|
|
|
|
|
|
|
|
|
This plugin creates trust files complatible with libotr and the files produced by gajim.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The files are located in :file:`$XDG_DATA_HOME/poezio/otr/` by default (so
|
|
|
|
|
:file:`~/.local/share/poezio/otr` in most cases).
|
|
|
|
|
|
|
|
|
|
Two files are created:
|
|
|
|
|
|
|
|
|
|
- An account_jid.key3 (:file:`example@example.com.key3`) file, which contains the private key
|
|
|
|
|
- An account_jid.fpr (:file:`example@example.com.fpr`) file, which contains the list of trusted
|
|
|
|
|
(or untrusted) JIDs and keys.
|
|
|
|
|
|
|
|
|
|
Configuration
|
|
|
|
|
-------------
|
|
|
|
|
|
|
|
|
|
.. glossary::
|
|
|
|
|
:sorted:
|
|
|
|
|
|
2014-04-29 23:55:23 +00:00
|
|
|
|
decode_xhtml
|
|
|
|
|
**Default:** ``true``
|
|
|
|
|
|
|
|
|
|
Decode embedded XHTML.
|
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
keys_dir
|
|
|
|
|
**Default:** ``$XDG_DATA_HOME/poezio/otr``
|
|
|
|
|
|
|
|
|
|
The directory in which you want keys and fpr to be stored.
|
|
|
|
|
|
|
|
|
|
allow_v2
|
|
|
|
|
**Default:** ``true``
|
|
|
|
|
|
|
|
|
|
Allow OTRv2
|
|
|
|
|
|
|
|
|
|
allow_v1
|
|
|
|
|
**Default:** ``false``
|
|
|
|
|
|
|
|
|
|
Allow OTRv1
|
|
|
|
|
|
2014-01-29 15:41:57 +00:00
|
|
|
|
log
|
|
|
|
|
**Default:** false
|
|
|
|
|
|
|
|
|
|
Log conversations (OTR start/end marker, and messages).
|
|
|
|
|
|
2014-04-29 23:55:23 +00:00
|
|
|
|
The :term:`allow_v1`, :term:`allow_v2`, :term:`decode_html`
|
|
|
|
|
and :term:`log` configuration parameters are tab-specific.
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
Important details
|
|
|
|
|
-----------------
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
The OTR session is considered for a full jid, but the trust is considered
|
|
|
|
|
with a bare JID. This is important to know in the case of Private Chats, since
|
|
|
|
|
you cannot always get the real the JID of your contact (or check if the same
|
|
|
|
|
nick is used by different people).
|
2013-04-13 20:33:06 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
.. _Off The Record messaging: http://wiki.xmpp.org/web/OTR
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
"""
|
2014-05-18 13:11:32 +00:00
|
|
|
|
from gettext import gettext as _
|
2013-08-04 13:30:03 +00:00
|
|
|
|
import potr
|
2012-03-25 21:40:22 +00:00
|
|
|
|
import logging
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
2012-03-25 21:40:22 +00:00
|
|
|
|
log = logging.getLogger(__name__)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
import os
|
2013-08-04 21:45:12 +00:00
|
|
|
|
import curses
|
|
|
|
|
|
2013-08-07 18:55:21 +00:00
|
|
|
|
from potr.context import NotEncryptedError, UnencryptedMessage, ErrorReceived, NotOTRMessage,\
|
|
|
|
|
STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account, crypt
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
2014-04-29 23:55:23 +00:00
|
|
|
|
import xhtml
|
2013-08-04 13:30:03 +00:00
|
|
|
|
from common import safeJID
|
2013-08-04 21:45:12 +00:00
|
|
|
|
from config import config
|
2014-05-18 13:11:32 +00:00
|
|
|
|
from plugin import BasePlugin
|
|
|
|
|
from tabs import ConversationTab, DynamicConversationTab, PrivateTab
|
|
|
|
|
from theming import get_theme, dump_tuple
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
OTR_DIR = os.path.join(os.getenv('XDG_DATA_HOME') or
|
|
|
|
|
'~/.local/share', 'poezio', 'otr')
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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__)
|
|
|
|
|
|
2013-08-04 21:45:12 +00:00
|
|
|
|
|
|
|
|
|
def hl(tab):
|
|
|
|
|
if tab.state != 'current':
|
|
|
|
|
tab.state = 'private'
|
|
|
|
|
|
|
|
|
|
conv_jid = safeJID(tab.name)
|
|
|
|
|
if 'private' in config.get('beep_on', 'highlight private').split():
|
2014-05-18 13:11:32 +00:00
|
|
|
|
if config.get_by_tabname('disable_beep', False, conv_jid.bare, False):
|
2013-08-04 21:45:12 +00:00
|
|
|
|
curses.beep()
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
class PoezioContext(Context):
|
|
|
|
|
def __init__(self, account, peer, xmpp, core):
|
|
|
|
|
super(PoezioContext, self).__init__(account, peer)
|
|
|
|
|
self.xmpp = xmpp
|
|
|
|
|
self.core = core
|
|
|
|
|
self.flags = {}
|
2013-08-05 17:45:35 +00:00
|
|
|
|
self.trustName = safeJID(peer).bare
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
def getPolicy(self, key):
|
|
|
|
|
if key in self.flags:
|
|
|
|
|
return self.flags[key]
|
|
|
|
|
else:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def inject(self, msg, appdata=None):
|
2014-05-18 13:11:32 +00:00
|
|
|
|
message = self.xmpp.make_message(mto=self.peer,
|
|
|
|
|
mbody=msg.decode('ascii'),
|
|
|
|
|
mtype='chat')
|
2013-08-11 21:32:44 +00:00
|
|
|
|
message.enable('carbon_private')
|
|
|
|
|
message.send()
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
def setState(self, newstate):
|
2014-05-18 13:11:32 +00:00
|
|
|
|
color_jid = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
|
|
|
|
|
color_info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
tab = self.core.get_tab_by_name(self.peer)
|
|
|
|
|
if not tab:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
tab = self.core.get_tab_by_name(safeJID(self.peer).bare,
|
|
|
|
|
DynamicConversationTab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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:
|
2013-08-05 17:45:35 +00:00
|
|
|
|
if self.getCurrentTrust():
|
2014-05-18 13:11:32 +00:00
|
|
|
|
msg = _('%(info)sRefreshed \x19btrusted\x19o%(info)s'
|
|
|
|
|
' OTR conversation with %(jid_c)s%(jid)s') % {
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid,
|
|
|
|
|
'jid': self.peer
|
|
|
|
|
}
|
|
|
|
|
tab.add_message(msg, typ=self.log)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
else:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
msg = _('%(info)sRefreshed \x19buntrusted\x19o%(info)s'
|
|
|
|
|
' OTR conversation with %(jid_c)s%(jid)s'
|
|
|
|
|
'%(info)s, key: \x19o%(key)s') % {
|
|
|
|
|
'jid': self.peer,
|
|
|
|
|
'key': self.getCurrentKey(),
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
|
|
|
|
|
tab.add_message(msg, typ=self.log)
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
elif newstate == STATE_FINISHED or newstate == STATE_PLAINTEXT:
|
|
|
|
|
log.debug('OTR conversation with %s finished', self.peer)
|
|
|
|
|
if tab:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
tab.add_message('%sEnded OTR conversation with %s%s' % (
|
|
|
|
|
color_info, color_jid, self.peer),
|
|
|
|
|
typ=self.log)
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
else:
|
|
|
|
|
if newstate == STATE_ENCRYPTED:
|
|
|
|
|
if tab:
|
2013-08-05 17:45:35 +00:00
|
|
|
|
if self.getCurrentTrust():
|
2014-05-18 13:11:32 +00:00
|
|
|
|
msg = _('%(info)sStarted a \x19btrusted\x19o%(info)s '
|
|
|
|
|
'OTR conversation with %(jid_c)s%(jid)s') % {
|
|
|
|
|
'jid': self.peer,
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
tab.add_message(msg, typ=self.log)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
else:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
msg = _('%(info)sStarted an \x19buntrusted\x19o%(info)s'
|
|
|
|
|
' OTR conversation with %(jid_c)s%(jid)s'
|
|
|
|
|
'%(info)s, key: \x19o%(key)s') % {
|
|
|
|
|
'jid': self.peer,
|
|
|
|
|
'key': self.getCurrentKey(),
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
tab.add_message(msg, typ=self.log)
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2013-08-04 22:08:11 +00:00
|
|
|
|
def drop_privkey(self):
|
|
|
|
|
try:
|
|
|
|
|
os.remove(self.key_dir + '.key3')
|
|
|
|
|
except:
|
|
|
|
|
log.exception('Error in drop_privkey (removing %s)', self.key_dir + '.key3')
|
|
|
|
|
self.privkey = None
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
def save_privkey(self):
|
|
|
|
|
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)
|
|
|
|
|
|
2013-08-05 17:45:35 +00:00
|
|
|
|
def load_trusts(self):
|
|
|
|
|
try:
|
|
|
|
|
with open(self.key_dir + '.fpr', 'r') as fpr_fd:
|
|
|
|
|
for line in fpr_fd:
|
|
|
|
|
ctx, acc, proto, fpr, trust = line[:-1].split('\t')
|
|
|
|
|
|
|
|
|
|
if acc != self.name or proto != 'xmpp':
|
|
|
|
|
continue
|
|
|
|
|
jid = safeJID(ctx).bare
|
|
|
|
|
if not jid:
|
|
|
|
|
continue
|
|
|
|
|
self.setTrust(jid, fpr, trust)
|
|
|
|
|
except:
|
|
|
|
|
log.error('Error in load_trusts', exc_info=True)
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
def save_trusts(self):
|
2013-08-05 17:45:35 +00:00
|
|
|
|
try:
|
|
|
|
|
with open(self.key_dir + '.fpr', 'w') as fpr_fd:
|
|
|
|
|
for uid, trusts in self.trusts.items():
|
|
|
|
|
for fpr, trustVal in trusts.items():
|
|
|
|
|
fpr_fd.write('\t'.join(
|
|
|
|
|
(uid, self.name, 'xmpp', fpr, trustVal)))
|
|
|
|
|
fpr_fd.write('\n')
|
|
|
|
|
except:
|
|
|
|
|
log.exception('Error in save_trusts', exc_info=True)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
saveTrusts = save_trusts
|
2013-08-05 17:45:35 +00:00
|
|
|
|
loadTrusts = load_trusts
|
2013-08-04 13:30:03 +00:00
|
|
|
|
loadPrivkey = load_privkey
|
|
|
|
|
savePrivkey = save_privkey
|
|
|
|
|
|
|
|
|
|
states = {
|
|
|
|
|
STATE_PLAINTEXT: 'plaintext',
|
|
|
|
|
STATE_ENCRYPTED: 'encrypted',
|
|
|
|
|
STATE_FINISHED: 'finished',
|
|
|
|
|
}
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
|
|
|
|
class Plugin(BasePlugin):
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
2012-03-25 21:40:22 +00:00
|
|
|
|
def init(self):
|
2013-08-04 13:30:03 +00:00
|
|
|
|
# set the default values from the config
|
2014-05-18 13:11:32 +00:00
|
|
|
|
allow_v2 = self.config.get('allow_v2', True)
|
|
|
|
|
POLICY_FLAGS['ALLOW_V2'] = allow_v2
|
|
|
|
|
allow_v1 = self.config.get('allow_v1', False)
|
|
|
|
|
POLICY_FLAGS['ALLOW_v1'] = allow_v1
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
global OTR_DIR
|
|
|
|
|
OTR_DIR = os.path.expanduser(self.config.get('keys_dir', '') or OTR_DIR)
|
|
|
|
|
try:
|
|
|
|
|
os.makedirs(OTR_DIR)
|
2013-08-06 20:30:03 +00:00
|
|
|
|
except OSError as e:
|
|
|
|
|
if e.errno != 17:
|
|
|
|
|
self.api.information('The OTR-specific folder could not be created'
|
|
|
|
|
' poezio will be unable to save keys and trusts', 'OTR')
|
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
except:
|
|
|
|
|
self.api.information('The OTR-specific folder could not be created'
|
|
|
|
|
' poezio will be unable to save keys and trusts', 'OTR')
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
|
2012-03-25 21:40:22 +00:00
|
|
|
|
ConversationTab.add_information_element('otr', self.display_encryption_status)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
PrivateTab.add_information_element('otr', self.display_encryption_status)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.account = PoezioAccount(self.core.xmpp.boundjid.bare, OTR_DIR)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
self.account.load_trusts()
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.contexts = {}
|
2013-08-05 17:45:35 +00:00
|
|
|
|
usage = '[start|refresh|end|fpr|ourfpr|drop|trust|untrust]'
|
2013-08-04 13:36:29 +00:00
|
|
|
|
shortdesc = 'Manage an OTR conversation'
|
2013-08-05 17:45:35 +00:00
|
|
|
|
desc = ('Manage an OTR conversation.\n'
|
|
|
|
|
'start/refresh: Start or refresh a conversation\n'
|
|
|
|
|
'end: End a conversation\n'
|
|
|
|
|
'fpr: Show the fingerprint of the key of the remote user\n'
|
|
|
|
|
'ourfpr: Show the fingerprint of your own key\n'
|
|
|
|
|
'drop: Remove the current key (FOREVER)\n'
|
|
|
|
|
'trust: Set this key for this contact as trusted\n'
|
|
|
|
|
'untrust: Remove the trust for the key of this contact\n')
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.api.add_tab_command(ConversationTab, 'otr', self.command_otr,
|
2013-08-04 13:36:29 +00:00
|
|
|
|
help=desc,
|
|
|
|
|
usage=usage,
|
|
|
|
|
short=shortdesc,
|
2013-08-04 13:30:03 +00:00
|
|
|
|
completion=self.completion_otr)
|
|
|
|
|
self.api.add_tab_command(PrivateTab, 'otr', self.command_otr,
|
2013-08-04 13:36:29 +00:00
|
|
|
|
help=desc,
|
|
|
|
|
usage=usage,
|
|
|
|
|
short=shortdesc,
|
2013-08-04 13:30:03 +00:00
|
|
|
|
completion=self.completion_otr)
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
|
|
|
|
def cleanup(self):
|
2014-05-19 21:58:15 +00:00
|
|
|
|
for context in self.contexts.values():
|
|
|
|
|
context.disconnect()
|
|
|
|
|
|
2012-03-25 21:40:22 +00:00
|
|
|
|
ConversationTab.remove_information_element('otr')
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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()
|
2014-01-29 15:41:57 +00:00
|
|
|
|
logging_policy = self.config.get_by_tabname('log', 'false', jid).lower()
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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)
|
2014-01-29 15:41:57 +00:00
|
|
|
|
self.contexts[jid].log = 1 if logging_policy != 'false' else 0
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.contexts[jid].flags = flags
|
|
|
|
|
return self.contexts[jid]
|
|
|
|
|
|
|
|
|
|
def on_conversation_msg(self, msg, tab):
|
2014-05-18 13:11:32 +00:00
|
|
|
|
color_jid = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
|
|
|
|
|
color_info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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
|
2014-05-18 13:11:32 +00:00
|
|
|
|
text = _('%(info)sThe following message from %(jid_c)s%(jid)s'
|
|
|
|
|
'%(info)s was \x19bnot\x19o%(info)s encrypted:'
|
|
|
|
|
'\x19o\n%(msg)s') % {
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid,
|
|
|
|
|
'jid': msg['from'],
|
|
|
|
|
'msg': err.args[0].decode('utf-8')}
|
|
|
|
|
tab.add_message(text, jid=msg['from'],
|
|
|
|
|
typ=0)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
del msg['body']
|
2013-08-07 18:55:21 +00:00
|
|
|
|
del msg['html']
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.core.refresh_window()
|
|
|
|
|
return
|
|
|
|
|
except ErrorReceived as err:
|
|
|
|
|
# Received an OTR error
|
2014-05-18 13:11:32 +00:00
|
|
|
|
text = _('%(info)sReceived the following error from '
|
|
|
|
|
'%(jid_c)s%(jid)s%(info)s:\x19o %(err)s') % {
|
|
|
|
|
'jid': msg['from'],
|
|
|
|
|
'err': err.args[0],
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
|
|
|
|
|
tab.add_message(text, typ=0)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
del msg['body']
|
2013-08-07 18:55:21 +00:00
|
|
|
|
del msg['html']
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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
|
2013-12-28 15:27:49 +00:00
|
|
|
|
# but do an additional check because of a bug with py3k
|
|
|
|
|
if ctx.state != STATE_PLAINTEXT or ctx.getPolicy('REQUIRE_ENCRYPTION'):
|
|
|
|
|
|
2014-05-18 13:11:32 +00:00
|
|
|
|
text = _('%(info)sThe following message from '
|
|
|
|
|
'%(jid_c)s%(jid)s%(info)s was \x19b'
|
|
|
|
|
'not\x19o%(info)s encrypted:\x19o\n%(msg)s') % {
|
|
|
|
|
'jid': msg['from'],
|
|
|
|
|
'msg': err.args[0].decode('utf-8'),
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
tab.add_message(text, jid=msg['from'],
|
|
|
|
|
typ=ctx.log)
|
2013-12-28 15:27:49 +00:00
|
|
|
|
del msg['body']
|
|
|
|
|
del msg['html']
|
|
|
|
|
hl(tab)
|
|
|
|
|
self.core.refresh_window()
|
|
|
|
|
return
|
2013-08-04 13:30:03 +00:00
|
|
|
|
return
|
|
|
|
|
except NotEncryptedError as err:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
text = _('%(info)sAn encrypted message from %(jid_c)s%(jid)s'
|
|
|
|
|
'%(info)s was received but is unreadable, as you are'
|
|
|
|
|
' not currently communicating privately.') % {
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid,
|
|
|
|
|
'jid': msg['from']}
|
|
|
|
|
tab.add_message(text, jid=msg['from'],
|
|
|
|
|
typ=0)
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
del msg['body']
|
2013-08-07 18:55:21 +00:00
|
|
|
|
del msg['html']
|
|
|
|
|
self.core.refresh_window()
|
|
|
|
|
return
|
|
|
|
|
except crypt.InvalidParameterError:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
tab.add_message('%sThe message from %s%s%s could not be decrypted.'
|
|
|
|
|
% (color_info, color_jid, msg['from'], color_info),
|
2014-05-22 20:07:49 +00:00
|
|
|
|
jid=msg['from'], typ=0)
|
2013-08-07 18:55:21 +00:00
|
|
|
|
hl(tab)
|
|
|
|
|
del msg['body']
|
|
|
|
|
del msg['html']
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.core.refresh_window()
|
2012-03-25 21:40:22 +00:00
|
|
|
|
return
|
2013-08-04 13:30:03 +00:00
|
|
|
|
except:
|
2014-05-18 13:11:32 +00:00
|
|
|
|
tab.add_message('%sAn unspecified error in the OTR plugin occured'
|
|
|
|
|
% color_info,
|
|
|
|
|
typ=0)
|
2013-12-28 15:27:49 +00:00
|
|
|
|
log.error('Unspecified error in the OTR plugin', exc_info=True)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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)
|
2014-05-22 20:07:49 +00:00
|
|
|
|
nick_color = None
|
2012-03-25 21:40:22 +00:00
|
|
|
|
else:
|
2013-08-04 13:30:03 +00:00
|
|
|
|
user = None
|
2014-05-22 20:07:49 +00:00
|
|
|
|
nick_color = get_theme().COLOR_REMOTE_USER
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
body = txt.decode()
|
2014-04-29 23:55:23 +00:00
|
|
|
|
if self.config.get_by_tabname('decode_xhtml', True, msg['from'].bare):
|
|
|
|
|
try:
|
|
|
|
|
body = xhtml.xhtml_to_poezio_colors(body, force=True)
|
|
|
|
|
except:
|
|
|
|
|
pass
|
2013-08-04 13:30:03 +00:00
|
|
|
|
tab.add_message(body, nickname=tab.nick, jid=msg['from'],
|
2014-04-29 23:55:23 +00:00
|
|
|
|
forced_user=user, typ=ctx.log,
|
2014-05-22 20:07:49 +00:00
|
|
|
|
nick_color=nick_color)
|
2013-08-04 21:45:12 +00:00
|
|
|
|
hl(tab)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.core.refresh_window()
|
|
|
|
|
del msg['body']
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
2013-08-04 13:30:03 +00:00
|
|
|
|
def on_conversation_say(self, msg, tab):
|
2012-03-25 21:40:22 +00:00
|
|
|
|
"""
|
2013-08-04 13:30:03 +00:00
|
|
|
|
On message sent
|
2012-03-25 21:40:22 +00:00
|
|
|
|
"""
|
2013-08-04 13:30:03 +00:00
|
|
|
|
if isinstance(tab, DynamicConversationTab) and tab.locked_resource:
|
|
|
|
|
name = safeJID(tab.name)
|
|
|
|
|
name.resource = tab.locked_resource
|
|
|
|
|
name = name.full
|
2012-03-25 21:40:22 +00:00
|
|
|
|
else:
|
2013-08-04 13:30:03 +00:00
|
|
|
|
name = tab.name
|
|
|
|
|
ctx = self.contexts.get(name)
|
|
|
|
|
if ctx and ctx.state == STATE_ENCRYPTED:
|
|
|
|
|
ctx.sendMessage(0, msg['body'].encode('utf-8'))
|
2014-04-17 18:39:01 +00:00
|
|
|
|
if not tab.send_chat_state('active'):
|
|
|
|
|
tab.send_chat_state('inactive', always_send=True)
|
|
|
|
|
|
2014-01-29 15:41:57 +00:00
|
|
|
|
tab.add_message(msg['body'],
|
|
|
|
|
nickname=self.core.own_nick or tab.own_nick,
|
2014-05-18 13:11:32 +00:00
|
|
|
|
nick_color=get_theme().COLOR_OWN_NICK,
|
2014-01-29 15:41:57 +00:00
|
|
|
|
identifier=msg['id'],
|
|
|
|
|
jid=self.core.xmpp.boundjid,
|
2014-02-03 08:34:19 +00:00
|
|
|
|
typ=ctx.log)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
# remove everything from the message so that it doesn’t get sent
|
|
|
|
|
del msg['body']
|
|
|
|
|
del msg['replace']
|
|
|
|
|
del msg['html']
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|
|
|
|
|
def display_encryption_status(self, jid):
|
2013-08-04 13:30:03 +00:00
|
|
|
|
context = self.get_context(jid)
|
|
|
|
|
state = states[context.state]
|
|
|
|
|
return ' OTR: %s' % state
|
|
|
|
|
|
|
|
|
|
def command_otr(self, arg):
|
2012-03-25 21:40:22 +00:00
|
|
|
|
"""
|
2013-08-04 13:30:03 +00:00
|
|
|
|
/otr [start|refresh|end|fpr|ourfpr]
|
2012-03-25 21:40:22 +00:00
|
|
|
|
"""
|
2013-08-04 13:30:03 +00:00
|
|
|
|
arg = arg.strip()
|
|
|
|
|
tab = self.api.current_tab()
|
|
|
|
|
name = tab.name
|
2014-05-18 13:11:32 +00:00
|
|
|
|
color_jid = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
|
|
|
|
|
color_info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
|
2013-08-04 13:30:03 +00:00
|
|
|
|
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':
|
2013-08-18 20:32:26 +00:00
|
|
|
|
if name in self.contexts:
|
|
|
|
|
ctx = self.contexts[name]
|
2013-08-04 13:30:03 +00:00
|
|
|
|
self.api.information('The key fingerprint for %s is %s' % (name, ctx.getCurrentKey()) , 'OTR')
|
2013-08-04 22:08:11 +00:00
|
|
|
|
elif arg == 'drop':
|
|
|
|
|
# drop the privkey (and obviously, end the current conversations before that)
|
|
|
|
|
for context in self.contexts.values():
|
|
|
|
|
if context.state not in (STATE_FINISHED, STATE_PLAINTEXT):
|
|
|
|
|
context.disconnect()
|
|
|
|
|
self.account.drop_privkey()
|
2014-05-18 13:11:32 +00:00
|
|
|
|
tab.add_message('%sPrivate key dropped.' % color_info, typ=0)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
elif arg == 'trust':
|
|
|
|
|
ctx = self.get_context(name)
|
|
|
|
|
key = ctx.getCurrentKey()
|
|
|
|
|
if key:
|
|
|
|
|
fpr = key.cfingerprint()
|
|
|
|
|
else:
|
|
|
|
|
return
|
2013-08-07 18:55:21 +00:00
|
|
|
|
if not ctx.getCurrentTrust():
|
|
|
|
|
ctx.setTrust(fpr, 'verified')
|
|
|
|
|
self.account.saveTrusts()
|
2014-05-18 13:11:32 +00:00
|
|
|
|
text = _('%(info)sYou added %(jid_c)s%(jid)s%(info)s with key '
|
|
|
|
|
'\x19o%(key)s%(info)s to your trusted list.') % {
|
|
|
|
|
'jid': ctx.trustName,
|
|
|
|
|
'key': key,
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
tab.add_message(text, typ=0)
|
2013-08-05 17:45:35 +00:00
|
|
|
|
elif arg == 'untrust':
|
|
|
|
|
ctx = self.get_context(name)
|
|
|
|
|
key = ctx.getCurrentKey()
|
|
|
|
|
if key:
|
|
|
|
|
fpr = key.cfingerprint()
|
|
|
|
|
else:
|
|
|
|
|
return
|
2013-08-07 18:55:21 +00:00
|
|
|
|
if ctx.getCurrentTrust():
|
|
|
|
|
ctx.setTrust(fpr, '')
|
|
|
|
|
self.account.saveTrusts()
|
2014-05-18 13:11:32 +00:00
|
|
|
|
text = _('%(info)sYou removed %(jid_c)s%(jid)s%(info)s with '
|
|
|
|
|
'key \x19o%(key)s%(info)s from your trusted list.') % {
|
|
|
|
|
'jid': ctx.trustName,
|
|
|
|
|
'key': key,
|
|
|
|
|
'info': color_info,
|
|
|
|
|
'jid_c': color_jid}
|
|
|
|
|
|
|
|
|
|
tab.add_message(text, typ=0)
|
2013-08-07 18:55:21 +00:00
|
|
|
|
self.core.refresh_window()
|
2013-08-04 13:30:03 +00:00
|
|
|
|
|
|
|
|
|
def completion_otr(self, the_input):
|
2014-05-18 13:11:32 +00:00
|
|
|
|
comp = ['start', 'fpr', 'ourfpr', 'refresh', 'end', 'trust', 'untrust']
|
|
|
|
|
return the_input.new_completion(comp, 1, quotify=False)
|
2012-03-25 21:40:22 +00:00
|
|
|
|
|