diff --git a/omemo/CHANGELOG b/omemo/CHANGELOG index 1e588f03de3362d02426adbd68e6d25b1ad366ae..9e332dde132a37ab6f58614d0d5461647f1ea6a4 100644 --- a/omemo/CHANGELOG +++ b/omemo/CHANGELOG @@ -1,3 +1,8 @@ +0.9.5 / 2016-10-10 +- Add GroupChat BETA +- Add Option to delete Fingerprints +- Add Option to deactivate Accounts for OMEMO + 0.9.0 / 2016-08-28 - Send INFO message to resources who dont support OMEMO - Check dependencys and give correct error message diff --git a/omemo/README.md b/omemo/README.md index 7196020f9de64a12aefb289d56faabafa6c510af..8b1095cf40dfc3fbe20f3a61a795e333dac94159 100644 --- a/omemo/README.md +++ b/omemo/README.md @@ -57,6 +57,13 @@ you have to trust at least **one** fingerprint to send messages. you can receive messages from fingerprints where you didnt made a trust decision, but you cant receive Messages from *not trusted* fingerprints +## Filetransfer + +For Filetransfer use the **httpupload** plugin. + +For decrypting and showing pictures in chat use the **url_image_preview** plugin. + +If you want to use these plugins together with *OMEMO* you have to install the `python-cryptography` package ## Debugging To see OMEMO related debug output start Gajim with the parameter `-l diff --git a/omemo/__init__.py b/omemo/__init__.py index 50dd68f20d9f8a11d0b1614900b3b29afefa01e0..943bc231e0b68d9a21209249161645e7d3819176 100644 --- a/omemo/__init__.py +++ b/omemo/__init__.py @@ -21,14 +21,15 @@ import logging import os import sqlite3 -import os +import shutil +import message_control -from common import caps_cache, gajim, ged +from common import caps_cache, gajim, ged, configpaths from common.pep import SUPPORTED_PERSONAL_USER_EVENTS from plugins import GajimPlugin from plugins.helpers import log_calls from nbxmpp.simplexml import Node -from nbxmpp import NS_CORRECT +from nbxmpp import NS_CORRECT, NS_ADDRESS from . import ui from .ui import Ui @@ -40,7 +41,7 @@ from .xmpp import ( from common import demandimport demandimport.enable() -demandimport.ignore += ['_imp'] +demandimport.ignore += ['_imp', '_thread', 'axolotl'] IQ_CALLBACK = {} @@ -54,42 +55,51 @@ ERROR_MSG = '' NS_HINTS = 'urn:xmpp:hints' NS_PGP = 'urn:xmpp:openpgp:0' -DB_DIR = gajim.gajimpaths.data_root +DB_DIR_OLD = gajim.gajimpaths.data_root +DB_DIR_NEW = configpaths.gajimpaths['MY_DATA'] log = logging.getLogger('gajim.plugin_system.omemo') try: - from .omemo.state import OmemoState -except Exception as e: - log.error(e) - ERROR_MSG = 'Error: ' + str(e) - -try: - import google.protobuf + prototest = __import__('google.protobuf') except Exception as e: log.error(e) ERROR_MSG = PROTOBUF_MISSING try: - import axolotl + axolotltest = __import__('axolotl') except Exception as e: log.error(e) ERROR_MSG = AXOLOTL_MISSING +if not ERROR_MSG: + try: + from .omemo.state import OmemoState + except Exception as e: + log.error(e) + ERROR_MSG = 'Error: ' + str(e) + GAJIM_VER = gajim.config.get('version') +GROUPCHAT = False if os.name != 'nt': try: SETUPTOOLS_MISSING = False - from pkg_resources import parse_version + pkg = __import__('pkg_resources') except Exception as e: log.error(e) SETUPTOOLS_MISSING = True ERROR_MSG = 'You are missing the Setuptools package.' if not SETUPTOOLS_MISSING: - if parse_version(GAJIM_VER) < parse_version('0.16.5'): + if pkg.parse_version(GAJIM_VER) < pkg.parse_version('0.16.5'): ERROR_MSG = GAJIM_VERSION + if pkg.parse_version(GAJIM_VER) > pkg.parse_version('0.16.5'): + GROUPCHAT = True +else: + # if GAJIM_VER < 0.16.5, the Plugin fails on missing dependencys earlier + if not GAJIM_VER == '0.16.5': + GROUPCHAT = True # pylint: disable=no-init # pylint: disable=attribute-defined-outside-init @@ -99,6 +109,8 @@ class OmemoPlugin(GajimPlugin): omemo_states = {} ui_list = {} + groupchat = {} + temp_groupchat = {} @log_calls('OmemoPlugin') def init(self): @@ -116,15 +128,48 @@ class OmemoPlugin(GajimPlugin): 'stanza-message-outgoing': (ged.PRECORE, self.handle_outgoing_stanza), 'message-outgoing': - (ged.PRECORE, self.handle_outgoing_event), - } + (ged.PRECORE, self.handle_outgoing_event)} + if GROUPCHAT: + self.events_handlers['gc-stanza-message-outgoing'] =\ + (ged.PRECORE, self.handle_outgoing_gc_stanza) + self.events_handlers['gc-presence-received'] =\ + (ged.PRECORE, self.gc_presence_received) + self.events_handlers['gc-config-changed-received'] =\ + (ged.PRECORE, self.gc_config_changed_received) + self.events_handlers['muc-admin-received'] =\ + (ged.PRECORE, self.room_memberlist_received) + self.config_dialog = ui.OMEMOConfigDialog(self) self.gui_extension_points = {'chat_control': (self.connect_ui, - self.disconnect_ui)} + self.disconnect_ui), + 'groupchat_control': (self.connect_ui, + self.disconnect_ui)} SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP) self.plugin = self self.announced = [] self.query_for_bundles = [] + self.disabled_accounts = [] + self.gc_message = {} + + self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), } + + for account in self.plugin.config['DISABLED_ACCOUNTS']: + self.disabled_accounts.append(account) + + 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 @log_calls('OmemoPlugin') def get_omemo_state(self, account): @@ -140,13 +185,14 @@ class OmemoPlugin(GajimPlugin): ------- OmemoState """ + if account in self.disabled_accounts: + return if account not in self.omemo_states: self.deactivate_gajim_e2e(account) - db_path = os.path.join(DB_DIR, 'omemo_' + account + '.db') - conn = sqlite3.connect(db_path, check_same_thread=False) - my_jid = gajim.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.plugin) @@ -170,6 +216,8 @@ class OmemoPlugin(GajimPlugin): event : SignedInEvent """ account = event.conn.name + if account in self.disabled_accounts: + return log.debug(account + ' => Announce Support after Sign In') self.query_for_bundles = [] @@ -183,11 +231,15 @@ class OmemoPlugin(GajimPlugin): """ Method called when the Plugin is activated in the PluginManager """ self.query_for_bundles = [] - if NS_NOTIFY not in gajim.gajim_common_features: - gajim.gajim_common_features.append(NS_NOTIFY) - self._compute_caps_hash() - # Publish bundle information + # Publish bundle information and Entity Caps for account in gajim.connections: + if account in self.disabled_accounts: + log.debug(account + + ' => Account is disabled') + continue + if NS_NOTIFY not in gajim.gajim_optional_features[account]: + gajim.gajim_optional_features[account].append(NS_NOTIFY) + self._compute_caps_hash(account) if account not in self.announced: if gajim.account_is_connected(account): log.debug(account + @@ -202,23 +254,25 @@ class OmemoPlugin(GajimPlugin): Removes OMEMO from the Entity Capabilities list """ - if NS_NOTIFY in gajim.gajim_common_features: - gajim.gajim_common_features.remove(NS_NOTIFY) - self._compute_caps_hash() + for account in gajim.connections: + if account in self.disabled_accounts: + continue + if NS_NOTIFY in gajim.gajim_optional_features[account]: + gajim.gajim_optional_features[account].remove(NS_NOTIFY) + self._compute_caps_hash(account) @staticmethod - def _compute_caps_hash(): + def _compute_caps_hash(account): """ Computes the hash for Entity Capabilities and publishes it """ - for acc in gajim.connections: - gajim.caps_hash[acc] = caps_cache.compute_caps_hash( - [gajim.gajim_identity], - gajim.gajim_common_features + - gajim.gajim_optional_features[acc]) - # re-send presence with new hash - connected = gajim.connections[acc].connected - if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible': - gajim.connections[acc].change_status( - gajim.SHOW_LIST[connected], gajim.connections[acc].status) + gajim.caps_hash[account] = caps_cache.compute_caps_hash( + [gajim.gajim_identity], + gajim.gajim_common_features + + gajim.gajim_optional_features[account]) + # re-send presence with new hash + connected = gajim.connections[account].connected + if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible': + gajim.connections[account].change_status( + gajim.SHOW_LIST[connected], gajim.connections[account].status) @log_calls('OmemoPlugin') def mam_message_received(self, msg): @@ -235,12 +289,15 @@ class OmemoPlugin(GajimPlugin): ------- Return means that the Event is passed on to Gajim """ + account = msg.conn.name + if account in self.disabled_accounts: + return + if msg.msg_.getTag('openpgp', namespace=NS_PGP): return omemo_encrypted_tag = msg.msg_.getTag('encrypted', namespace=NS_OMEMO) if omemo_encrypted_tag: - account = msg.conn.name log.debug(account + ' => OMEMO MAM msg received') state = self.get_omemo_state(account) @@ -293,12 +350,14 @@ class OmemoPlugin(GajimPlugin): ------- 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('openpgp', namespace=NS_PGP): return - if msg.stanza.getTag('encrypted', namespace=NS_OMEMO) and \ - msg.mtype == 'chat': - account = msg.conn.name + if msg.stanza.getTag('encrypted', namespace=NS_OMEMO): log.debug(account + ' => OMEMO msg received') state = self.get_omemo_state(account) @@ -307,27 +366,63 @@ class OmemoPlugin(GajimPlugin): 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)) - msg_dict['sender_jid'] = gajim.get_jid_without_resource(from_jid) - plaintext = state.decrypt_msg(msg_dict) + + 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) + return True + 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') + else: + msg_dict['sender_jid'] = gajim. \ + get_jid_without_resource(from_jid) + plaintext = state.decrypt_msg(msg_dict) if not plaintext: - return + return True msg.msgtxt = plaintext # Gajim bug: there must be a body or the message # gets dropped from history msg.stanza.setBody(plaintext) - contact_jid = gajim.get_jid_without_resource(from_jid) - if account in self.ui_list and \ - contact_jid in self.ui_list[account]: - self.ui_list[account][contact_jid].activate_omemo() + if msg.mtype != 'groupchat': + contact_jid = gajim.get_jid_without_resource(from_jid) + if account in self.ui_list and \ + contact_jid in self.ui_list[account]: + self.ui_list[account][contact_jid].activate_omemo() return False - elif msg.stanza.getTag('body') and msg.mtype == 'chat': + elif msg.stanza.getTag('body'): account = msg.conn.name from_jid = str(msg.stanza.getFrom()) @@ -347,6 +442,157 @@ class OmemoPlugin(GajimPlugin): log.debug('No Ui present for ' + jid + ', Ui Warning not shown') + 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 + + 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) + + @log_calls('OmemoPlugin') + 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 = gajim.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) + + gajim.connections[account].get_affiliation_list(room, 'owner') + gajim.connections[account].get_affiliation_list(room, 'admin') + gajim.connections[account].get_affiliation_list(room, 'member') + + self.ui_list[account][room].sensitive(True) + + @log_calls('OmemoPlugin') + def gc_config_changed_received(self, event): + account = event.conn.name + if account in self.disabled_accounts: + return + log.debug('CONFIG CHANGE') + log.debug(event.room_jid) + log.debug(event.status_code) + + def handle_outgoing_gc_stanza(self, event): + """ 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 + if account in self.disabled_accounts: + return + try: + if not event.msg_iq.getTag('body'): + return + state = self.get_omemo_state(account) + full_jid = str(event.msg_iq.getAttr('to')) + to_jid = gajim.get_jid_without_resource(full_jid) + if to_jid not in self.groupchat: + return + if not state.encryption.is_active(to_jid): + return + # Delete previous Message out of Correction Message Stanza + if event.msg_iq.getTag('replace', namespace=NS_CORRECT): + event.msg_iq.delChild('encrypted', attrs={'xmlns': NS_OMEMO}) + + plaintext = event.msg_iq.getBody() + msg_dict = state.create_gc_msg( + gajim.get_jid_from_account(account), + to_jid, + plaintext.encode('utf8')) + if not msg_dict: + return True + + self.gc_message[msg_dict['payload']] = plaintext + encrypted_node = OmemoMessage(msg_dict) + event.msg_iq.delChild('body') + event.msg_iq.addChild(node=encrypted_node) + + # XEP-xxxx: Explicit Message Encryption + if not event.msg_iq.getTag('encrypted', attrs={'xmlns': NS_EME}): + eme_node = Node('encrypted', 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) + except Exception as e: + log.debug(e) + return True + @log_calls('OmemoPlugin') def handle_outgoing_event(self, event): """ Handles a message outgoing event @@ -363,6 +609,8 @@ class OmemoPlugin(GajimPlugin): Return if encryption is not activated """ account = event.account + if account in self.disabled_accounts: + return state = self.get_omemo_state(account) if not state.encryption.is_active(event.jid): @@ -385,11 +633,13 @@ class OmemoPlugin(GajimPlugin): Return if encryption is not activated or any other exception or error occurs """ + account = event.conn.name + if account in self.disabled_accounts: + return try: if not event.msg_iq.getTag('body'): return - account = event.conn.name state = self.get_omemo_state(account) full_jid = str(event.msg_iq.getAttr('to')) to_jid = gajim.get_jid_without_resource(full_jid) @@ -463,6 +713,11 @@ class OmemoPlugin(GajimPlugin): 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 @@ -470,7 +725,6 @@ class OmemoPlugin(GajimPlugin): event.conn.name))) if len(devices_list) == 0: return False - account = event.conn.name contact_jid = gajim.get_jid_without_resource(event.fjid) state = self.get_omemo_state(account) my_jid = gajim.get_jid_from_account(account) @@ -557,6 +811,8 @@ class OmemoPlugin(GajimPlugin): Gajim ChatControl object """ account = chat_control.contact.account.name + if account in self.disabled_accounts: + return contact_jid = chat_control.contact.jid if account not in self.ui_list: self.ui_list[account] = {} @@ -577,6 +833,11 @@ class OmemoPlugin(GajimPlugin): else: log.warning(account + " => No devices for " + contact_jid) + if chat_control.type_id == message_control.TYPE_GC: + self.ui_list[account][contact_jid] = Ui(self, chat_control, + omemo_enabled, state) + self.ui_list[account][contact_jid].sensitive(False) + @log_calls('OmemoPlugin') def disconnect_ui(self, chat_control): """ Calls the removeUi method to remove all relatad UI objects. @@ -588,6 +849,8 @@ class OmemoPlugin(GajimPlugin): """ contact_jid = chat_control.contact.jid account = chat_control.contact.account.name + if account in self.disabled_accounts: + return self.ui_list[account][contact_jid].removeUi() def are_keys_missing(self, account, contact_jid): @@ -613,7 +876,7 @@ class OmemoPlugin(GajimPlugin): if my_jid not in self.query_for_bundles: devices_without_session = state \ - .devices_without_sessions(my_jid) + .devices_without_sessions(my_jid) self.query_for_bundles.append(my_jid) diff --git a/omemo/config_dialog.ui b/omemo/config_dialog.ui index 648e4b7f3ee79e1a4e53906ddf45c8ccfe4e894d..fb8e197de856708ee55f64ea9fa0b6cffa852d70 100644 --- a/omemo/config_dialog.ui +++ b/omemo/config_dialog.ui @@ -4,7 +4,7 @@ <!-- interface-naming-policy toplevel-contextual --> <object class="GtkListStore" id="account_store"> <columns> - <!-- column-name accountname --> + <!-- column-name accounts --> <column type="gchararray"/> </columns> </object> @@ -14,6 +14,12 @@ <column type="gchararray"/> </columns> </object> + <object class="GtkListStore" id="disabled_account_store"> + <columns> + <!-- column-name accounts --> + <column type="gchararray"/> + </columns> + </object> <object class="GtkListStore" id="fingerprint_store"> <columns> <!-- column-name id --> @@ -24,6 +30,8 @@ <column type="gchararray"/> <!-- column-name fingerprint --> <column type="gchararray"/> + <!-- column-name deviceid --> + <column type="gint"/> </columns> </object> <object class="GtkNotebook" id="notebook1"> @@ -104,7 +112,6 @@ <object class="GtkLabel" id="fingerprint_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label"><tt>-------- -------- -------- -------- -------- </tt></property> <property name="use_markup">True</property> <property name="selectable">True</property> </object> @@ -147,7 +154,6 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes">0</property> </object> <packing> <property name="expand">False</property> @@ -200,6 +206,9 @@ <object class="GtkTreeViewColumn" id="name_column"> <property name="resizable">True</property> <property name="title">Name</property> + <property name="clickable">True</property> + <property name="sort_indicator">True</property> + <property name="sort_column_id">1</property> <child> <object class="GtkCellRendererText" id="cellrenderertext2"/> <attributes> @@ -261,6 +270,21 @@ <property name="position">0</property> </packing> </child> + <child> + <object class="GtkButton" id="delfprbutton"> + <property name="label" translatable="yes">Delete Fingerprint</property> + <property name="width_request">200</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="delfpr_button_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> </object> <packing> <property name="expand">False</property> @@ -400,6 +424,170 @@ <property name="tab_fill">False</property> </packing> </child> + <child> + <object class="GtkVBox" id="vbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="label6"> + <property name="height_request">30</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">You have to restart Gajim for changes to take effect !</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="foreground" value="#ffff00000000"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="spacing">5</property> + <child> + <object class="GtkVBox" id="vbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="active_accounts_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">account_store</property> + <child> + <object class="GtkTreeViewColumn" id="treeviewcolumn1"> + <property name="title" translatable="yes">Active Accounts</property> + <property name="alignment">0.5</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext5"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="disable_accounts_btn"> + <property name="label" translatable="yes">Disable Account</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="disable_accounts_btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow4"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkTreeView" id="disabled_accounts_view"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="model">disabled_account_store</property> + <child> + <object class="GtkTreeViewColumn" id="treeviewcolumn2"> + <property name="title" translatable="yes">Disabled Accounts</property> + <property name="alignment">0.5</property> + <child> + <object class="GtkCellRendererText" id="cellrenderertext6"/> + <attributes> + <attribute name="text">0</attribute> + </attributes> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="activate_accounts_btn"> + <property name="label" translatable="yes">Activate Account</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="activate_accounts_btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="disable_accounts"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Disable Accounts</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab_fill">False</property> + </packing> + </child> </object> <object class="GtkMenu" id="fprclipboard_menu"> <property name="visible">True</property> diff --git a/omemo/fpr_dialog.ui b/omemo/fpr_dialog.ui index fb2f61ddfbb3ebb1fa71bc12d5528710ac7d5084..b46cb462a13520d34010d74a8fa5825f9cc24223 100644 --- a/omemo/fpr_dialog.ui +++ b/omemo/fpr_dialog.ui @@ -2,12 +2,6 @@ <interface> <requires lib="gtk+" version="2.16"/> <!-- interface-naming-policy toplevel-contextual --> - <object class="GtkListStore" id="account_store"> - <columns> - <!-- column-name accountname --> - <column type="gchararray"/> - </columns> - </object> <object class="GtkListStore" id="fingerprint_store"> <columns> <!-- column-name id --> @@ -18,6 +12,8 @@ <column type="gchararray"/> <!-- column-name fingerprint --> <column type="gchararray"/> + <!-- column-name deviceid --> + <column type="gint"/> </columns> </object> <object class="GtkNotebook" id="notebook1"> @@ -41,7 +37,9 @@ <object class="GtkTreeView" id="fingerprint_view"> <property name="visible">True</property> <property name="can_focus">True</property> + <property name="has_tooltip">True</property> <property name="model">fingerprint_store</property> + <property name="headers_clickable">False</property> <property name="search_column">0</property> <property name="tooltip_column">3</property> <signal name="button-press-event" handler="fpr_button_pressed_cb" swapped="no"/> @@ -49,8 +47,11 @@ <object class="GtkTreeViewColumn" id="name_column"> <property name="resizable">True</property> <property name="title">Name</property> + <property name="clickable">True</property> + <property name="sort_indicator">True</property> + <property name="sort_column_id">1</property> <child> - <object class="GtkCellRendererText" id="cellrenderertext2"/> + <object class="GtkCellRendererText" id="NameCell"/> <attributes> <attribute name="text">1</attribute> </attributes> @@ -62,7 +63,7 @@ <property name="resizable">True</property> <property name="title">Trust</property> <child> - <object class="GtkCellRendererText" id="cellrenderertoggle1"/> + <object class="GtkCellRendererText" id="TrustCell"/> <attributes> <attribute name="text">2</attribute> </attributes> @@ -74,7 +75,7 @@ <property name="resizable">True</property> <property name="title">Fingerprint</property> <child> - <object class="GtkCellRendererText" id="cellrenderertext4"/> + <object class="GtkCellRendererText" id="FingerprintCell"/> <attributes> <attribute name="markup">3</attribute> </attributes> @@ -198,7 +199,7 @@ <property name="resizable">True</property> <property name="title">Name</property> <child> - <object class="GtkCellRendererText" id="cellrenderertext1"/> + <object class="GtkCellRendererText" id="NameCell1"/> <attributes> <attribute name="text">1</attribute> </attributes> @@ -210,7 +211,7 @@ <property name="resizable">True</property> <property name="title">Trust</property> <child> - <object class="GtkCellRendererText" id="cellrenderertoggle2"/> + <object class="GtkCellRendererText" id="TrustCell2"/> <attributes> <attribute name="text">2</attribute> </attributes> @@ -222,7 +223,7 @@ <property name="resizable">True</property> <property name="title">Fingerprint</property> <child> - <object class="GtkCellRendererText" id="cellrenderertext3"/> + <object class="GtkCellRendererText" id="FingerprintCell1"/> <attributes> <attribute name="markup">3</attribute> </attributes> @@ -289,8 +290,6 @@ <object class="GtkMenuItem" id="copyfprclipboard_item"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes" comments="Context menu item">Copy to clipboard</property> - <property name="use_underline">True</property> <signal name="activate" handler="clipboard_button_cb" swapped="no"/> </object> </child> diff --git a/omemo/manifest.ini b/omemo/manifest.ini index 5bf7f33113c5c8d3e9bb16551459c0e6f7f020fd..1a89a7e22ba1e6378690ccabb60d915d7aa3a5cd 100644 --- a/omemo/manifest.ini +++ b/omemo/manifest.ini @@ -1,7 +1,7 @@ [info] name: OMEMO short_name: omemo -version: 0.9.0 +version: 0.9.5 description: OMEMO is an XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption based on Axolotl and PEP. You need to install some dependencys, you can find install instructions for your system in the Github Wiki. authors: Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de> Daniel Gultsch <daniel@gultsch.de> diff --git a/omemo/omemo/liteaxolotlstore.py b/omemo/omemo/liteaxolotlstore.py index 90502979444e50d08453c3ec4a5bb7e9b9ef8954..4ee00d17df18dc19428a7cd4d62423fa3618c718 100644 --- a/omemo/omemo/liteaxolotlstore.py +++ b/omemo/omemo/liteaxolotlstore.py @@ -83,10 +83,16 @@ class LiteAxolotlStore(AxolotlStore): def saveIdentity(self, recepientId, identityKey): self.identityKeyStore.saveIdentity(recepientId, identityKey) + def deleteIdentity(self, recipientId, identityKey): + self.identityKeyStore.deleteIdentity(recipientId, identityKey) + def isTrustedIdentity(self, recepientId, identityKey): return self.identityKeyStore.isTrustedIdentity(recepientId, identityKey) + def setTrust(self, identityKey, trust): + return self.identityKeyStore.setTrust(identityKey, trust) + def getTrustedFingerprints(self, jid): return self.identityKeyStore.getTrustedFingerprints(jid) @@ -127,6 +133,9 @@ class LiteAxolotlStore(AxolotlStore): # TODO Reuse this return self.sessionStore.getSubDeviceSessions(recepientId) + def getJidFromDevice(self, device_id): + return self.sessionStore.getJidFromDevice(device_id) + def storeSession(self, recepientId, deviceId, sessionRecord): self.sessionStore.storeSession(recepientId, deviceId, sessionRecord) @@ -139,6 +148,15 @@ class LiteAxolotlStore(AxolotlStore): def deleteAllSessions(self, recepientId): self.sessionStore.deleteAllSessions(recepientId) + def getSessionsFromJid(self, recipientId): + return self.sessionStore.getSessionsFromJid(recipientId) + + def getSessionsFromJids(self, recipientId): + return self.sessionStore.getSessionsFromJids(recipientId) + + def getAllSessions(self): + return self.sessionStore.getAllSessions() + def loadSignedPreKey(self, signedPreKeyId): return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId) diff --git a/omemo/omemo/liteidentitykeystore.py b/omemo/omemo/liteidentitykeystore.py index d644fa05c4f5edd6d864a7f9cf44b8d2f8b41a37..1e02e638e74f8cdb3fdb04e3ac7fbaac0f1cfac2 100644 --- a/omemo/omemo/liteidentitykeystore.py +++ b/omemo/omemo/liteidentitykeystore.py @@ -86,6 +86,13 @@ class LiteIdentityKeyStore(IdentityKeyStore): return result is not None + def deleteIdentity(self, recipientId, identityKey): + q = "DELETE FROM identities WHERE recipient_id = ? AND public_key = ?" + c = self.dbConn.cursor() + c.execute(q, (recipientId, + identityKey.getPublicKey().serialize())) + self.dbConn.commit() + def isTrustedIdentity(self, recipientId, identityKey): q = "SELECT trust FROM identities WHERE recipient_id = ? " \ "AND public_key = ?" @@ -160,8 +167,8 @@ class LiteIdentityKeyStore(IdentityKeyStore): c.execute(q, fingerprints) self.dbConn.commit() - def setTrust(self, _id, trust): - q = "UPDATE identities SET trust = ? WHERE _id = ?" + def setTrust(self, identityKey, trust): + q = "UPDATE identities SET trust = ? WHERE public_key = ?" c = self.dbConn.cursor() - c.execute(q, (trust, _id)) + c.execute(q, (trust, identityKey.getPublicKey().serialize())) self.dbConn.commit() diff --git a/omemo/omemo/litesessionstore.py b/omemo/omemo/litesessionstore.py index 0de1e5e6fcad3dacf075769239d0196a5a7a2f85..13e469e78607affd3e86600bb1b37414a6a0e3c3 100644 --- a/omemo/omemo/litesessionstore.py +++ b/omemo/omemo/litesessionstore.py @@ -48,6 +48,14 @@ class LiteSessionStore(SessionStore): deviceIds = [r[0] for r in result] return deviceIds + def getJidFromDevice(self, device_id): + q = "SELECT recipient_id from sessions WHERE device_id = ?" + c = self.dbConn.cursor() + c.execute(q, (device_id, )) + result = c.fetchone() + + return result[0] + def getActiveDeviceTuples(self): q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1" c = self.dbConn.cursor() @@ -82,6 +90,33 @@ class LiteSessionStore(SessionStore): self.dbConn.cursor().execute(q, (recipientId, )) self.dbConn.commit() + def getAllSessions(self): + q = "SELECT _id, recipient_id, device_id, record, active from sessions" + c = self.dbConn.cursor() + result = [] + for row in c.execute(q): + result.append((row[0], row[1], row[2], row[3], row[4])) + return result + + def getSessionsFromJid(self, recipientId): + q = "SELECT _id, recipient_id, device_id, record, active from sessions" \ + " WHERE recipient_id = ?" + c = self.dbConn.cursor() + result = [] + for row in c.execute(q, (recipientId,)): + result.append((row[0], row[1], row[2], row[3], row[4])) + return result + + def getSessionsFromJids(self, recipientId): + q = "SELECT _id, recipient_id, device_id, record, active from sessions" \ + " WHERE recipient_id IN ({})" \ + .format(', '.join(['?'] * len(recipientId))) + c = self.dbConn.cursor() + result = [] + for row in c.execute(q, recipientId): + result.append((row[0], row[1], row[2], row[3], row[4])) + return result + def setActiveState(self, deviceList, jid): c = self.dbConn.cursor() @@ -96,28 +131,6 @@ class LiteSessionStore(SessionStore): c.execute(q, deviceList) self.dbConn.commit() - def getActiveSessionsKeys(self, recipientId): - q = "SELECT record FROM sessions WHERE active = 1 AND recipient_id = ?" - c = self.dbConn.cursor() - result = [] - for row in c.execute(q, (recipientId,)): - public_key = (SessionRecord(serialized=row[0]). - getSessionState().getRemoteIdentityKey(). - getPublicKey()) - result.append(public_key.serialize()) - return result - - def getAllActiveSessionsKeys(self): - q = "SELECT record FROM sessions WHERE active = 1" - c = self.dbConn.cursor() - result = [] - for row in c.execute(q): - public_key = (SessionRecord(serialized=row[0]). - getSessionState().getRemoteIdentityKey(). - getPublicKey()) - result.append(public_key.serialize()) - return result - def getInactiveSessionsKeys(self, recipientId): q = "SELECT record FROM sessions WHERE active = 0 AND recipient_id = ?" c = self.dbConn.cursor() diff --git a/omemo/omemo/state.py b/omemo/omemo/state.py index fe79796ebaa8ce92402f16cce5ccb5d09dbf70a1..6db7f1a42f55ac15856b155c79313fab5e7df17f 100644 --- a/omemo/omemo/state.py +++ b/omemo/omemo/state.py @@ -201,7 +201,7 @@ class OmemoState: except (NoSessionException, InvalidMessageException) as e: log.warning('No Session found ' + e.message) log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' + - str(sid)) + str(sid)) return except (DuplicateMessageException) as e: log.warning('Duplicate message found ' + str(e.args)) @@ -226,24 +226,82 @@ class OmemoState: log.error('No known devices') return - for dev in devices_list: - self.get_session_cipher(jid, dev) - session_ciphers = self.session_ciphers[jid] - if not session_ciphers: - log.warning('No session ciphers for ' + jid) - return - # Encrypt the message key with for each of receivers devices - for rid, cipher in session_ciphers.items(): + for device in devices_list: try: - if self.isTrusted(cipher) == TRUSTED: - encrypted_keys[rid] = cipher.encrypt(key).serialize() + if self.isTrusted(jid, device) == TRUSTED: + cipher = self.get_session_cipher(jid, device) + encrypted_keys[device] = cipher.encrypt(key).serialize() else: log.debug('Skipped Device because Trust is: ' + - str(self.isTrusted(cipher))) + str(self.isTrusted(jid, device))) except: - log.warning('Failed to find key for device ' + str(rid)) + log.warning('Failed to find key for device ' + str(device)) + + if len(encrypted_keys) == 0: + log.error('Encrypted keys empty') + raise NoValidSessions('Encrypted keys empty') + + my_other_devices = set(self.own_devices) - set({self.own_device_id}) + # Encrypt the message key with for each of our own devices + for device in my_other_devices: + try: + if self.isTrusted(from_jid, device) == TRUSTED: + cipher = self.get_session_cipher(from_jid, device) + encrypted_keys[device] = cipher.encrypt(key).serialize() + else: + log.debug('Skipped own Device because Trust is: ' + + str(self.isTrusted(from_jid, device))) + except: + log.warning('Failed to find key for device ' + str(device)) + + payload = encrypt(key, iv, plaintext) + + result = {'sid': self.own_device_id, + 'keys': encrypted_keys, + 'jid': jid, + 'iv': iv, + 'payload': payload} + + log.debug('Finished encrypting message') + return result + + def create_gc_msg(self, from_jid, jid, plaintext): + key = get_random_bytes(16) + iv = get_random_bytes(16) + encrypted_keys = {} + room = jid + encrypted_jids = [] + + devices_list = self.device_list_for(jid, True) + + if len(devices_list) == 0: + log.error('No known devices') + return + + for tup in devices_list: + 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] + if jid_to == self.own_jid: + continue + if jid_to in encrypted_jids: # We already encrypted to this JID + continue + for rid, cipher in self.session_ciphers[jid_to].items(): + try: + if self.isTrusted(jid_to, rid) == TRUSTED: + encrypted_keys[rid] = cipher.encrypt(key). \ + serialize() + else: + log.debug('Skipped Device because Trust is: ' + + str(self.isTrusted(jid_to, rid))) + except: + log.exception('ERROR:') + log.warning('Failed to find key for device ' + + str(rid)) + encrypted_jids.append(jid_to) if len(encrypted_keys) == 0: log_msg = 'Encrypted keys empty' log.error(log_msg) @@ -254,12 +312,13 @@ class OmemoState: for dev in my_other_devices: try: cipher = self.get_session_cipher(from_jid, dev) - if self.isTrusted(cipher) == TRUSTED: + if self.isTrusted(from_jid, dev) == TRUSTED: encrypted_keys[dev] = cipher.encrypt(key).serialize() else: log.debug('Skipped own Device because Trust is: ' + - str(self.isTrusted(cipher))) + str(self.isTrusted(from_jid, dev))) except: + log.exception('ERROR:') log.warning('Failed to find key for device ' + str(dev)) payload = encrypt(key, iv, plaintext) @@ -273,14 +332,36 @@ class OmemoState: log.debug('Finished encrypting message') return result - def isTrusted(self, cipher): - self.cipher = cipher - self.state = self.cipher.sessionStore. \ - loadSession(self.cipher.recipientId, self.cipher.deviceId). \ - getSessionState() - self.key = self.state.getRemoteIdentityKey() - return self.store.identityKeyStore. \ - isTrustedIdentity(self.cipher.recipientId, self.key) + def device_list_for(self, jid, gc=False): + """ Return a list of known device ids for the specified jid. + Parameters + ---------- + jid : string + The contacts jid + gc : bool + Groupchat Message + """ + if gc: + room = jid + devicelist = [] + for nick in self.plugin.groupchat[room]: + jid_to = self.plugin.groupchat[room][nick] + if jid_to == self.own_jid: + continue + for device in self.device_ids[jid_to]: + devicelist.append((jid_to, device)) + return devicelist + + if jid == self.own_jid: + return set(self.own_devices) - set({self.own_device_id}) + if jid not in self.device_ids: + return set() + return set(self.device_ids[jid]) + + def isTrusted(self, recipient_id, device_id): + record = self.store.loadSession(recipient_id, device_id) + identity_key = record.getSessionState().getRemoteIdentityKey() + return self.store.isTrustedIdentity(recipient_id, identity_key) def getTrustedFingerprints(self, recipient_id): inactive = self.store.getInactiveSessionsKeys(recipient_id) @@ -296,20 +377,6 @@ class OmemoState: return undecided - def device_list_for(self, jid): - """ Return a list of known device ids for the specified jid. - - Parameters - ---------- - jid : string - The contacts jid - """ - if jid == self.own_jid: - return set(self.own_devices) - set({self.own_device_id}) - if jid not in self.device_ids: - return set() - return set(self.device_ids[jid]) - def devices_without_sessions(self, jid): """ List device_ids for the given jid which have no axolotl session. @@ -364,10 +431,10 @@ class OmemoState: def handleWhisperMessage(self, recipient_id, device_id, key): whisperMessage = WhisperMessage(serialized=key) - sessionCipher = self.get_session_cipher(recipient_id, device_id) log.debug(self.account + " => Received WhisperMessage from " + recipient_id) - if self.isTrusted(sessionCipher) >= TRUSTED: + if self.isTrusted(recipient_id, device_id): + sessionCipher = self.get_session_cipher(recipient_id, device_id) key = sessionCipher.decryptMsg(whisperMessage) return key else: diff --git a/omemo/pkgs/PKGBUILD b/omemo/pkgs/PKGBUILD index 62ead0db7d7c053c0d4aea98d2e9caf341ea577c..fcda983d5c4c7754bd0775b1def3f68e509b1dd2 100644 --- a/omemo/pkgs/PKGBUILD +++ b/omemo/pkgs/PKGBUILD @@ -2,8 +2,8 @@ pkgname=gajim-plugin-omemo _pkgname=gajim-omemo -pkgver=0.8.1 -pkgrel=2 +pkgver=0.9 +pkgrel=1 pkgdesc="Gajim plugin for OMEMO Multi-End Message and Object Encryption." arch=(any) url="https://github.com/omemo/${_pkgname}" @@ -12,7 +12,7 @@ depends=("gajim" "python2-setuptools" "python2-cryptography" "python2-axolotl-gi provides=('gajim-plugin-omemo') conflicts=('gajim-plugin-omemo-git') source=("https://github.com/omemo/${_pkgname}/archive/${pkgver}.tar.gz") -sha512sums=('e9280033fbe111f5010f2e9e8fa32c5b8c0abe308000f9a043a1c5e8215c96f8be434876b1d72cc8d68aed4ddaebe9655c70f9648a2db718cba71d90434fee2e') +sha512sums=('536d0a9e368dadefefba34b02e74194c314eb0fc6343fcbb64390b7e447fb8be0214e921359959f831d0bcfaef09ae6825110ebeea947ac5a5ef3bc73da72541') package() { cd $srcdir/gajim-omemo-${pkgver} diff --git a/omemo/ui.py b/omemo/ui.py index 7701c6b245e7478336a4bb1bab142dc977db227d..180de55daaf75bd033b918990bde3ccfbaac8142 100644 --- a/omemo/ui.py +++ b/omemo/ui.py @@ -23,11 +23,14 @@ import logging import gobject import gtk +import message_control # pylint: disable=import-error import gtkgui_helpers from common import gajim +from dialogs import YesNoDialog from plugins.gui import GajimPluginConfigDialog +from axolotl.state.sessionrecord import SessionRecord # pylint: enable=import-error log = logging.getLogger('gajim.plugin_system.omemo') @@ -106,6 +109,12 @@ class Ui(object): self.account = self.contact.account.name self.windowinstances = {} + self.groupchat = False + if chat_control.type_id == message_control.TYPE_GC: + self.groupchat = True + self.omemo_capable = False + self.room = self.chat_control.room_jid + self.display_omemo_state() self.refresh_auth_lock_icon() @@ -134,6 +143,9 @@ class Ui(object): item.set_image(gtk.image_new_from_file(icon_path)) item.set_submenu(submenu) + if self.groupchat: + item.set_sensitive(self.omemo_capable) + # at index 8 is the separator after the esession encryption entry menu.insert(item, 8) return menu @@ -160,7 +172,38 @@ class Ui(object): log.debug(self.account + ' => Sending Message to ' + self.contact.jid) - self.chat_control.send_message = omemo_send_message + def omemo_send_gc_message(message, xhtml=None, process_commands=True): + self.new_fingerprints_available() + if self.encryption_active(): + missing = True + own_jid = gajim.get_jid_from_account(self.account) + for nick in self.plugin.groupchat[self.room]: + real_jid = self.plugin.groupchat[self.room][nick] + if real_jid == own_jid: + continue + if not self.plugin.are_keys_missing(self.account, + real_jid): + missing = False + if missing: + log.debug(self.account + + ' => No Trusted Fingerprints for ' + + self.room) + self.no_trusted_fingerprints_warning() + else: + self.chat_control.orig_send_message(message, xhtml, + process_commands) + log.debug(self.account + ' => Sending Message to ' + + self.room) + else: + self.chat_control.orig_send_message(message, xhtml, + process_commands) + log.debug(self.account + ' => Sending Message to ' + + self.room) + + if self.groupchat: + self.chat_control.send_message = omemo_send_gc_message + else: + self.chat_control.send_message = omemo_send_message def set_omemo_state(self, enabled): """ @@ -183,6 +226,12 @@ class Ui(object): self.omemobutton.set_omemo_state(enabled) self.display_omemo_state() + def sensitive(self, value): + self.omemobutton.set_sensitive(value) + self.omemo_capable = value + if value: + self.chat_control.prepare_context_menu + def encryption_active(self): return self.state.encryption.is_active(self.contact.jid) @@ -191,16 +240,31 @@ class Ui(object): self.set_omemo_state(True) def new_fingerprints_available(self): - fingerprints = self.state.store.getNewFingerprints(self.contact.jid) - if fingerprints: - self.show_fingerprint_window(fingerprints) + jid = self.contact.jid + if self.groupchat and self.room in self.plugin.groupchat: + for nick in self.plugin.groupchat[self.room]: + real_jid = self.plugin.groupchat[self.room][nick] + fingerprints = self.state.store. \ + getNewFingerprints(real_jid) + if fingerprints: + self.show_fingerprint_window(fingerprints) + elif not self.groupchat: + fingerprints = self.state.store.getNewFingerprints(jid) + if fingerprints: + self.show_fingerprint_window(fingerprints) def show_fingerprint_window(self, fingerprints=None): if 'dialog' not in self.windowinstances: - self.windowinstances['dialog'] = \ - FingerprintWindow(self.plugin, self.contact, - self.chat_control.parent_win.window, - self.windowinstances) + if self.groupchat: + self.windowinstances['dialog'] = \ + FingerprintWindow(self.plugin, self.contact, + self.chat_control.parent_win.window, + self.windowinstances, groupchat=True) + else: + self.windowinstances['dialog'] = \ + FingerprintWindow(self.plugin, self.contact, + self.chat_control.parent_win.window, + self.windowinstances) self.windowinstances['dialog'].show_all() if fingerprints: log.debug(self.account + @@ -226,10 +290,12 @@ class Ui(object): def no_trusted_fingerprints_warning(self): msg = "To send an encrypted message, you have to " \ - "first trust the fingerprint of your contact!" + "first trust the fingerprint of your contact!" self.chat_control.print_conversation_line(msg, 'status', '', None) def refresh_auth_lock_icon(self): + if self.groupchat: + return if self.encryption_active(): if self.state.getUndecidedFingerprints(self.contact.jid): self.chat_control._show_lock_image(True, 'OMEMO', True, True, @@ -257,39 +323,130 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): self.B.set_translation_domain('gajim_plugins') self.B.add_from_file(self.GTK_BUILDER_FILE_PATH) - self.fpr_model = gtk.ListStore(gobject.TYPE_INT, - gobject.TYPE_STRING, - gobject.TYPE_STRING, - gobject.TYPE_STRING) + try: + self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS'] + except KeyError: + self.plugin.config['DISABLED_ACCOUNTS'] = [] + self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS'] - self.device_model = gtk.ListStore(gobject.TYPE_STRING) + log.debug('Disabled Accounts:') + log.debug(self.disabled_accounts) - self.account_store = self.B.get_object('account_store') - - for account in sorted(gajim.contacts.get_accounts()): - self.account_store.append(row=(account,)) + self.fpr_model = self.B.get_object('fingerprint_store') + self.device_model = self.B.get_object('deviceid_store') self.fpr_view = self.B.get_object('fingerprint_view') - self.fpr_view.set_model(self.fpr_model) - self.fpr_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - self.device_view = self.B.get_object('deviceid_view') - self.device_view.set_model(self.device_model) + self.disabled_acc_store = self.B.get_object('disabled_account_store') + self.account_store = self.B.get_object('account_store') - if len(self.account_store) > 0: - self.B.get_object('account_combobox').set_active(0) + self.active_acc_view = self.B.get_object('active_accounts_view') + self.disabled_acc_view = self.B.get_object('disabled_accounts_view') self.child.pack_start(self.B.get_object('notebook1')) self.B.connect_signals(self) + self.plugin_active = False + def on_run(self): - self.update_context_list() - self.account_combobox_changed_cb(self.B.get_object('account_combobox')) + for plugin in gajim.plugin_manager.active_plugins: + log.debug(type(plugin)) + if type(plugin).__name__ == 'OmemoPlugin': + self.plugin_active = True + break + self.update_account_store() + self.update_account_combobox() + self.update_disabled_account_view() + + if len(self.account_store) > 0 and \ + self.plugin_active is True: + self.account_combobox_changed_cb( + self.B.get_object('account_combobox')) + + def is_in_accountstore(self, account): + for row in self.account_store: + if row[0] == account: + return True + return False + + def update_account_store(self): + for account in sorted(gajim.contacts.get_accounts()): + if account not in self.disabled_accounts and \ + not self.is_in_accountstore(account): + self.account_store.append(row=(account,)) + + def update_account_combobox(self): + if self.plugin_active is False: + return + if len(self.account_store) > 0: + self.B.get_object('account_combobox').set_active(0) + else: + self.account_combobox_changed_cb( + self.B.get_object('account_combobox')) def account_combobox_changed_cb(self, box, *args): self.update_context_list() + def update_disabled_account_view(self): + self.disabled_acc_store.clear() + for account in self.disabled_accounts: + self.disabled_acc_store.append(row=(account,)) + + def activate_accounts_btn_clicked(self, button, *args): + mod, paths = self.disabled_acc_view.get_selection().get_selected_rows() + for path in paths: + it = mod.get_iter(path) + account = mod.get(it, 0) + if account[0] in self.disabled_accounts and \ + not self.is_in_accountstore(account[0]): + self.account_store.append(row=(account[0],)) + self.disabled_accounts.remove(account[0]) + self.update_disabled_account_view() + self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts + self.update_account_combobox() + + def disable_accounts_btn_clicked(self, button, *args): + mod, paths = self.active_acc_view.get_selection().get_selected_rows() + for path in paths: + it = mod.get_iter(path) + account = mod.get(it, 0) + if account[0] not in self.disabled_accounts and \ + self.is_in_accountstore(account[0]): + self.disabled_accounts.append(account[0]) + self.account_store.remove(it) + self.update_disabled_account_view() + self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts + self.update_account_combobox() + + def delfpr_button_clicked(self, button, *args): + active = self.B.get_object('account_combobox').get_active() + account = self.account_store[active][0] + + state = self.plugin.get_omemo_state(account) + + mod, paths = self.fpr_view.get_selection().get_selected_rows() + + def on_yes(checked): + record = state.store.loadSession(jid, deviceid) + identity_key = record.getSessionState().getRemoteIdentityKey() + + state.store.deleteSession(jid, deviceid) + state.store.deleteIdentity(jid, identity_key) + self.update_context_list() + + for path in paths: + it = mod.get_iter(path) + jid, fpr, deviceid = mod.get(it, 1, 3, 4) + fpr = fpr[31:-12] + + YesNoDialog( + 'Delete Fingerprint?', + 'Do you want to delete the ' + 'fingerprint of <b>{}</b> on your account <b>{}</b>?' + '\n\n<tt>{}</tt>'.format(jid, account, fpr), + on_response_yes=on_yes, transient_for=self) + def trust_button_clicked_cb(self, button, *args): active = self.B.get_object('account_combobox').get_active() account = self.account_store[active][0] @@ -298,41 +455,42 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): mod, paths = self.fpr_view.get_selection().get_selected_rows() + def on_yes(checked, identity_key): + state.store.setTrust(identity_key, TRUSTED) + try: + if self.plugin.ui_list[account]: + self.plugin.ui_list[account][jid]. \ + refresh_auth_lock_icon() + except: + log.debug('UI not available') + self.update_context_list() + + def on_no(identity_key): + state.store.setTrust(identity_key, UNTRUSTED) + try: + if jid in self.plugin.ui_list[account]: + self.plugin.ui_list[account][jid]. \ + refresh_auth_lock_icon() + except: + log.debug('UI not available') + self.update_context_list() + for path in paths: it = mod.get_iter(path) - _id, user, fpr = mod.get(it, 0, 1, 3) + jid, fpr, deviceid = mod.get(it, 1, 3, 4) fpr = fpr[31:-12] - dlg = gtk.Dialog('Trust / Revoke Fingerprint', self, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_YES, gtk.RESPONSE_YES, - gtk.STOCK_NO, gtk.RESPONSE_NO)) - l = gtk.Label() - l.set_markup('Do you want to trust the ' - 'fingerprint of <b>%s</b> on your account <b>%s</b>?' - '\n\n<tt>%s</tt>' % (user, account, fpr)) - l.set_line_wrap(True) - l.set_padding(12, 12) - dlg.vbox.pack_start(l) - dlg.show_all() - - response = dlg.run() - if response == gtk.RESPONSE_YES: - state.store.identityKeyStore.setTrust(_id, TRUSTED) - try: - if self.plugin.ui_list[account]: - self.plugin.ui_list[account][user].refresh_auth_lock_icon() - except: - dlg.destroy() - else: - if response == gtk.RESPONSE_NO: - state.store.identityKeyStore.setTrust(_id, UNTRUSTED) - try: - if user in self.plugin.ui_list[account]: - self.plugin.ui_list[account][user].refresh_auth_lock_icon() - except: - dlg.destroy() - self.update_context_list() + record = state.store.loadSession(jid, deviceid) + identity_key = record.getSessionState().getRemoteIdentityKey() + + YesNoDialog( + 'Trust / Revoke Fingerprint?', + 'Do you want to trust the fingerprint of <b>{}</b> ' + 'on your account <b>{}</b>?\n\n' + '<tt>{}</tt>'.format(jid, account, fpr), + on_response_yes=(on_yes, identity_key), + on_response_no=(on_no, identity_key), + transient_for=self) def cleardevice_button_clicked_cb(self, button, *args): active = self.B.get_object('account_combobox').get_active() @@ -376,68 +534,84 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): gtk.Clipboard(selection='PRIMARY').set_text('\n'.join(fprs)) def update_context_list(self): + log.debug('update_context_list') self.fpr_model.clear() self.device_model.clear() + if len(self.account_store) == 0: + self.B.get_object('ID').set_markup('') + self.B.get_object('fingerprint_label').set_markup('') + self.B.get_object('trust_button').set_sensitive(False) + self.B.get_object('delfprbutton').set_sensitive(False) + self.B.get_object('refresh').set_sensitive(False) + self.B.get_object('cleardevice_button').set_sensitive(False) + return active = self.B.get_object('account_combobox').get_active() account = self.account_store[active][0] - state = self.plugin.get_omemo_state(account) + # Set buttons active + self.B.get_object('trust_button').set_sensitive(True) + self.B.get_object('delfprbutton').set_sensitive(True) + self.B.get_object('refresh').set_sensitive(True) + if account == 'Local': + self.B.get_object('cleardevice_button').set_sensitive(False) + else: + self.B.get_object('cleardevice_button').set_sensitive(True) + + # Set FPR Label and DeviceID + state = self.plugin.get_omemo_state(account) deviceid = state.own_device_id self.B.get_object('ID').set_markup('<tt>%s</tt>' % deviceid) ownfpr = binascii.hexlify(state.store.getIdentityKeyPair() .getPublicKey().serialize()) - ownfpr = self.human_hash(ownfpr[2:]) + ownfpr = human_hash(ownfpr[2:]) self.B.get_object('fingerprint_label').set_markup('<tt>%s</tt>' % ownfpr) - fprDB = state.store.identityKeyStore.getAllFingerprints() - activeSessions = state.store.sessionStore. \ - getAllActiveSessionsKeys() - for item in fprDB: - _id, jid, fpr, tr = item - active = fpr in activeSessions - fpr = binascii.hexlify(fpr) - fpr = self.human_hash(fpr[2:]) - if tr == UNTRUSTED: - if active: - self.fpr_model.append((_id, jid, 'False', - '<tt><span foreground="#FF0040">%s</span></tt>' % fpr)) - else: - self.fpr_model.append((_id, jid, 'False', - '<tt><span foreground="#585858">%s</span></tt>' % fpr)) - elif tr == TRUSTED: - if active: - self.fpr_model.append((_id, jid, 'True', - '<tt><span foreground="#2EFE2E">%s</span></tt>' % fpr)) - else: - self.fpr_model.append((_id, jid, 'True', - '<tt><span foreground="#585858">%s</span></tt>' % fpr)) - else: - if active: - self.fpr_model.append((_id, jid, 'Undecided', - '<tt><span foreground="#FF8000">%s</span></tt>' % fpr)) - else: - self.fpr_model.append((_id, jid, 'Undecided', - '<tt><span foreground="#585858">%s</span></tt>' % fpr)) + # Set Fingerprint List + trust_str = {0: 'False', 1: 'True', 2: 'Undecided'} + session_db = state.store.getAllSessions() + + for item in session_db: + color = {0: '#FF0040', # red + 1: '#2EFE2E', # green + 2: '#FF8000'} # orange + + _id, jid, deviceid, record, active = item + + active = bool(active) + + identity_key = SessionRecord(serialized=record). \ + getSessionState().getRemoteIdentityKey() + fpr = binascii.hexlify(identity_key.getPublicKey().serialize()) + fpr = human_hash(fpr[2:]) + + trust = state.store.isTrustedIdentity(jid, identity_key) + + if not active: + color[trust] = '#585858' # grey + self.fpr_model.append( + (_id, jid, trust_str[trust], + '<tt><span foreground="{}">{}</span></tt>'. + format(color[trust], fpr), + deviceid)) + + # Set Device ID List for item in state.own_devices: self.device_model.append([item]) - def human_hash(self, fpr): - fpr = fpr.upper() - fplen = len(fpr) - wordsize = fplen // 8 - buf = '' - for w in range(0, fplen, wordsize): - buf += '{0} '.format(fpr[w:w + wordsize]) - return buf.rstrip() - class FingerprintWindow(gtk.Dialog): - def __init__(self, plugin, contact, parent, windowinstances): + def __init__(self, plugin, contact, parent, windowinstances, + groupchat=False): + self.groupchat = groupchat self.contact = contact self.windowinstances = windowinstances + self.account = self.contact.account.name + self.plugin = plugin + self.omemostate = self.plugin.get_omemo_state(self.account) + self.own_jid = gajim.get_jid_from_account(self.account) gtk.Dialog.__init__(self, title=('Fingerprints for %s') % contact.jid, parent=parent, @@ -445,44 +619,33 @@ class FingerprintWindow(gtk.Dialog): close_button = self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE) close_button.connect('clicked', self.on_close_button_clicked) self.connect('delete-event', self.on_window_delete) - self.plugin = plugin + self.GTK_BUILDER_FILE_PATH = \ self.plugin.local_file_path('fpr_dialog.ui') - self.B = gtk.Builder() - self.B.set_translation_domain('gajim_plugins') - self.B.add_from_file(self.GTK_BUILDER_FILE_PATH) + self.xml = gtk.Builder() + self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH) + self.xml.set_translation_domain('gajim_plugins') - self.fpr_model = gtk.ListStore(gobject.TYPE_INT, - gobject.TYPE_STRING, - gobject.TYPE_STRING, - gobject.TYPE_STRING) + self.fpr_model = self.xml.get_object('fingerprint_store') - self.fpr_view = self.B.get_object('fingerprint_view') - self.fpr_view.set_model(self.fpr_model) - self.fpr_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - - self.fpr_view_own = self.B.get_object('fingerprint_view_own') - self.fpr_view_own.set_model(self.fpr_model) - self.fpr_view_own.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - - self.notebook = self.B.get_object('notebook1') + self.fpr_view = self.xml.get_object('fingerprint_view') + self.fpr_view_own = self.xml.get_object('fingerprint_view_own') + self.notebook = self.xml.get_object('notebook1') self.child.pack_start(self.notebook) - self.B.connect_signals(self) - - self.account = self.contact.account.name - self.omemostate = self.plugin.get_omemo_state(self.account) + self.xml.connect_signals(self) + # Set own Fingerprint Label ownfpr = binascii.hexlify(self.omemostate.store.getIdentityKeyPair() .getPublicKey().serialize()) - ownfpr = self.human_hash(ownfpr[2:]) - - self.B.get_object('fingerprint_label_own').set_markup('<tt>%s</tt>' - % ownfpr) - + ownfpr = human_hash(ownfpr[2:]) + self.xml.get_object('fingerprint_label_own').set_markup('<tt>%s</tt>' + % ownfpr) self.update_context_list() + self.show_all() + def on_close_button_clicked(self, widget): del self.windowinstances['dialog'] self.hide() @@ -492,41 +655,43 @@ class FingerprintWindow(gtk.Dialog): self.hide() def trust_button_clicked_cb(self, button, *args): + state = self.omemostate + if self.notebook.get_current_page() == 1: mod, paths = self.fpr_view_own.get_selection().get_selected_rows() else: mod, paths = self.fpr_view.get_selection().get_selected_rows() + def on_yes(checked, identity_key): + state.store.setTrust(identity_key, TRUSTED) + if not self.groupchat: + self.plugin.ui_list[self.account][self.contact.jid]. \ + refresh_auth_lock_icon() + self.update_context_list() + + def on_no(identity_key): + state.store.setTrust(identity_key, UNTRUSTED) + if not self.groupchat: + self.plugin.ui_list[self.account][self.contact.jid]. \ + refresh_auth_lock_icon() + self.update_context_list() + for path in paths: it = mod.get_iter(path) - _id, user, fpr = mod.get(it, 0, 1, 3) + jid, fpr, deviceid = mod.get(it, 1, 3, 4) fpr = fpr[31:-12] - dlg = gtk.Dialog('Trust / Revoke Fingerprint', self, - gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, - (gtk.STOCK_YES, gtk.RESPONSE_YES, - gtk.STOCK_NO, gtk.RESPONSE_NO)) - l = gtk.Label() - l.set_markup('Do you want to trust the ' - 'fingerprint of <b>%s</b> on your account <b>%s</b>?' - '\n\n<tt>%s</tt>' % (user, self.account, fpr)) - l.set_line_wrap(True) - l.set_padding(12, 12) - dlg.vbox.pack_start(l) - dlg.show_all() - response = dlg.run() - if response == gtk.RESPONSE_YES: - self.omemostate.store.identityKeyStore.setTrust(_id, TRUSTED) - self.plugin.ui_list[self.account][self.contact.jid]. \ - refresh_auth_lock_icon() - dlg.destroy() - else: - if response == gtk.RESPONSE_NO: - self.omemostate.store.identityKeyStore.setTrust(_id, UNTRUSTED) - self.plugin.ui_list[self.account][self.contact.jid]. \ - refresh_auth_lock_icon() - dlg.destroy() - self.update_context_list() + record = state.store.loadSession(jid, deviceid) + identity_key = record.getSessionState().getRemoteIdentityKey() + + YesNoDialog( + 'Trust / Revoke Fingerprint?', + 'Do you want to trust the fingerprint of <b>{}</b> ' + 'on your account <b>{}</b>?\n\n' + '<tt>{}</tt>'.format(jid, self.account, fpr), + on_response_yes=(on_yes, identity_key), + on_response_no=(on_no, identity_key), + transient_for=self) def fpr_button_pressed_cb(self, tw, event): if event.button == 3: @@ -541,7 +706,7 @@ class FingerprintWindow(gtk.Dialog): # selection, otherwise we only select the new item keep_selection = tw.get_selection().path_is_selected(pthinfo[0]) - pop = self.B.get_object('fprclipboard_menu') + pop = self.xml.get_object('fprclipboard_menu') pop.popup(None, None, None, event.button, event.time) # keep_selection=True -> no further processing of click event @@ -565,48 +730,56 @@ class FingerprintWindow(gtk.Dialog): def update_context_list(self, *args): self.fpr_model.clear() + state = self.omemostate if self.notebook.get_current_page() == 1: - jid = gajim.get_jid_from_account(self.account) + contact_jid = self.own_jid else: - jid = self.contact.jid - - fprDB = self.omemostate.store.identityKeyStore.getFingerprints(jid) - activeSessions = self.omemostate.store.sessionStore. \ - getActiveSessionsKeys(jid) - - for item in fprDB: - _id, jid, fpr, tr = item - active = fpr in activeSessions - fpr = binascii.hexlify(fpr) - fpr = self.human_hash(fpr[2:]) - if tr == UNTRUSTED: - if active: - self.fpr_model.append((_id, jid, 'False', - '<tt><span foreground="#FF0040">%s</span></tt>' % fpr)) - else: - self.fpr_model.append((_id, jid, 'False', - '<tt><span foreground="#585858">%s</span></tt>' % fpr)) - elif tr == TRUSTED: - if active: - self.fpr_model.append((_id, jid, 'True', - '<tt><span foreground="#2EFE2E">%s</span></tt>' % fpr)) - else: - self.fpr_model.append((_id, jid, 'True', - '<tt><span foreground="#585858">%s</span></tt>' % fpr)) - else: - if active: - self.fpr_model.append((_id, jid, 'Undecided', - '<tt><span foreground="#FF8000">%s</span></tt>' % fpr)) - else: - self.fpr_model.append((_id, jid, 'Undecided', - '<tt><span foreground="#585858">%s</span></tt>' % fpr)) - - def human_hash(self, fpr): - fpr = fpr.upper() - fplen = len(fpr) - wordsize = fplen // 8 - buf = '' - for w in range(0, fplen, wordsize): - buf += '{0} '.format(fpr[w:w + wordsize]) - return buf.rstrip() + contact_jid = self.contact.jid + + trust_str = {0: 'False', 1: 'True', 2: 'Undecided'} + if self.groupchat and self.notebook.get_current_page() == 0: + contact_jids = [] + for nick in self.plugin.groupchat[contact_jid]: + real_jid = self.plugin.groupchat[contact_jid][nick] + if real_jid == self.own_jid: + continue + contact_jids.append(real_jid) + session_db = state.store.getSessionsFromJids(contact_jids) + else: + session_db = state.store.getSessionsFromJid(contact_jid) + + for item in session_db: + color = {0: '#FF0040', # red + 1: '#2EFE2E', # green + 2: '#FF8000'} # orange + + _id, jid, deviceid, record, active = item + + active = bool(active) + + identity_key = SessionRecord(serialized=record). \ + getSessionState().getRemoteIdentityKey() + fpr = binascii.hexlify(identity_key.getPublicKey().serialize()) + fpr = human_hash(fpr[2:]) + + trust = state.store.isTrustedIdentity(jid, identity_key) + + if not active: + color[trust] = '#585858' # grey + + self.fpr_model.append( + (_id, jid, trust_str[trust], + '<tt><span foreground="{}">{}</span></tt>'. + format(color[trust], fpr), + deviceid)) + + +def human_hash(fpr): + fpr = fpr.upper() + fplen = len(fpr) + wordsize = fplen // 8 + buf = '' + for w in range(0, fplen, wordsize): + buf += '{0} '.format(fpr[w:w + wordsize]) + return buf.rstrip()