Commit 6fd32591 authored by Philipp Hörist's avatar Philipp Hörist
Browse files

[omemo] Refactor Plugin

parent 87ece239
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of OMEMO Gajim Plugin.
#
# OMEMO Gajim 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; version 3 only.
#
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging
from collections import defaultdict
from gajim.common import app
log = logging.getLogger('gajim.plugin_system.omemo')
class DeviceManager:
def __init__(self):
self.__device_store = defaultdict(set)
self.__muc_member_store = defaultdict(set)
reg_id = self._storage.getLocalRegistrationId()
if reg_id is None:
raise ValueError('No own device found')
self.__own_device = (reg_id % 2147483646) + 1
self.add_device(self._own_jid, self.__own_device)
log.info('Our device id: %s', self.__own_device)
for jid, device in self._storage.getActiveDeviceTuples():
log.info('Load device from storage: %s - %s', jid, device)
self.add_device(jid, device)
def update_devicelist(self, jid, devicelist):
self.__device_store[jid] = set(devicelist)
log.info('Saved devices for %s', jid)
self._storage.setActiveState(jid, devicelist)
def add_muc_member(self, room_jid, jid):
log.info('Saved MUC member %s %s', room_jid, jid)
self.__muc_member_store[room_jid].add(jid)
def remove_muc_member(self, room_jid, jid):
log.info('Removed MUC member %s %s', room_jid, jid)
self.__muc_member_store[room_jid].discard(jid)
def get_muc_members(self, room_jid, without_self=True):
members = set(self.__muc_member_store[room_jid])
if without_self:
members.discard(self._own_jid)
return members
def add_device(self, jid, device):
self.__device_store[jid].add(device)
def get_devices(self, jid, without_self=False):
devices = set(self.__device_store[jid])
if without_self:
devices.discard(self._own_jid)
return devices
def get_devices_for_encryption(self, jid):
devices_for_encryption = []
if app.contacts.get_groupchat_contact(self._account, jid) is not None:
devices_for_encryption = self._get_devices_for_muc_encryption(jid)
else:
devices_for_encryption = self._get_devices_for_encryption(jid)
if not devices_for_encryption:
raise NoDevicesFound
devices_for_encryption += self._get_own_devices_for_encryption()
return devices_for_encryption
def _get_devices_for_muc_encryption(self, jid):
devices_for_encryption = []
for jid_ in self.__muc_member_store[jid]:
devices_for_encryption += self._get_devices_for_encryption(jid_)
return devices_for_encryption
def _get_own_devices_for_encryption(self):
devices_for_encryption = []
own_devices = self.get_devices(self._own_jid)
own_devices.discard(self.own_device)
for device in own_devices:
if self._storage.isTrusted(self._own_jid, device):
devices_for_encryption.append((self._own_jid, device))
return devices_for_encryption
def _get_devices_for_encryption(self, jid):
devices_for_encryption = []
devices = self.get_devices(jid)
for device in devices:
if self._storage.isTrusted(jid, device):
devices_for_encryption.append((jid, device))
return devices_for_encryption
@property
def own_device(self):
return self.__own_device
@property
def devices_for_publish(self):
devices = self.get_devices(self._own_jid)
if self.own_device not in devices:
devices.add(self.own_device)
return devices
@property
def is_own_device_published(self):
return self.own_device in self.get_devices(self._own_jid)
class NoDevicesFound(Exception):
pass
......@@ -32,17 +32,11 @@ from axolotl.identitykeypair import IdentityKeyPair
from axolotl.util.medium import Medium
from axolotl.util.keyhelper import KeyHelper
from omemo.backend.util import Trust
from omemo.backend.util import DEFAULT_PREKEY_AMOUNT
log = logging.getLogger('gajim.plugin_system.omemo')
DEFAULT_PREKEY_AMOUNT = 100
MIN_PREKEY_AMOUNT = 80
SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days
SPK_CYCLE_TIME = 86400 # 24 Hours
UNDECIDED = 2
TRUSTED = 1
UNTRUSTED = 0
log = logging.getLogger('gajim.plugin_system.omemo')
class LiteAxolotlStore(AxolotlStore):
......@@ -323,7 +317,7 @@ class LiteAxolotlStore(AxolotlStore):
', '.join(['?'] * len(recipientIds)))
return self._con.execute(query, recipientIds).fetchall()
def setActiveState(self, deviceList, jid):
def setActiveState(self, jid, deviceList):
query = '''UPDATE sessions SET active = 1
WHERE recipient_id = ? AND device_id IN ({})'''.format(
', '.join(['?'] * len(deviceList)))
......@@ -421,7 +415,7 @@ class LiteAxolotlStore(AxolotlStore):
if not self.containsIdentity(recipientId, identityKey):
self._con.execute(query, (recipientId,
identityKey.getPublicKey().serialize(),
UNDECIDED))
Trust.UNDECIDED))
self._con.commit()
def containsIdentity(self, recipientId, identityKey):
......@@ -442,17 +436,14 @@ class LiteAxolotlStore(AxolotlStore):
self._con.commit()
def isTrustedIdentity(self, recipientId, identityKey):
return True
def getTrustForIdentity(self, recipientId, identityKey):
query = '''SELECT trust FROM identities WHERE recipient_id = ?
AND public_key = ?'''
public_key = identityKey.getPublicKey().serialize()
result = self._con.execute(query, (recipientId, public_key)).fetchone()
if result is None:
return True
states = [UNTRUSTED, TRUSTED, UNDECIDED]
if result.trust in states:
return result.trust
return False
return result.trust if result is not None else None
def getAllFingerprints(self):
query = '''SELECT _id, recipient_id, public_key, trust FROM identities
......@@ -467,14 +458,9 @@ class LiteAxolotlStore(AxolotlStore):
def getTrustedFingerprints(self, jid):
query = '''SELECT public_key FROM identities
WHERE recipient_id = ? AND trust = ?'''
result = self._con.execute(query, (jid, TRUSTED)).fetchall()
result = self._con.execute(query, (jid, Trust.TRUSTED)).fetchall()
return [row.public_key for row in result]
def getUndecidedFingerprints(self, jid):
query = '''SELECT trust FROM identities
WHERE recipient_id = ? AND trust = ?'''
return self._con.execute(query, (jid, UNDECIDED)).fetchall()
def getNewFingerprints(self, jid):
query = '''SELECT _id FROM identities WHERE shown = 0
AND recipient_id = ?'''
......@@ -494,6 +480,18 @@ class LiteAxolotlStore(AxolotlStore):
self._con.execute(query, (trust, public_key))
self._con.commit()
def isTrusted(self, recipient_id, device_id):
record = self.loadSession(recipient_id, device_id)
identity_key = record.getSessionState().getRemoteIdentityKey()
return self.getTrustForIdentity(
recipient_id, identity_key) == Trust.TRUSTED
def isUntrusted(self, recipient_id, device_id):
record = self.loadSession(recipient_id, device_id)
identity_key = record.getSessionState().getRemoteIdentityKey()
return self.getTrustForIdentity(
recipient_id, identity_key) not in (Trust.TRUSTED, Trust.UNDECIDED)
def activate(self, jid):
query = '''INSERT OR REPLACE INTO encryption_state (jid, encryption)
VALUES (?, 1)'''
......
This diff is collapsed.
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# This file is part of OMEMO Gajim Plugin.
#
# OMEMO Gajim 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; version 3 only.
#
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import binascii
import textwrap
from enum import IntEnum
DEFAULT_PREKEY_AMOUNT = 100
MIN_PREKEY_AMOUNT = 80
SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days
SPK_CYCLE_TIME = 86400 # 24 Hours
class Trust(IntEnum):
UNTRUSTED = 0
TRUSTED = 1
UNDECIDED = 2
def get_fingerprint(identity_key, formatted=False):
public_key = identity_key.getPublicKey().serialize()
fingerprint = binascii.hexlify(public_key).decode()[2:]
if not formatted:
return fingerprint
fplen = len(fingerprint)
wordsize = fplen // 8
buf = ''
for w in range(0, fplen, wordsize):
buf += '{0} '.format(fingerprint[w:w + wordsize])
buf = textwrap.fill(buf, width=36)
return buf.rstrip().upper()
......@@ -16,11 +16,8 @@
# You should have received a copy of the GNU General Public License
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import binascii
import logging
import os
import textwrap
from enum import IntEnum, unique
from gi.repository import GdkPixbuf
......@@ -29,6 +26,8 @@ from gajim.common import configpaths
from gajim.plugins.gui import GajimPluginConfigDialog
from gajim.plugins.helpers import get_builder
from omemo.backend.util import get_fingerprint
log = logging.getLogger('gajim.plugin_system.omemo')
PILLOW = False
......@@ -40,13 +39,6 @@ except ImportError as error:
log.error('python-qrcode or dependencies of it are not available')
@unique
class State(IntEnum):
UNTRUSTED = 0
TRUSTED = 1
UNDECIDED = 2
class OMEMOConfigDialog(GajimPluginConfigDialog):
def init(self):
# pylint: disable=attribute-defined-outside-init
......@@ -62,19 +54,8 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
self.plugin.config['DISABLED_ACCOUNTS'] = []
self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS']
log.debug('Disabled Accounts:')
log.debug(self.disabled_accounts)
self.device_model = self._ui.get_object('deviceid_store')
self.disabled_acc_store = self._ui.get_object('disabled_account_store')
self.account_store = self._ui.get_object('account_store')
self.active_acc_view = self._ui.get_object('active_accounts_view')
self.disabled_acc_view = self._ui.get_object('disabled_accounts_view')
box = self.get_content_area()
box.pack_start(self._ui.get_object('notebook1'), True, True, 0)
box.pack_start(self._ui.notebook1, True, True, 0)
self._ui.connect_signals(self)
......@@ -91,7 +72,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
self.update_disabled_account_view()
def is_in_accountstore(self, account):
for row in self.account_store:
for row in self._ui.account_store:
if row[0] == account:
return True
return False
......@@ -103,32 +84,34 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
if account == 'Local':
continue
if not self.is_in_accountstore(account):
self.account_store.append(row=(account,))
self._ui.account_store.append(row=(account,))
def update_account_combobox(self):
if self.plugin_active is False:
return
if len(self.account_store) > 0:
self._ui.get_object('account_combobox').set_active(0)
if self._ui.account_store:
self._ui.account_combobox.set_active(0)
else:
self.account_combobox_changed_cb(
self._ui.get_object('account_combobox'))
self.account_combobox_changed_cb(self._ui.account_combobox)
def account_combobox_changed_cb(self, box, *args):
self.update_context_list()
def get_qrcode(self, jid, sid, fingerprint):
@staticmethod
def _get_qrcode(jid, sid, identity_key):
fingerprint = get_fingerprint(identity_key)
file_name = 'omemo_{}.png'.format(jid)
path = os.path.join(
configpaths.get('MY_DATA'), file_name)
ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint)
log.debug('Verification String: ' + ver_string)
log.debug('Verification String: %s', ver_string)
if os.path.exists(path):
return path
qr = qrcode.QRCode(version=None, error_correction=2, box_size=4, border=1)
qr = qrcode.QRCode(version=None, error_correction=2,
box_size=4, border=1)
qr.add_data(ver_string)
qr.make(fit=True)
img = qr.make_image()
......@@ -136,99 +119,89 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
return path
def update_disabled_account_view(self):
self.disabled_acc_store.clear()
self._ui.disabled_account_store.clear()
for account in self.disabled_accounts:
self.disabled_acc_store.append(row=(account,))
self._ui.disabled_account_store.append(row=(account,))
def activate_accounts_btn_clicked(self, button, *args):
mod, paths = self.disabled_acc_view.get_selection().get_selected_rows()
def activate_accounts_btn_clicked(self, _button, *args):
selection = self._ui.disabled_accounts_view.get_selection()
mod, paths = 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._ui.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()
def disable_accounts_btn_clicked(self, _button, *args):
selection = self._ui.active_accounts_view.get_selection()
mod, paths = 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._ui.account_store.remove(it)
self.update_disabled_account_view()
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
self.update_account_combobox()
def cleardevice_button_clicked_cb(self, button, *args):
active = self._ui.get_object('account_combobox').get_active()
account = self.account_store[active][0]
app.connections[account].get_module('OMEMO').set_devicelist(new=True)
active = self._ui.account_combobox.get_active()
account = self._ui.account_store[active][0]
app.connections[account].get_module('OMEMO').clear_devicelist()
self.update_context_list()
def refresh_button_clicked_cb(self, button, *args):
self.update_context_list()
def update_context_list(self):
self.device_model.clear()
self.qrcode = self._ui.get_object('qrcode')
self.qrinfo = self._ui.get_object('qrinfo')
if len(self.account_store) == 0:
self._ui.get_object('ID').set_markup('')
self._ui.get_object('fingerprint_label').set_markup('')
self._ui.get_object('refresh').set_sensitive(False)
self._ui.get_object('cleardevice_button').set_sensitive(False)
self._ui.get_object('qrcode').clear()
self._ui.deviceid_store.clear()
if not self._ui.account_store:
self._ui.ID.set_markup('')
self._ui.fingerprint_label.set_markup('')
self._ui.refresh.set_sensitive(False)
self._ui.cleardevice_button.set_sensitive(False)
self._ui.qrcode.clear()
return
active = self._ui.get_object('account_combobox').get_active()
account = self.account_store[active][0]
active = self._ui.account_combobox.get_active()
account = self._ui.account_store[active][0]
# Set buttons active
self._ui.get_object('refresh').set_sensitive(True)
self._ui.refresh.set_sensitive(True)
if account == 'Local':
self._ui.get_object('cleardevice_button').set_sensitive(False)
self._ui.cleardevice_button.set_sensitive(False)
else:
self._ui.get_object('cleardevice_button').set_sensitive(True)
self._ui.cleardevice_button.set_sensitive(True)
# Set FPR Label and DeviceID
state = self.plugin.get_omemo(account)
deviceid = state.own_device_id
self._ui.get_object('ID').set_markup('<tt>%s</tt>' % deviceid)
omemo = self.plugin.get_omemo(account)
self._ui.ID.set_markup('<tt>%s</tt>' % omemo.backend.own_device)
ownfpr = binascii.hexlify(state.store.getIdentityKeyPair()
.getPublicKey().serialize()).decode('utf-8')
human_ownfpr = self.human_hash(ownfpr[2:])
self._ui.get_object('fingerprint_label').set_markup('<tt>%s</tt>'
% human_ownfpr)
identity_key = omemo.backend.storage.getIdentityKeyPair()
fpr = get_fingerprint(identity_key, formatted=True)
self._ui.fingerprint_label.set_markup('<tt>%s</tt>' % fpr)
own_jid = app.get_jid_from_account(account)
# Set Device ID List
for item in state.own_devices:
self.device_model.append([item])
for item in omemo.backend.get_devices(own_jid, without_self=True):
self._ui.deviceid_store.append([item])
# Set QR Verification Code
if PILLOW:
path = self.get_qrcode(
app.get_jid_from_account(account), deviceid, ownfpr[2:])
path = self._get_qrcode(own_jid,
omemo.backend.own_device,
identity_key)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
self.qrcode.set_from_pixbuf(pixbuf)
self.qrcode.show()
self.qrinfo.hide()
self._ui.qrcode.set_from_pixbuf(pixbuf)
self._ui.qrcode.show()
self._ui.qrinfo.hide()
else:
self.qrcode.hide()
self.qrinfo.show()
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])
buf = textwrap.fill(buf, width=36)
return buf.rstrip()
self._ui.qrcode.hide()
self._ui.qrinfo.show()
......@@ -15,8 +15,6 @@
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging
import binascii
import textwrap
from gi.repository import Gtk
from gi.repository import GdkPixbuf
......@@ -27,6 +25,7 @@ from gajim.plugins.plugins_i18n import _
from omemo.gtk.util import DialogButton, ButtonAction
from omemo.gtk.util import NewConfirmationDialog
from omemo.gtk.util import Trust
from omemo.backend.util import get_fingerprint
log = logging.getLogger('gajim.plugin_system.omemo')
......@@ -44,23 +43,23 @@ TRUST_DATA = {
class KeyDialog(Gtk.Dialog):
def __init__(self, plugin, contact, transient, windowinstances,
def __init__(self, plugin, contact, transient, windows,
groupchat=False):
super().__init__(title=_('OMEMO Fingerprints'), destroy_with_parent=True)
super().__init__(title=_('OMEMO Fingerprints'),
destroy_with_parent=True)
self.set_transient_for(transient)
self.set_resizable(True)
self.set_default_size(-1, 400)
self.set_default_size(500, 450)
self.get_style_context().add_class('omemo-key-dialog')
self._groupchat = groupchat
self._contact = contact
self._windowinstances = windowinstances
self._windows = windows
self._account = self._contact.account.name
self._plugin = plugin
self._con = app.connections[self._account].get_module('OMEMO')
self.omemostate = self._plugin.get_omemo(self._account)
self._omemo = self._plugin.get_omemo(self._account)
self._own_jid = app.get_jid_from_account(self._account)
# Header
......@@ -88,9 +87,8 @@ class KeyDialog(Gtk.Dialog):
omemo_pixbuf = GdkPixbuf.Pixbuf.new_from_file(omemo_img_path)
self._omemo_logo.set_from_pixbuf(omemo_pixbuf)
ownfpr = binascii.hexlify(self.omemostate.store.getIdentityKeyPair()
.getPublicKey().serialize()).decode('utf-8')
ownfpr_format = KeyRow._format_fingerprint(ownfpr[2:])
identity_key = self._omemo.backend.storage.getIdentityKeyPair()
ownfpr_format = get_fingerprint(identity_key, formatted=True)
self._ownfpr = Gtk.Label(label=ownfpr_format)
self._ownfpr.get_style_context().add_class('omemo-mono')
self._ownfpr.set_selectable(True)
......@@ -113,52 +111,43 @@ class KeyDialog(Gtk.Dialog):
self.show_all()
def update(self):
self._listbox.foreach(lambda row: self._listbox.remove(row))
self._listbox.foreach(self._listbox.remove)
self._load_fingerprints(self._own_jid)
self._load_fingerprints(self._contact.jid, self._groupchat is True)
def _load_fingerprints(self, contact_jid, groupchat=False):
from axolotl.state.sessionrecord import SessionRecord
state = self.omemostate
if groupchat:
contact_jids = []
for nick in self._con.groupchat[contact_jid]:
real_jid = self._con.groupchat[contact_jid][nick]
if real_jid == self._own_jid:
continue
contact_jids.append(real_jid)
session_db = state.store.getSessionsFromJids(contact_jids)
members = list(self._omemo.backend.get_muc_members(contact_jid))
sessions = self._omemo.backend.storage.getSessionsFromJids(members)
else:
session_db = state.store.getSessionsFromJid(contact_jid)
for item in session_db:
_id, jid, deviceid, record, active = item
active = bool(active)
identity_key = SessionRecord(serialized=record). \
getSessionState().getRemoteIdentityKey()
fpr = binascii.hexlify(identity_key.getPublicKey().serialize()).decode('utf-8')
fpr = fpr[2:]