diff options
-rw-r--r-- | omemo/omemo/state.py | 16 | ||||
-rw-r--r-- | omemo/omemo_connection.py | 780 | ||||
-rw-r--r-- | omemo/omemoplugin.py | 1014 | ||||
-rw-r--r-- | omemo/ui.py | 8 | ||||
-rw-r--r-- | omemo/xmpp.py | 3 |
5 files changed, 871 insertions, 950 deletions
diff --git a/omemo/omemo/state.py b/omemo/omemo/state.py index e4f5498..2cd5234 100644 --- a/omemo/omemo/state.py +++ b/omemo/omemo/state.py @@ -52,18 +52,18 @@ UNDECIDED = 2 class OmemoState:
- def __init__(self, own_jid, connection, account, plugin):
+ def __init__(self, own_jid, db_con, account, xmpp_con):
""" Instantiates an OmemoState object.
:param connection: an :py:class:`sqlite3.Connection`
"""
self.account = account
- self.plugin = plugin
+ self.xmpp_con = xmpp_con
self.session_ciphers = {}
self.own_jid = own_jid
self.device_ids = {}
self.own_devices = []
- self.store = LiteAxolotlStore(connection)
+ self.store = LiteAxolotlStore(db_con)
self.encryption = self.store.encryptionStore
for jid, device_id in self.store.getActiveDeviceTuples():
if jid != own_jid:
@@ -302,8 +302,8 @@ class OmemoState: self.get_session_cipher(tup[0], tup[1])
# Encrypt the message key with for each of receivers devices
- for nick in self.plugin.groupchat[room]:
- jid_to = self.plugin.groupchat[room][nick]
+ for nick in self.xmpp_con.groupchat[room]:
+ jid_to = self.xmpp_con.groupchat[room][nick]
if jid_to == self.own_jid:
continue
if jid_to in encrypted_jids: # We already encrypted to this JID
@@ -366,8 +366,8 @@ class OmemoState: if gc:
room = jid
devicelist = []
- for nick in self.plugin.groupchat[room]:
- jid_to = self.plugin.groupchat[room][nick]
+ for nick in self.xmpp_con.groupchat[room]:
+ jid_to = self.xmpp_con.groupchat[room][nick]
if jid_to == self.own_jid:
continue
try:
@@ -449,7 +449,7 @@ class OmemoState: key = sessionCipher.decryptPkmsg(preKeyWhisperMessage)
# Publish new bundle after PreKey has been used
# for building a new Session
- self.plugin.publish_bundle(self.account)
+ self.xmpp_con.publish_bundle()
self.add_device(recipient_id, device_id)
return key
except UntrustedIdentityException as e:
diff --git a/omemo/omemo_connection.py b/omemo/omemo_connection.py new file mode 100644 index 0000000..98247fd --- /dev/null +++ b/omemo/omemo_connection.py @@ -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 diff --git a/omemo/omemoplugin.py b/omemo/omemoplugin.py index 1f20e5f..fa2dfd4 100644 --- a/omemo/omemoplugin.py +++ b/omemo/omemoplugin.py @@ -21,35 +21,18 @@ the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. ''' import logging -import os -import sqlite3 -import shutil -import nbxmpp import binascii import threading -import time from gi.repository import GLib -from nbxmpp.simplexml import Node -from nbxmpp import NS_ADDRESS from gajim import dialogs -from gajim.common import caps_cache, app, ged, configpaths +from gajim.common import app, ged from gajim.common.pep import SUPPORTED_PERSONAL_USER_EVENTS from gajim.plugins import GajimPlugin from gajim.groupchat_control import GroupchatControl -from .xmpp import ( - NS_NOTIFY, NS_OMEMO, NS_EME, BundleInformationAnnouncement, - BundleInformationQuery, DeviceListAnnouncement, DevicelistQuery, - DevicelistPEP, OmemoMessage, successful, unpack_device_bundle, - unpack_device_list_update, unpack_encrypted) - -from gajim.common.connection_handlers_events import ( - MessageReceivedEvent, MamMessageReceivedEvent, MessageNotSentEvent) - - -IQ_CALLBACK = {} +from omemo.xmpp import DevicelistPEP CRYPTOGRAPHY_MISSING = 'You are missing Python-Cryptography' AXOLOTL_MISSING = 'You are missing Python-Axolotl or use an outdated version' @@ -57,61 +40,40 @@ PROTOBUF_MISSING = 'OMEMO cant import Google Protobuf, you can find help in ' \ 'the GitHub Wiki' ERROR_MSG = '' -NS_HINTS = 'urn:xmpp:hints' -DB_DIR_OLD = app.gajimpaths.data_root -DB_DIR_NEW = configpaths.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') try: - from .file_decryption import FileDecryption -except Exception as e: - log.exception(e) + from omemo.file_decryption import FileDecryption +except Exception as error: + log.exception(error) ERROR_MSG = CRYPTOGRAPHY_MISSING try: import google.protobuf -except Exception as e: - log.error(e) +except Exception as error: + log.error(error) ERROR_MSG = PROTOBUF_MISSING try: import axolotl -except Exception as e: - log.error(e) +except Exception as error: + log.error(error) ERROR_MSG = AXOLOTL_MISSING if not ERROR_MSG: try: - from .omemo.state import OmemoState - from .ui import OMEMOConfigDialog, FingerprintWindow - except Exception as e: - log.error(e) - ERROR_MSG = 'Error: ' + str(e) + from omemo.omemo_connection import OMEMOConnection + from omemo.ui import OMEMOConfigDialog, FingerprintWindow + except Exception as error: + log.error(error) + ERROR_MSG = 'Error: %s' % error # pylint: disable=no-init # pylint: disable=attribute-defined-outside-init class OmemoPlugin(GajimPlugin): - - omemo_states = {} - groupchat = {} - temp_groupchat = {} - def init(self): """ Init """ if ERROR_MSG: @@ -122,21 +84,16 @@ class OmemoPlugin(GajimPlugin): self.encryption_name = 'OMEMO' self.allow_groupchat = True self.events_handlers = { - 'pep-received': (ged.PRECORE, self.handle_device_list_update), - 'raw-iq-received': (ged.PRECORE, self.handle_iq_received), 'signed-in': (ged.PRECORE, self.signed_in), - 'gc-presence-received': (ged.PRECORE, self.gc_presence_received), - 'gc-config-changed-received': - (ged.PRECORE, self.gc_config_changed_received), - 'muc-admin-received': (ged.PRECORE, self.room_memberlist_received), } self.config_dialog = OMEMOConfigDialog(self) self.gui_extension_points = { - 'hyperlink_handler': (self.file_decryption, None), + 'hyperlink_handler': (self._file_decryption, None), 'encrypt' + self.encryption_name: (self._encrypt_message, None), - 'gc_encrypt' + self.encryption_name: (self._gc_encrypt_message, None), - 'decrypt': (self.message_received, None), + 'gc_encrypt' + self.encryption_name: ( + self._gc_encrypt_message, None), + 'decrypt': (self._message_received, None), 'send_message' + self.encryption_name: ( self.before_sendmessage, None), 'encryption_dialog' + self.encryption_name: ( @@ -145,11 +102,9 @@ class OmemoPlugin(GajimPlugin): self.encryption_state, None)} SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP) - self.announced = [] - self.query_for_bundles = [] self.disabled_accounts = [] - self.gc_message = {} self.windowinstances = {} + self.connections = {} self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), } @@ -162,66 +117,6 @@ class OmemoPlugin(GajimPlugin): schemes += ' aesgcm://' app.config.set('uri_schemes', schemes) - def migrate_dbpath(self, account, my_jid): - old_dbpath = os.path.join(DB_DIR_OLD, 'omemo_' + account + '.db') - new_dbpath = os.path.join(DB_DIR_NEW, 'omemo_' + my_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_state(self, account): - """ 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 - """ - if account in self.disabled_accounts: - return - if account not in self.omemo_states: - my_jid = app.get_jid_from_account(account) - db_path = self.migrate_dbpath(account, my_jid) - - conn = sqlite3.connect(db_path, check_same_thread=False) - self.omemo_states[account] = OmemoState(my_jid, conn, account, - self) - - return self.omemo_states[account] - - def file_decryption(self, url, kind, instance, window): - FileDecryption(self).hyperlink_handler(url, kind, instance, window) - - def encrypt_file(self, file, account, callback): - thread = threading.Thread(target=self._encrypt_file_thread, - args=(file, account, callback)) - thread.daemon = True - thread.start() - - def _encrypt_file_thread(self, file, account, callback): - state = self.get_omemo_state(account) - encrypted_data, key, iv = state.encrypt_file(file.get_data(full=True)) - file.encrypted = True - file.size = len(encrypted_data) - file.user_data = binascii.hexlify(iv + key).decode('utf-8') - file.data = encrypted_data - if file.event.isSet(): - return - GLib.idle_add(callback, file) - def signed_in(self, event): """ Method called on SignIn @@ -232,57 +127,64 @@ class OmemoPlugin(GajimPlugin): account = event.conn.name if account in self.disabled_accounts: return - log.debug(account + - ' => Announce Support after Sign In') - self.query_for_bundles = [] - self.announced = [] - self.announced.append(account) - self.publish_bundle(account) - self.query_own_devicelist(account) + if account not in self.connections: + self.connections[account] = OMEMOConnection(account, self) + self.connections[account].signed_in(event) def activate(self): """ Method called when the Plugin is activated in the PluginManager """ - self.query_for_bundles = [] - # Publish bundle information and Entity Caps for account in app.connections: if account in self.disabled_accounts: - log.debug(account + - ' => Account is disabled') continue - if NS_NOTIFY not in app.gajim_optional_features[account]: - app.gajim_optional_features[account].append(NS_NOTIFY) - self._compute_caps_hash(account) - if account not in self.announced: - if app.account_is_connected(account): - log.debug(account + - ' => Announce Support after Plugin Activation') - self.announced.append(account) - self.publish_bundle(account) - self.query_own_devicelist(account) + self.connections[account] = OMEMOConnection(account, self) + self.connections[account].activate() def deactivate(self): """ Method called when the Plugin is deactivated in the PluginManager - - Removes OMEMO from the Entity Capabilities list """ - for account in app.connections: - if account in self.disabled_accounts: - continue - if NS_NOTIFY in app.gajim_optional_features[account]: - app.gajim_optional_features[account].remove(NS_NOTIFY) - self._compute_caps_hash(account) + for account in self.connections: + self.connections[account].deactivate() def activate_encryption(self, chat_control): if isinstance(chat_control, GroupchatControl): if chat_control.room_jid not in self.groupchat: dialogs.ErrorDialog( - _('Bad Configuration'), - _('To use OMEMO in a Groupchat, the Groupchat should be' - ' non-anonymous and members-only.')) + _('Bad Configuration'), + _('To use OMEMO in a Groupchat, the Groupchat should be' + ' non-anonymous and members-only.')) return False return True + def _message_received(self, conn, obj, callback): + self.connections[conn.name].message_received(conn, obj, callback) + + def _gc_encrypt_message(self, conn, obj, callback): + self.connections[conn.name].gc_encrypt_message(conn, obj, callback) + + def _encrypt_message(self, conn, obj, callback): + self.connections[conn.name].encrypt_message(conn, obj, callback) + + def _file_decryption(self, url, kind, instance, window): + FileDecryption(self).hyperlink_handler(url, kind, instance, window) + + def encrypt_file(self, file, account, callback): + thread = threading.Thread(target=self._encrypt_file_thread, + args=(file, account, callback)) + thread.daemon = True + thread.start() + + def _encrypt_file_thread(self, file, account, callback): + omemo = self.get_omemo(account) + encrypted_data, key, iv = omemo.encrypt_file(file.get_data(full=True)) + file.encrypted = True + file.size = len(encrypted_data) + file.user_data = binascii.hexlify(iv + key).decode('utf-8') + file.data = encrypted_data + if file.event.isSet(): + return + GLib.idle_add(callback, file) + @staticmethod def encryption_state(chat_control, state): state['visible'] = True @@ -291,6 +193,9 @@ class OmemoPlugin(GajimPlugin): def on_encryption_button_clicked(self, chat_control): self.show_fingerprint_window(chat_control) + def get_omemo(self, account): + return self.connections[account].omemo + def before_sendmessage(self, chat_control): account = chat_control.account contact = chat_control.contact @@ -303,37 +208,38 @@ class OmemoPlugin(GajimPlugin): real_jid = self.groupchat[room][nick] if real_jid == own_jid: continue - if not self.are_keys_missing(account, real_jid): + if not self.connections[account].are_keys_missing(real_jid): missing = False if missing: - log.debug(account + ' => No Trusted Fingerprints for ' + room) + log.debug('%s => No Trusted Fingerprints for %s', + account, room) self.no_trusted_fingerprints_warning(chat_control) else: - if self.are_keys_missing(account, contact.jid): - log.debug(account + ' => No Trusted Fingerprints for ' + - contact.jid) + if self.connections[account].are_keys_missing(contact.jid): + log.debug('%s => No Trusted Fingerprints for %s', + account, contact.jid) self.no_trusted_fingerprints_warning(chat_control) chat_control.sendmessage = False else: - log.debug(account + ' => Sending Message to ' + - contact.jid) + log.debug('%s => Sending Message to %s', + account, contact.jid) def new_fingerprints_available(self, chat_control): jid = chat_control.contact.jid account = chat_control.account - state = self.get_omemo_state(account) + omemo = self.get_omemo(account) if isinstance(chat_control, GroupchatControl): room_jid = chat_control.room_jid if room_jid in self.groupchat: for nick in self.groupchat[room_jid]: real_jid = self.groupchat[room_jid][nick] - fingerprints = state.store. \ + fingerprints = omemo.store. \ getNewFingerprints(real_jid) if fingerprints: self.show_fingerprint_window( chat_control, fingerprints) elif not isinstance(chat_control, GroupchatControl): - fingerprints = state.store.getNewFingerprints(jid) + fingerprints = omemo.store.getNewFingerprints(jid) if fingerprints: self.show_fingerprint_window( chat_control, fingerprints) @@ -341,7 +247,7 @@ class OmemoPlugin(GajimPlugin): def show_fingerprint_window(self, chat_control, fingerprints=None): contact = chat_control.contact account = chat_control.account - state = self.get_omemo_state(account) + omemo = self.get_omemo(account) transient = chat_control.parent_win.window if 'dialog' not in self.windowinstances: if isinstance(chat_control, GroupchatControl): @@ -354,782 +260,16 @@ class OmemoPlugin(GajimPlugin): self.windowinstances) self.windowinstances['dialog'].show_all() if fingerprints: - log.debug(account + - ' => Showing Fingerprint Prompt for ' + - contact.jid) - state.store.setShownFingerprints(fingerprints) + log.debug('%s => Showing Fingerprint Prompt for %s', + account, contact.jid) + omemo.store.setShownFingerprints(fingerprints) else: self.windowinstances['dialog'].update_context_list() if fingerprints: - state.store.setShownFingerprints(fingerprints) - - @staticmethod - def _compute_caps_hash(account): - """ Computes the hash for Entity Capabilities and publishes it """ - app.caps_hash[account] = caps_cache.compute_caps_hash( - [app.gajim_identity], - app.gajim_common_features + - app.gajim_optional_features[account]) - # re-send presence with new hash - connected = app.connections[account].connected - if connected > 1 and app.SHOW_LIST[connected] != 'invisible': - app.connections[account].change_status( - app.SHOW_LIST[connected], app.connections[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 - """ - account = msg.conn.name - if account in self.disabled_accounts: - return - - omemo_encrypted_tag = msg.msg_.getTag('encrypted', namespace=NS_OMEMO) - if omemo_encrypted_tag: - log.debug(account + ' => OMEMO MAM msg received') - - state = self.get_omemo_state(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 = state.decrypt_msg(msg_dict) - - if not plaintext: - msg.encrypted = 'drop' - return - - self.print_msg_to_log(msg.msg_) - - msg.msgtxt = plaintext - msg.encrypted = self.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 - """ - account = msg.conn.name - if account in self.disabled_accounts: - return - - if msg.stanza.getTag('encrypted', namespace=NS_OMEMO): - log.debug(account + ' => OMEMO msg received') - - state = self.get_omemo_state(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=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 = state.store. \ - getJidFromDevice(msg_dict['sid']) - if not from_jid: - log.error(account + - ' => Cant decrypt GroupChat Message ' - 'from ' + 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'] == state.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(account + ' => Cant decrypt own GroupChat ' - 'Message') - msg.encrypted = 'drop' - return - else: - msg_dict['sender_jid'] = app. \ - get_jid_without_resource(from_jid) - plaintext = state.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.encryption_name - - def room_memberlist_received(self, event): - account = event.conn.name - if account in self.disabled_accounts: - 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: ' + jid) - - def gc_presence_received(self, event): - account = event.conn.name - if account in self.disabled_accounts: - 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: ' + 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) - - app.connections[account].get_affiliation_list(room, 'owner') - app.connections[account].get_affiliation_list(room, 'admin') - app.connections[account].get_affiliation_list(room, 'member') - - def gc_config_changed_received(self, event): - account = event.conn.name - room = event.room_jid - if account in self.disabled_accounts: - return - 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 - ---------- - event : StanzaMessageOutgoingEvent - - Returns - ------- - Return if encryption is not activated or any other - exception or error occurs - """ - account = event.conn.name - try: - if account in self.disabled_accounts: - raise OMEMOError('Account disabled in OMEMO config') - - self.cleanup_stanza(event) - - if not event.message: - callback(event) - return - - state = self.get_omemo_state(account) - to_jid = app.get_jid_without_resource(event.jid) - own_jid = app.get_jid_from_account(account) - - msg_dict = state.create_gc_msg( - 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 - - The body is getting encrypted - - Parameters - ---------- - event : StanzaMessageOutgoingEvent - - Returns - ------- - Return if encryption is not activated or any other - exception or error occurs - """ - account = event.conn.name - try: - if account in self.disabled_accounts: - raise OMEMOError('Account disabled in OMEMO config') - - self.cleanup_stanza(event) - - if not event.message: - callback(event) - return - - state = self.get_omemo_state(account) - to_jid = app.get_jid_without_resource(event.jid) - own_jid = app.get_jid_from_account(account) - - plaintext = event.message.encode('utf8') - msg_dict = state.create_msg(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.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 - - - See also - -------- - 4.2 Discovering peer support - http://conversations.im/xeps/multi-end.html#usecases-discovering - """ - - account = event.conn.name - if account in self.disabled_accounts: - return False - - if event.pep_type != 'headline': - 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(account + - ' => Received empty or invalid Devicelist from: ' + - contact_jid) - return False - - state = self.get_omemo_state(account) - my_jid = app.get_jid_from_account(account) - - if contact_jid == my_jid: - log.info(account + ' => Received own device list:' + str( - devices_list)) - state.set_own_devices(devices_list) - state.store.sessionStore.setActiveState(devices_list, my_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 state.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(account) - else: - log.info(account + ' => Received device list for ' + - contact_jid + ':' + str(devices_list)) - state.set_devices(contact_jid, devices_list) - state.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, account, new=False): - """ Get all currently known own active device ids and publish them - - Parameters - ---------- - account : str - the account name - - new : bool - if True, a devicelist with only one - (the current id of this instance) device id is pushed - """ - state = self.get_omemo_state(account) - if new: - devices_list = [state.own_device_id] - else: - devices_list = state.own_devices - devices_list.append(state.own_device_id) - devices_list = list(set(devices_list)) - state.set_own_devices(devices_list) - - log.debug(account + ' => Publishing own Devices: ' + str( - devices_list)) - iq = DeviceListAnnouncement(devices_list) - app.connections[account].connection.send(iq) - id_ = str(iq.getAttr('id')) - IQ_CALLBACK[id_] = lambda event: log.debug(event) - - def are_keys_missing(self, account, contact_jid): - """ Checks if devicekeys are missing and querys the - bundles - - Parameters - ---------- - account : str - the account name - contact_jid : str - bare jid of the contact - - Returns - ------- - bool - Returns True if there are no trusted Fingerprints - """ - state = self.get_omemo_state(account) - my_jid = app.get_jid_from_account(account) - - # Fetch Bundles of own other Devices - if my_jid not in self.query_for_bundles: - - devices_without_session = state \ - .devices_without_sessions(my_jid) - - self.query_for_bundles.append(my_jid) - - if devices_without_session: - for device_id in devices_without_session: - self.fetch_device_bundle_information(account, my_jid, - device_id) - - # Fetch Bundles of contacts devices - if contact_jid not in self.query_for_bundles: - - devices_without_session = state \ - .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(account, contact_jid, - device_id) - - if state.getTrustedFingerprints(contact_jid): - return False - return True - - @staticmethod - def handle_iq_received(event): - """ Method called when an IQ is received - - Parameters - ---------- - event : RawIqReceived - """ - id_ = str(event.stanza.getAttr("id")) - if id_ in IQ_CALLBACK: - try: - IQ_CALLBACK[id_](event.stanza) - except: - raise - finally: - del IQ_CALLBACK[id_] - - def fetch_device_bundle_information(self, account, jid, device_id): - """ Fetch bundle information for specified jid, key, and create axolotl - session on success. - - Parameters - ---------- - account : str - The account name - jid : str - The jid to query for bundle information - device_id : int - The device_id for which we are missing an axolotl session - """ - log.info(account + ' => Fetch bundle device ' + str(device_id) + - '#' + jid) - iq = BundleInformationQuery(jid, device_id) - iq_id = str(iq.getAttr('id')) - IQ_CALLBACK[iq_id] = \ - lambda stanza: self.session_from_prekey_bundle(account, - stanza, jid, - device_id) - app.connections[account].connection.send(iq) - - def session_from_prekey_bundle(self, account, stanza, - recipient_id, 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. - - See also - -------- - - 4.4 Building a session: - http://conversations.im/xeps/multi-end.html#usecases-building - - Parameters: - ----------- - account : str - The account name - stanza - The stanza object received from callback - recipient_id : str - The recipient jid - device_id : int - The device_id for which the bundle was queried - - """ - state = self.get_omemo_state(account) - bundle_dict = unpack_device_bundle(stanza, device_id) - if not bundle_dict: - log.warning('Failed to build Session with ' + recipient_id) - return - - if state.build_session(recipient_id, device_id, bundle_dict): - log.info(account + ' => session created for: ' + recipient_id) - # Trigger dialog to trust new Fingerprints if - # the Chat Window is Open - ctrl = app.interface.msg_win_mgr.get_control( - recipient_id, account) - if ctrl: - self.new_fingerprints_available(ctrl) - - def query_own_devicelist(self, account): - """ Query own devicelist from the server. - - Parameters - ---------- - account : str - the account name - """ - my_jid = app.get_jid_from_account(account) - iq = DevicelistQuery(my_jid) - app.connections[account].connection.send(iq) - log.info(account + ' => Querry own devicelist ...') - id_ = str(iq.getAttr("id")) - IQ_CALLBACK[id_] = lambda stanza: \ - self.handle_devicelist_result(account, stanza) - - def publish_bundle(self, account): - """ Publish our bundle information to the PEP node. - - Parameters - ---------- - account : str - the account name - - See also - -------- - 4.3 Announcing bundle information: - http://conversations.im/xeps/multi-end.html#usecases-announcing - """ - state = self.get_omemo_state(account) - iq = BundleInformationAnnouncement(state.bundle, state.own_device_id) - app.connections[account].connection.send(iq) - id_ = str(iq.getAttr("id")) - log.info(account + " => Publishing bundle ...") - IQ_CALLBACK[id_] = lambda stanza: \ - self.handle_publish_result(account, stanza) - - @staticmethod - def handle_publish_result(account, stanza): - """ Log if publishing our bundle was successful - - Parameters - ---------- - account : str - the account name - stanza - The stanza object received from callback - """ - if successful(stanza): - log.info(account + ' => Publishing bundle was successful') - else: - log.error(account + ' => Publishing bundle was NOT successful') - - def handle_devicelist_result(self, account, stanza): - """ If query was successful add own device to the list. - - Parameters - ---------- - account : str - the account name - stanza - The stanza object received from callback - """ - - my_jid = app.get_jid_from_account(account) - state = self.get_omemo_state(account) - - if successful(stanza): - devices_list = list(set(unpack_device_list_update(stanza, account))) - if not devices_list: - log.error(account + ' => Devicelistquery was NOT successful') - self.publish_own_devices_list(account, new=True) - return False - contact_jid = stanza.getAttr('from') - if isinstance(contact_jid, nbxmpp.JID): - contact_jid = str(contact_jid) - if contact_jid == my_jid: - state.set_own_devices(devices_list) - state.store.sessionStore.setActiveState(devices_list, my_jid) - log.info(account + ' => Devicelistquery was successful') - # 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 state.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(account) - else: - log.error(account + ' => Devicelistquery was NOT successful') - self.publish_own_devices_list(account, new=True) - - def clear_device_list(self, account): - """ Clears the local devicelist of our own devices and publishes - a new one including only the current ID of this device - - Parameters - ---------- - account : str - the account name - """ - connection = app.connections[account].connection - if not connection: - return - state = self.get_omemo_state(account) - devices_list = [state.own_device_id] - state.set_own_devices(devices_list) - - log.info(account + ' => Clearing devices_list ' + str(devices_list)) - iq = DeviceListAnnouncement(devices_list) - connection.send(iq) - id_ = str(iq.getAttr('id')) - IQ_CALLBACK[id_] = lambda event: log.info(event) - - @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) + omemo.store.setShownFingerprints(fingerprints) @staticmethod def no_trusted_fingerprints_warning(chat_control): msg = "To send an encrypted message, you have to " \ "first trust the fingerprint of your contact!" chat_control.print_conversation_line(msg, 'status', '', None) - - def omemo_enable_for(self, jid, account): - """ Used by the UI to enable OMEMO for a specified contact. - - To activate OMEMO check first if a Ui Object exists for the - Contact. If it exists use Ui.activate_omemo(). Only if there - is no Ui Object for the contact this method is to be used. - - Parameters - ---------- - jid : str - bare jid - account : str - the account name - """ - state = self.get_omemo_state(account) - state.encryption.activate(jid) - - def omemo_disable_for(self, jid, account): - """ Used by the UI to disable OMEMO for a specified contact. - - WARNING - OMEMO should only be disabled through - User interaction with the UI. - - Parameters - ---------- - jid : str - bare jid - account : str - the account name - """ - state = self.get_omemo_state(account) - state.encryption.deactivate(jid) - - -class OMEMOError(Exception): - pass diff --git a/omemo/ui.py b/omemo/ui.py index fe63ffa..0e5a122 100644 --- a/omemo/ui.py +++ b/omemo/ui.py @@ -174,7 +174,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): active = self.B.get_object('account_combobox').get_active()
account = self.account_store[active][0]
- state = self.plugin.get_omemo_state(account)
+ state = self.plugin.get_omemo(account)
mod, paths = self.fpr_view.get_selection().get_selected_rows()
@@ -202,7 +202,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): active = self.B.get_object('account_combobox').get_active()
account = self.account_store[active][0]
- state = self.plugin.get_omemo_state(account)
+ state = self.plugin.get_omemo(account)
mod, paths = self.fpr_view.get_selection().get_selected_rows()
@@ -311,7 +311,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): self.B.get_object('cleardevice_button').set_sensitive(True)
# Set FPR Label and DeviceID
- state = self.plugin.get_omemo_state(account)
+ state = self.plugin.get_omemo(account)
deviceid = state.own_device_id
self.B.get_object('ID').set_markup('<tt>%s</tt>' % deviceid)
@@ -373,7 +373,7 @@ class FingerprintWindow(Gtk.Dialog): self.windowinstances = windowinstances
self.account = self.contact.account.name
self.plugin = plugin
- self.omemostate = self.plugin.get_omemo_state(self.account)
+ self.omemostate = self.plugin.get_omemo(self.account)
self.own_jid = app.get_jid_from_account(self.account)
Gtk.Dialog.__init__(self,
title=('Fingerprints for %s') % contact.jid,
diff --git a/omemo/xmpp.py b/omemo/xmpp.py index 48fcf3c..061d6d8 100644 --- a/omemo/xmpp.py +++ b/omemo/xmpp.py @@ -39,6 +39,7 @@ NS_OMEMO = 'eu.siacs.conversations.axolotl' NS_DEVICE_LIST = NS_OMEMO + '.devicelist'
NS_NOTIFY = NS_DEVICE_LIST + '+notify'
NS_BUNDLES = NS_OMEMO + '.bundles:'
+NS_HINTS = 'urn:xmpp:hints'
log = logging.getLogger('gajim.plugin_system.omemo')
@@ -149,7 +150,7 @@ class DevicelistQuery(Iq): class DevicelistPEP(AbstractPEP):
- type_ = 'headline'
+ type_ = 'omemo-devicelist'
namespace = NS_DEVICE_LIST
def _extract_info(self, items):
|