[omemo] Refactor whole plugin

- create a OMEMOConnection class to mimic more how Gajim does things
This commit is contained in:
Philipp Hörist
2017-11-10 21:21:53 +01:00
parent 18598abf24
commit ce480aa5d7
5 changed files with 871 additions and 950 deletions

780
omemo/omemo_connection.py Normal file
View File

@@ -0,0 +1,780 @@
import os
import time
import shutil
import logging
import sqlite3
import nbxmpp
from nbxmpp.simplexml import Node
from gajim.common import app
from gajim.common import ged
from gajim.common import caps_cache
from gajim.common.connection_handlers_events import (
MessageReceivedEvent, MamMessageReceivedEvent, MessageNotSentEvent)
from omemo.xmpp import (
NS_NOTIFY, NS_OMEMO, NS_EME, NS_HINTS, BundleInformationAnnouncement,
BundleInformationQuery, DeviceListAnnouncement, DevicelistQuery,
OmemoMessage, successful, unpack_device_bundle,
unpack_device_list_update, unpack_encrypted)
from omemo.omemo.state import OmemoState
DB_DIR_OLD = app.gajimpaths.data_root
DB_DIR_NEW = app.gajimpaths['MY_DATA']
ALLOWED_TAGS = [('request', nbxmpp.NS_RECEIPTS),
('active', nbxmpp.NS_CHATSTATES),
('gone', nbxmpp.NS_CHATSTATES),
('inactive', nbxmpp.NS_CHATSTATES),
('paused', nbxmpp.NS_CHATSTATES),
('composing', nbxmpp.NS_CHATSTATES),
('no-store', nbxmpp.NS_MSG_HINTS),
('store', nbxmpp.NS_MSG_HINTS),
('no-copy', nbxmpp.NS_MSG_HINTS),
('no-permanent-store', nbxmpp.NS_MSG_HINTS),
('replace', nbxmpp.NS_CORRECT),
('thread', None)]
log = logging.getLogger('gajim.plugin_system.omemo')
class OMEMOConnection:
def __init__(self, account, plugin):
self.account = account
self.plugin = plugin
self.own_jid = self.get_own_jid(stripped=True)
self.omemo = self.__get_omemo()
self.groupchat = {}
self.temp_groupchat = {}
self.gc_message = {}
self.query_for_bundles = []
app.ged.register_event_handler('pep-received', ged.PRECORE,
self.handle_device_list_update)
app.ged.register_event_handler('signed-in', ged.PRECORE,
self.signed_in)
app.ged.register_event_handler('gc-presence-received', ged.PRECORE,
self.gc_presence_received)
app.ged.register_event_handler('gc-config-changed-received', ged.PRECORE,
self.gc_config_changed_received)
app.ged.register_event_handler('muc-admin-received', ged.PRECORE,
self.room_memberlist_received)
def get_con(self):
return app.connections[self.account]
def send_with_callback(self, stanza, callback, data=None):
if data is None:
self.get_con().connection.SendAndCallForResponse(stanza, callback)
else:
self.get_con().connection.SendAndCallForResponse(
stanza, callback, data)
def get_own_jid(self, stripped=False):
if stripped:
return self.get_con().get_own_jid().getStripped()
return self.get_con().get_own_jid()
def migrate_dbpath(self):
old_dbpath = os.path.join(DB_DIR_OLD, 'omemo_' + self.account + '.db')
new_dbpath = os.path.join(DB_DIR_NEW, 'omemo_' + self.own_jid + '.db')
if os.path.exists(old_dbpath):
log.debug('Migrating DBName and Path ..')
try:
shutil.move(old_dbpath, new_dbpath)
return new_dbpath
except Exception:
log.exception('Migration Error:')
return old_dbpath
return new_dbpath
def __get_omemo(self):
""" Returns the the OmemoState for the specified account.
Creates the OmemoState if it does not exist yet.
Parameters
----------
account : str
the account name
Returns
-------
OmemoState
"""
db_path = self.migrate_dbpath()
conn = sqlite3.connect(db_path, check_same_thread=False)
return OmemoState(self.own_jid, conn, self.account, self)
def signed_in(self, event):
""" Method called on SignIn
Parameters
----------
event : SignedInEvent
"""
if event.conn.name != self.account:
return
log.debug('%s => Announce Support after Sign In', self.account)
self.query_for_bundles = []
self.publish_bundle()
self.query_own_devicelist()
def activate(self):
""" Method called when the Plugin is activated in the PluginManager
"""
# self.query_for_bundles = []
if NS_NOTIFY not in app.gajim_optional_features[self.account]:
app.gajim_optional_features[self.account].append(NS_NOTIFY)
self._compute_caps_hash()
if app.account_is_connected(self.account):
log.debug('%s => Announce Support after Plugin Activation',
self.account)
self.query_for_bundles = []
self.publish_bundle()
self.query_own_devicelist()
def deactivate(self):
""" Method called when the Plugin is deactivated in the PluginManager
Removes OMEMO from the Entity Capabilities list
"""
if NS_NOTIFY in app.gajim_optional_features[self.account]:
app.gajim_optional_features[self.account].remove(NS_NOTIFY)
self._compute_caps_hash()
def _compute_caps_hash(self):
""" Computes the hash for Entity Capabilities and publishes it """
app.caps_hash[self.account] = caps_cache.compute_caps_hash(
[app.gajim_identity],
app.gajim_common_features +
app.gajim_optional_features[self.account])
# re-send presence with new hash
connected = app.connections[self.account].connected
if connected > 1 and app.SHOW_LIST[connected] != 'invisible':
app.connections[self.account].change_status(
app.SHOW_LIST[connected], app.connections[self.account].status)
def message_received(self, conn, obj, callback):
if obj.encrypted:
return
if isinstance(obj, MessageReceivedEvent):
self._message_received(obj)
elif isinstance(obj, MamMessageReceivedEvent):
self._mam_message_received(obj)
if obj.encrypted == 'OMEMO':
callback(obj)
def _mam_message_received(self, msg):
""" Handles an incoming MAM message
Payload is decrypted and the plaintext is written into the
event object. Afterwards the event is passed on further to Gajim.
Parameters
----------
msg : MamMessageReceivedEvent
Returns
-------
Return means that the Event is passed on to Gajim
"""
if msg.conn.name != self.account:
return
omemo_encrypted_tag = msg.msg_.getTag('encrypted', namespace=NS_OMEMO)
if omemo_encrypted_tag:
log.debug('%s => OMEMO MAM msg received', self.account)
from_jid = str(msg.msg_.getAttr('from'))
from_jid = app.get_jid_without_resource(from_jid)
msg_dict = unpack_encrypted(omemo_encrypted_tag)
msg_dict['sender_jid'] = from_jid
plaintext = self.omemo.decrypt_msg(msg_dict)
if not plaintext:
msg.encrypted = 'drop'
return
self.print_msg_to_log(msg.msg_)
msg.msgtxt = plaintext
msg.encrypted = self.plugin.encryption_name
return
def _message_received(self, msg):
""" Handles an incoming message
Payload is decrypted and the plaintext is written into the
event object. Afterwards the event is passed on further to Gajim.
Parameters
----------
msg : MessageReceivedEvent
Returns
-------
Return means that the Event is passed on to Gajim
"""
if msg.conn.name != self.account:
return
if msg.stanza.getTag('encrypted', namespace=NS_OMEMO):
log.debug('%s => OMEMO msg received', self.account)
if msg.forwarded and msg.sent:
from_jid = str(msg.stanza.getTo()) # why gajim? why?
log.debug('message was forwarded doing magic')
else:
from_jid = str(msg.stanza.getFrom())
self.print_msg_to_log(msg.stanza)
msg_dict = unpack_encrypted(msg.stanza.getTag
('encrypted', namespace=NS_OMEMO))
if msg.mtype == 'groupchat':
address_tag = msg.stanza.getTag('addresses',
namespace=nbxmpp.NS_ADDRESS)
if address_tag: # History Message from MUC
from_jid = address_tag.getTag(
'address', attrs={'type': 'ofrom'}).getAttr('jid')
else:
try:
from_jid = self.groupchat[msg.jid][msg.resource]
except KeyError:
log.debug('Groupchat: Last resort trying to '
'find SID in DB')
from_jid = self.omemo.store. \
getJidFromDevice(msg_dict['sid'])
if not from_jid:
log.error('%s => Cant decrypt GroupChat Message '
'from %s', self.account, msg.resource)
msg.encrypted = 'drop'
return
self.groupchat[msg.jid][msg.resource] = from_jid
log.debug('GroupChat Message from: %s', from_jid)
plaintext = ''
if msg_dict['sid'] == self.omemo.own_device_id:
if msg_dict['payload'] in self.gc_message:
plaintext = self.gc_message[msg_dict['payload']]
del self.gc_message[msg_dict['payload']]
else:
log.error('%s => Cant decrypt own GroupChat Message',
self.account)
msg.encrypted = 'drop'
return
else:
msg_dict['sender_jid'] = app. \
get_jid_without_resource(from_jid)
plaintext = self.omemo.decrypt_msg(msg_dict)
if not plaintext:
msg.encrypted = 'drop'
return
msg.msgtxt = plaintext
# Gajim bug: there must be a body or the message
# gets dropped from history
msg.stanza.setBody(plaintext)
msg.encrypted = self.plugin.encryption_name
def room_memberlist_received(self, event):
if event.conn.name != self.account:
return
log.debug('Room %s Memberlist received: %s',
event.fjid, event.users_dict)
room = event.fjid
if room not in self.groupchat:
self.groupchat[room] = {}
def jid_known(jid):
for nick in self.groupchat[room]:
if self.groupchat[room][nick] == jid:
return True
return False
for jid in event.users_dict:
if not jid_known(jid):
# Add JID with JID because we have no Nick yet
self.groupchat[room][jid] = jid
log.debug('JID Added: %s', jid)
def gc_presence_received(self, event):
if event.conn.name != self.account:
return
if not hasattr(event, 'real_jid') or not event.real_jid:
return
room = event.room_jid
jid = app.get_jid_without_resource(event.real_jid)
nick = event.nick
if '303' in event.status_code: # Nick Changed
if room in self.groupchat:
if nick in self.groupchat[room]:
del self.groupchat[room][nick]
self.groupchat[room][event.new_nick] = jid
log.debug('Nick Change: old: %s, new: %s, jid: %s ',
nick, event.new_nick, jid)
log.debug('Members after Change: %s', self.groupchat[room])
else:
if nick in self.temp_groupchat[room]:
del self.temp_groupchat[room][nick]
self.temp_groupchat[room][event.new_nick] = jid
return
if room not in self.groupchat:
if room not in self.temp_groupchat:
self.temp_groupchat[room] = {}
if nick not in self.temp_groupchat[room]:
self.temp_groupchat[room][nick] = jid
else:
# Check if we received JID over Memberlist
if jid in self.groupchat[room]:
del self.groupchat[room][jid]
# Add JID with Nick
if nick not in self.groupchat[room]:
self.groupchat[room][nick] = jid
log.debug('JID Added: %s', jid)
if '100' in event.status_code: # non-anonymous Room (Full JID)
if room not in self.groupchat:
self.groupchat[room] = self.temp_groupchat[room]
log.debug('OMEMO capable Room found: %s', room)
self.get_con().get_affiliation_list(room, 'owner')
self.get_con().get_affiliation_list(room, 'admin')
self.get_con().get_affiliation_list(room, 'member')
def gc_config_changed_received(self, event):
if event.conn.name != self.account:
return
room = event.room_jid
if '172' in event.status_code:
if room not in self.groupchat:
self.groupchat[room] = self.temp_groupchat[room]
log.debug('CONFIG CHANGE')
log.debug(event.room_jid)
log.debug(event.status_code)
def gc_encrypt_message(self, conn, event, callback):
""" Manipulates the outgoing groupchat stanza
The body is getting encrypted
Parameters
----------
conn : nbxmpp.NonBlockingClient
event : GcStanzaMessageOutgoingEvent
callback: func
The callback. Its only called if the stanza was encrypted.
This prevents any accidental sending of unencrypted messages.
"""
if event.conn.name != self.account:
return
try:
self.cleanup_stanza(event)
if not event.message:
callback(event)
return
to_jid = app.get_jid_without_resource(event.jid)
msg_dict = self.omemo.create_gc_msg(
self.own_jid, to_jid, event.message.encode('utf8'))
if not msg_dict:
raise OMEMOError('Error while encrypting')
except OMEMOError as error:
log.error(error)
app.nec.push_incoming_event(
MessageNotSentEvent(
None, conn=conn, jid=event.jid, message=event.message,
error=error, time_=time.time(), session=None))
return
self.gc_message[msg_dict['payload']] = event.message
encrypted_node = OmemoMessage(msg_dict)
event.msg_iq.addChild(node=encrypted_node)
# XEP-0380: Explicit Message Encryption
eme_node = Node('encryption', attrs={'xmlns': NS_EME,
'name': 'OMEMO',
'namespace': NS_OMEMO})
event.msg_iq.addChild(node=eme_node)
# Add Message for devices that dont support OMEMO
support_msg = 'You received a message encrypted with ' \
'OMEMO but your client doesnt support OMEMO.'
event.msg_iq.setBody(support_msg)
# Store Hint for MAM
store = Node('store', attrs={'xmlns': NS_HINTS})
event.msg_iq.addChild(node=store)
self.print_msg_to_log(event.msg_iq)
callback(event)
def encrypt_message(self, conn, event, callback):
""" Manipulates the outgoing stanza
Encrypt the body
Parameters
----------
conn : nbxmpp.NonBlockingClient
event : StanzaMessageOutgoingEvent
callback: func
The callback. Its only called if the stanza was encrypted.
This prevents any accidental sending of unencrypted messages.
"""
if event.conn.name != self.account:
return
try:
self.cleanup_stanza(event)
if not event.message:
callback(event)
return
to_jid = app.get_jid_without_resource(event.jid)
plaintext = event.message.encode('utf8')
msg_dict = self.omemo.create_msg(self.own_jid, to_jid, plaintext)
if not msg_dict:
raise OMEMOError('Error while encrypting')
except OMEMOError as error:
log.error(error)
app.nec.push_incoming_event(
MessageNotSentEvent(
None, conn=conn, jid=event.jid, message=event.message,
error=error, time_=time.time(), session=event.session))
return
encrypted_node = OmemoMessage(msg_dict)
event.msg_iq.addChild(node=encrypted_node)
# XEP-0380: Explicit Message Encryption
eme_node = Node('encryption', attrs={'xmlns': NS_EME,
'name': 'OMEMO',
'namespace': NS_OMEMO})
event.msg_iq.addChild(node=eme_node)
# Store Hint for MAM
store = Node('store', attrs={'xmlns': NS_HINTS})
event.msg_iq.addChild(node=store)
self.print_msg_to_log(event.msg_iq)
event.xhtml = None
event.encrypted = self.plugin.encryption_name
callback(event)
@staticmethod
def cleanup_stanza(obj):
''' We make sure only allowed tags are in the stanza '''
stanza = nbxmpp.Message(
to=obj.msg_iq.getTo(),
typ=obj.msg_iq.getType())
stanza.setThread(obj.msg_iq.getThread())
for tag, ns in ALLOWED_TAGS:
node = obj.msg_iq.getTag(tag, namespace=ns)
if node:
stanza.addChild(node=node)
obj.msg_iq = stanza
def handle_device_list_update(self, event):
""" Check if the passed event is a device list update and store the new
device ids.
Parameters
----------
event : PEPReceivedEvent
Returns
-------
bool
True if the given event was a valid device list update event
"""
if event.conn.name != self.account:
return
if event.pep_type != 'omemo-devicelist':
return False
devices_list = list(set(unpack_device_list_update(event.stanza,
event.conn.name)))
contact_jid = app.get_jid_without_resource(event.fjid)
if not devices_list:
log.error('%s => Received empty or invalid Devicelist from: %s',
self.account, contact_jid)
return False
if self.get_own_jid().bareMatch(contact_jid):
log.info('%s => Received own device list: %s',
self.account, devices_list)
self.omemo.set_own_devices(devices_list)
self.omemo.store.sessionStore.setActiveState(
devices_list, self.own_jid)
# remove contact from list, so on send button pressed
# we query for bundle and build a session
if contact_jid in self.query_for_bundles:
self.query_for_bundles.remove(contact_jid)
if not self.omemo.own_device_id_published():
# Our own device_id is not in the list, it could be
# overwritten by some other client
self.publish_own_devices_list()
else:
log.info('%s => Received device list for %s: %s',
self.account, contact_jid, devices_list)
self.omemo.set_devices(contact_jid, devices_list)
self.omemo.store.sessionStore.setActiveState(
devices_list, contact_jid)
# remove contact from list, so on send button pressed
# we query for bundle and build a session
if contact_jid in self.query_for_bundles:
self.query_for_bundles.remove(contact_jid)
# Enable Encryption on receiving first Device List
# TODO
return True
def publish_own_devices_list(self, new=False):
""" Get all currently known own active device ids and publish them
Parameters
----------
new : bool
if True, a devicelist with only one
(the current id of this instance) device id is pushed
"""
if new:
devices_list = [self.omemo.own_device_id]
else:
devices_list = self.omemo.own_devices
devices_list.append(self.omemo.own_device_id)
devices_list = list(set(devices_list))
self.omemo.set_own_devices(devices_list)
log.debug('%s => Publishing own Devices: %s',
self.account, devices_list)
device_announce = DeviceListAnnouncement(devices_list)
self.send_with_callback(device_announce,
self.device_list_publish_result)
@staticmethod
def device_list_publish_result(stanza):
log.debug(stanza)
def are_keys_missing(self, contact_jid):
""" Checks if devicekeys are missing and querys the
bundles
Parameters
----------
contact_jid : str
bare jid of the contact
Returns
-------
bool
Returns True if there are no trusted Fingerprints
"""
# Fetch Bundles of own other Devices
if self.own_jid not in self.query_for_bundles:
devices_without_session = self.omemo \
.devices_without_sessions(self.own_jid)
self.query_for_bundles.append(self.own_jid)
if devices_without_session:
for device_id in devices_without_session:
self.fetch_device_bundle_information(self.own_jid,
device_id)
# Fetch Bundles of contacts devices
if contact_jid not in self.query_for_bundles:
devices_without_session = self.omemo \
.devices_without_sessions(contact_jid)
self.query_for_bundles.append(contact_jid)
if devices_without_session:
for device_id in devices_without_session:
self.fetch_device_bundle_information(contact_jid,
device_id)
if self.omemo.getTrustedFingerprints(contact_jid):
return False
return True
def fetch_device_bundle_information(self, jid, device_id):
""" Fetch bundle information for specified jid, key, and create axolotl
session on success.
Parameters
----------
jid : str
The jid to query for bundle information
device_id : int
The device id for which we want the bundle
"""
log.info('%s => Fetch bundle device %s#%s',
self.account, device_id, jid)
bundle_query = BundleInformationQuery(jid, device_id)
self.send_with_callback(bundle_query,
self.session_from_prekey_bundle,
{'jid': jid, 'device_id': device_id})
def session_from_prekey_bundle(self, conn, stanza, jid, device_id):
""" Starts a session from a PreKey bundle.
This method tries to build an axolotl session when a PreKey bundle
is fetched.
If a session can not be build it will fail silently but log the a
warning.
Parameters
----------
conn : nbxmpp.NonBlockingClient
stanza : nbxmpp.Iq
The stanza
jid : str
Jid of the contact
device_id : int
The device id
"""
bundle_dict = unpack_device_bundle(stanza, device_id)
if not bundle_dict:
log.warning('Failed to build Session with %s', jid)
return
if self.omemo.build_session(jid, device_id, bundle_dict):
log.info('%s => session created for: %s',
self.account, jid)
# Trigger dialog to trust new Fingerprints if
# the Chat Window is Open
ctrl = app.interface.msg_win_mgr.get_control(
jid, self.account)
if ctrl:
self.plugin.new_fingerprints_available(ctrl)
def query_own_devicelist(self):
""" Query own devicelist from the server """
device_query = DevicelistQuery(self.own_jid)
log.info('%s => Querry own devicelist ...', self.account)
self.send_with_callback(device_query, self.handle_devicelist_result)
def publish_bundle(self):
""" Publish our bundle information to the PEP node """
bundle_announce = BundleInformationAnnouncement(
self.omemo.bundle, self.omemo.own_device_id)
log.info('%s => Publishing bundle ...', self.account)
self.send_with_callback(bundle_announce, self.handle_publish_result)
def handle_publish_result(self, stanza):
""" Log if publishing our bundle was successful
Parameters
----------
stanza : nbxmpp.Iq
The stanza
"""
if successful(stanza):
log.info('%s => Publishing bundle was successful', self.account)
else:
log.error('%s => Publishing bundle was NOT successful',
self.account)
def handle_devicelist_result(self, stanza):
""" If query was successful add own device to the list.
Parameters
----------
stanza : nbxmpp.Iq
The stanza
"""
if successful(stanza):
devices_list = list(set(unpack_device_list_update(stanza, self.account)))
if not devices_list:
log.error('%s => Devicelistquery was NOT successful',
self.account)
self.publish_own_devices_list(new=True)
return False
self.omemo.set_own_devices(devices_list)
self.omemo.store.sessionStore.setActiveState(
devices_list, self.own_jid)
log.info('%s => Devicelistquery was successful', self.account)
if not self.omemo.own_device_id_published():
# Our own device_id is not in the list, it could be
# overwritten by some other client
self.publish_own_devices_list()
else:
log.error('%s => Devicelistquery was NOT successful', self.account)
self.publish_own_devices_list(new=True)
def clear_device_list(self):
""" Overwrite the current devicelist on the server with only
our device id.
"""
if not app.account_is_connected(self.account):
return
devices_list = [self.omemo.own_device_id]
self.omemo.set_own_devices(devices_list)
log.info('%s => Clearing devices_list %s', self.account, devices_list)
device_announce = DeviceListAnnouncement(devices_list)
self.send_with_callback(device_announce, self.clear_device_list_result)
@staticmethod
def clear_device_list_result(stanza):
log.info(stanza)
@staticmethod
def print_msg_to_log(stanza):
""" Prints a stanza in a fancy way to the log """
log.debug('-'*15)
stanzastr = '\n' + stanza.__str__(fancy=True)
stanzastr = stanzastr[0:-1]
log.debug(stanzastr)
log.debug('-'*15)
class OMEMOError(Exception):
pass