Commit cc04e3ac authored by Philipp Hörist's avatar Philipp Hörist

[omemo] Port omemoplugin changes from master

parent a58ff4f6
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright 2015 Daniel Gultsch <daniel@cgultsch.de>
#
# This file is part of Gajim-OMEMO plugin.
#
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
#
import logging
import os
import sqlite3
from common import caps_cache, gajim, ged
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 . import ui
from .ui import Ui
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 common import demandimport
# demandimport.enable()
# demandimport.ignore += ['_imp']
IQ_CALLBACK = {}
AXOLOTL_MISSING = 'You are missing Python-Axolotl or use an outdated version'
PROTOBUF_MISSING = 'OMEMO cant import Google Protobuf, you can find help in ' \
'the GitHub Wiki'
GAJIM_VERSION = 'OMEMO only works with the latest Gajim version, get the ' \
'latest version from gajim.org'
ERROR_MSG = ''
NS_HINTS = 'urn:xmpp:hints'
NS_PGP = 'urn:xmpp:openpgp:0'
DB_DIR = gajim.gajimpaths.data_root
log = logging.getLogger('gajim.plugin_system.omemo')
try:
from .omemo.state import OmemoState
except Exception as e:
log.error(e)
ERROR_MSG = 'Error: {}'.format(e)
try:
import google.protobuf
except Exception as e:
log.error(e)
ERROR_MSG = PROTOBUF_MISSING
try:
SETUPTOOLS_MISSING = False
from pkg_resources import parse_version
except Exception as e:
SETUPTOOLS_MISSING = True
ERROR_MSG = 'You are missing the Setuptools package.'
if not SETUPTOOLS_MISSING:
try:
import axolotl
if parse_version(axolotl.__version__) < parse_version('0.1.35'):
ERROR_MSG = AXOLOTL_MISSING
except Exception as e:
log.error(e)
ERROR_MSG = AXOLOTL_MISSING
# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init
class OmemoPlugin(GajimPlugin):
omemo_states = {}
ui_list = {}
@log_calls('OmemoPlugin')
def init(self):
""" Init """
if ERROR_MSG:
self.activatable = False
self.available_text = ERROR_MSG
return
self.events_handlers = {
'mam-message-received': (ged.PRECORE, self.mam_message_received),
'message-received': (ged.PRECORE, self.message_received),
'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),
'stanza-message-outgoing':
(ged.PRECORE, self.handle_outgoing_stanza),
'message-outgoing':
(ged.PRECORE, self.handle_outgoing_event),
}
self.config_dialog = ui.OMEMOConfigDialog(self)
self.gui_extension_points = {'chat_control': (self.connect_ui,
self.disconnect_ui)}
SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP)
self.plugin = self
self.announced = []
self.query_for_bundles = []
@log_calls('OmemoPlugin')
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 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)
self.omemo_states[account] = OmemoState(my_jid, conn, account,
self.plugin)
return self.omemo_states[account]
@staticmethod
def deactivate_gajim_e2e(account):
""" Deativates E2E encryption in Gajim """
gajim.config.set_per('accounts', account,
'autonegotiate_esessions', False)
gajim.config.set_per('accounts', account,
'enable_esessions', False)
log.info(str(account) + " => Gajim E2E encryption disabled")
@log_calls('OmemoPlugin')
def signed_in(self, event):
""" Method called on SignIn
Parameters
----------
event : SignedInEvent
"""
account = event.conn.name
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)
@log_calls('OmemoPlugin')
def activate(self):
""" 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
for account in gajim.connections:
if account not in self.announced:
if gajim.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)
@log_calls('OmemoPlugin')
def deactivate(self):
""" Method called when the Plugin is deactivated in the PluginManager
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()
@staticmethod
def _compute_caps_hash():
""" 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)
@log_calls('OmemoPlugin')
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.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)
from_jid = str(msg.msg_.getAttr('from'))
from_jid = gajim.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:
return
self.print_msg_to_log(msg.msg_)
msg.msgtxt = plaintext
contact_jid = msg.with_
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.msg_.getTag('body'):
account = msg.conn.name
jid = msg.with_
state = self.get_omemo_state(account)
omemo_enabled = state.encryption.is_active(jid)
if omemo_enabled:
msg.msgtxt = '**Unencrypted** ' + msg.msgtxt
@log_calls('OmemoPlugin')
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.stanza.getTag('openpgp', namespace=NS_PGP):
return
if msg.stanza.getTag('encrypted', namespace=NS_OMEMO) and \
msg.mtype == 'chat':
account = msg.conn.name
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))
msg_dict['sender_jid'] = gajim.get_jid_without_resource(from_jid)
plaintext = state.decrypt_msg(msg_dict)
if not plaintext:
return
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()
return False
elif msg.stanza.getTag('body') and msg.mtype == 'chat':
account = msg.conn.name
from_jid = str(msg.stanza.getFrom())
jid = gajim.get_jid_without_resource(from_jid)
state = self.get_omemo_state(account)
omemo_enabled = state.encryption.is_active(jid)
if omemo_enabled:
msg.msgtxt = '**Unencrypted** ' + msg.msgtxt
# msg.stanza.setBody(msg.msgtxt)
try:
gui = self.ui_list[account].get(jid, None)
if gui and gui.encryption_active():
gui.plain_warning()
except KeyError:
log.debug('No Ui present for ' + jid +
', Ui Warning not shown')
@log_calls('OmemoPlugin')
def handle_outgoing_event(self, event):
""" Handles a message outgoing event
In this event we have no stanza. XHTML is set to None
so that it doesnt make its way into the stanza
Parameters
----------
event : MessageOutgoingEvent
Returns
-------
Return if encryption is not activated
"""
account = event.account
state = self.get_omemo_state(account)
if not state.encryption.is_active(event.jid):
return False
event.xhtml = None
@log_calls('OmemoPlugin')
def handle_outgoing_stanza(self, event):
""" 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
"""
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)
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().encode('utf-8')
msg_dict = state.create_msg(
gajim.get_jid_from_account(account), to_jid, plaintext)
if not msg_dict:
return True
encrypted_node = OmemoMessage(msg_dict)
# Check if non-OMEMO resource is online
contacts = gajim.contacts.get_contacts(account, to_jid)
non_omemo_resource_online = False
for contact in contacts:
if contact.show == 'offline':
continue
if not contact.supports(NS_NOTIFY):
log.debug(contact.get_full_jid() +
' => Contact doesnt support OMEMO, '
'adding Info Message to Body')
support_msg = 'You received a message encrypted with ' \
'OMEMO but your client doesnt support OMEMO.'
event.msg_iq.setBody(support_msg)
non_omemo_resource_online = True
if not non_omemo_resource_online:
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)
# 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_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
"""
if event.pep_type != 'headline':
return False
devices_list = list(set(unpack_device_list_update(event.stanza,
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)
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
if not state.encryption.exist(contact_jid):
if account in self.ui_list and \
contact_jid in self.ui_list[account]:
log.debug(account +
' => Switch encryption ON automatically ...')
self.ui_list[account][contact_jid].activate_omemo()
else:
log.debug(account +
' => Switch encryption ON automatically ...')
self.omemo_enable_for(contact_jid, account)
if account in self.ui_list and \
contact_jid not in self.ui_list[account]:
chat_control = gajim.interface.msg_win_mgr.get_control(
contact_jid, account)
if chat_control:
self.connect_ui(chat_control)
return True
@log_calls('OmemoPlugin')
def publish_own_devices_list(self, account):
""" Check if the passed event is a device list update and store the new
device ids.
Parameters
----------
account : str
the account name
"""
state = self.get_omemo_state(account)
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)
gajim.connections[account].connection.send(iq)
id_ = str(iq.getAttr('id'))
IQ_CALLBACK[id_] = lambda event: log.debug(event)
@log_calls('OmemoPlugin')
def connect_ui(self, chat_control):
""" Method called from Gajim when a Chat Window is opened
Parameters
----------
chat_control : ChatControl
Gajim ChatControl object
"""
account = chat_control.contact.account.name
contact_jid = chat_control.contact.jid
if account not in self.ui_list:
self.ui_list[account] = {}
state = self.get_omemo_state(account)
my_jid = gajim.get_jid_from_account(account)
omemo_enabled = state.encryption.is_active(contact_jid)
if omemo_enabled:
log.debug(account + " => Adding OMEMO ui for " + contact_jid)
self.ui_list[account][contact_jid] = Ui(self, chat_control,
omemo_enabled, state)
self.ui_list[account][contact_jid].new_fingerprints_available()
return
if contact_jid in state.device_ids or contact_jid == my_jid:
log.debug(account + " => Adding OMEMO ui for " + contact_jid)
self.ui_list[account][contact_jid] = Ui(self, chat_control,
omemo_enabled, state)
self.ui_list[account][contact_jid].new_fingerprints_available()
else:
log.warning(account + " => No devices for " + contact_jid)
@log_calls('OmemoPlugin')
def disconnect_ui(self, chat_control):
""" Calls the removeUi method to remove all relatad UI objects.
Parameters
----------
chat_control : ChatControl
Gajim ChatControl object
"""
contact_jid = chat_control.contact.jid
account = chat_control.contact.account.name
self.ui_list[account][contact_jid].removeUi()
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 = gajim.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
else:
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: