From 6fd32591fc57d0a7d293c79b797db0d32b98a41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sun, 17 Feb 2019 16:58:34 +0100 Subject: [PATCH] [omemo] Refactor Plugin --- omemo/backend/devices.py | 124 +++++++++ omemo/backend/liteaxolotlstore.py | 46 ++-- omemo/backend/state.py | 400 +++++++++--------------------- omemo/backend/util.py | 44 ++++ omemo/gtk/config.py | 139 +++++------ omemo/gtk/key.py | 94 +++---- omemo/modules/omemo.py | 338 ++++++++++--------------- omemo/plugin.py | 93 +++---- 8 files changed, 579 insertions(+), 699 deletions(-) create mode 100644 omemo/backend/devices.py create mode 100644 omemo/backend/util.py diff --git a/omemo/backend/devices.py b/omemo/backend/devices.py new file mode 100644 index 0000000..ca57aaf --- /dev/null +++ b/omemo/backend/devices.py @@ -0,0 +1,124 @@ +# Copyright (C) 2019 Philipp Hörist +# +# 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 . + +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 diff --git a/omemo/backend/liteaxolotlstore.py b/omemo/backend/liteaxolotlstore.py index 5ef2ddc..7ae01e8 100644 --- a/omemo/backend/liteaxolotlstore.py +++ b/omemo/backend/liteaxolotlstore.py @@ -32,18 +32,12 @@ 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 - class LiteAxolotlStore(AxolotlStore): def __init__(self, db_path): @@ -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)''' diff --git a/omemo/backend/state.py b/omemo/backend/state.py index 5eaa087..c253177 100644 --- a/omemo/backend/state.py +++ b/omemo/backend/state.py @@ -24,7 +24,6 @@ from nbxmpp.structs import OMEMOMessage from axolotl.ecc.djbec import DjbECPublicKey from axolotl.identitykey import IdentityKey -from axolotl.untrustedidentityexception import UntrustedIdentityException from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage from axolotl.protocol.whispermessage import WhisperMessage @@ -35,50 +34,39 @@ from axolotl.util.keyhelper import KeyHelper from axolotl.duplicatemessagexception import DuplicateMessageException from omemo.backend.aes import aes_decrypt, aes_encrypt +from omemo.backend.devices import DeviceManager +from omemo.backend.devices import NoDevicesFound from omemo.backend.liteaxolotlstore import LiteAxolotlStore -from omemo.backend.liteaxolotlstore import DEFAULT_PREKEY_AMOUNT -from omemo.backend.liteaxolotlstore import MIN_PREKEY_AMOUNT -from omemo.backend.liteaxolotlstore import SPK_CYCLE_TIME -from omemo.backend.liteaxolotlstore import SPK_ARCHIVE_TIME +from omemo.backend.util import get_fingerprint +from omemo.backend.util import DEFAULT_PREKEY_AMOUNT +from omemo.backend.util import MIN_PREKEY_AMOUNT +from omemo.backend.util import SPK_CYCLE_TIME +from omemo.backend.util import SPK_ARCHIVE_TIME log = logging.getLogger('gajim.plugin_system.omemo') -UNTRUSTED = 0 -TRUSTED = 1 -UNDECIDED = 2 - - -class OmemoState: +class OmemoState(DeviceManager): def __init__(self, own_jid, db_path, account, xmpp_con): - self.account = account - self.xmpp_con = xmpp_con + self._account = account + self._own_jid = own_jid self._session_ciphers = defaultdict(dict) - self.own_jid = own_jid - self.device_ids = {} - self.own_devices = [] + self._storage = LiteAxolotlStore(db_path) - self.store = LiteAxolotlStore(db_path) - for jid, device_id in self.store.getActiveDeviceTuples(): - if jid != own_jid: - self.add_device(jid, device_id) - else: - self.add_own_device(device_id) + DeviceManager.__init__(self) - log.info('%s => Roster devices after boot: %s', - self.account, self.device_ids) - log.info('%s => Own devices after boot: %s', - self.account, self.own_devices) - log.debug('%s => %s PreKeys available', - self.account, - self.store.getPreKeyCount()) + self.xmpp_con = xmpp_con + + log.info('%s => %s PreKeys available', + self._account, + self._storage.getPreKeyCount()) def build_session(self, jid, device_id, bundle): - session = SessionBuilder(self.store, self.store, self.store, - self.store, jid, device_id) + session = SessionBuilder(self._storage, self._storage, self._storage, + self._storage, jid, device_id) - registration_id = self.store.getLocalRegistrationId() + registration_id = self._storage.getLocalRegistrationId() prekey = bundle.pick_prekey() otpk = DjbECPublicKey(prekey['key'][1:]) @@ -98,62 +86,30 @@ class OmemoState: session.processPreKeyBundle(prekey_bundle) return self._get_session_cipher(jid, device_id) - def set_devices(self, name, devices): - self.device_ids[name] = devices - log.info('%s => Saved devices for %s', self.account, name) - - def add_device(self, name, device_id): - if name not in self.device_ids: - self.device_ids[name] = [device_id] - elif device_id not in self.device_ids[name]: - self.device_ids[name].append(device_id) - - def set_own_devices(self, devices): - """ Overwrite the current :py:attribute:`OmemoState.own_devices` with - the given devices. - - Parameters - ---------- - devices : [int] - A list of device_ids - """ - self.own_devices = devices - log.info('%s => Saved own devices', self.account) - - def add_own_device(self, device_id): - if device_id not in self.own_devices: - self.own_devices.append(device_id) + @property + def storage(self): + return self._storage @property - def own_device_id(self): - reg_id = self.store.getLocalRegistrationId() - assert reg_id is not None, \ - "Requested device_id but there is no generated" - - return (reg_id % 2147483646) + 1 - - def own_device_id_published(self): - """ Return `True` only if own device id was added via - :py:method:`OmemoState.set_own_devices()`. - """ - return self.own_device_id in self.own_devices + def own_fingerprint(self): + return get_fingerprint(self._storage.getIdentityKeyPair()) @property def bundle(self): self._check_pre_key_count() bundle = {'otpks': []} - for k in self.store.loadPendingPreKeys(): + for k in self._storage.loadPendingPreKeys(): key = k.getKeyPair().getPublicKey().serialize() bundle['otpks'].append({'key': key, 'id': k.getId()}) - ik_pair = self.store.getIdentityKeyPair() + ik_pair = self._storage.getIdentityKeyPair() bundle['ik'] = ik_pair.getPublicKey().serialize() self._cycle_signed_pre_key(ik_pair) - spk = self.store.loadSignedPreKey( - self.store.getCurrentSignedPreKeyId()) + spk = self._storage.loadSignedPreKey( + self._storage.getCurrentSignedPreKeyId()) bundle['spk_signature'] = spk.getSignature() bundle['spk'] = {'key': spk.getKeyPair().getPublicKey().serialize(), 'id': spk.getId()} @@ -161,24 +117,28 @@ class OmemoState: return OMEMOBundle(**bundle) def decrypt_message(self, omemo_message, jid): - if omemo_message.sid == self.own_device_id: + if omemo_message.sid == self.own_device: log.info('Received previously sent message by us') raise SelfMessage try: - encrypted_key, prekey = omemo_message.keys[self.own_device_id] + encrypted_key, prekey = omemo_message.keys[self.own_device] except KeyError: log.info('Received message not for our device') raise MessageNotForDevice try: if prekey: - key = self._process_pre_key_message( + key, fingerprint = self._process_pre_key_message( jid, omemo_message.sid, encrypted_key) else: - key = self._process_message( + key, fingerprint = self._process_message( jid, omemo_message.sid, encrypted_key) + except SenderNotTrusted: + log.info('Sender not trusted, ignore message') + raise + except DuplicateMessageException: log.info('Received duplicated message') raise DuplicateMessage @@ -193,238 +153,116 @@ class OmemoState: result = aes_decrypt(key, omemo_message.iv, omemo_message.payload) log.debug("Decrypted Message => %s", result) - return result + return result, fingerprint - def create_msg(self, jid, plaintext): - encrypted_keys = {} + def _get_whipser_message(self, jid, device, key): + cipher = self._get_session_cipher(jid, device) + cipher_key = cipher.encrypt(key) + prekey = isinstance(cipher_key, PreKeyWhisperMessage) + return cipher_key.serialize(), prekey - devices_list = self.device_list_for(jid) - if not devices_list: - log.error('No known devices') + def encrypt(self, jid, plaintext): + try: + devices_for_encryption = self.get_devices_for_encryption(jid) + except NoDevicesFound: + log.warning('No devices for encryption found for: %s', jid) return result = aes_encrypt(plaintext) + whisper_messages = defaultdict(dict) - # Encrypt the message key with for each of receivers devices - for device in devices_list: + for jid_, device in devices_for_encryption: try: - if self.isTrusted(jid, device) == TRUSTED: - cipher = self._get_session_cipher(jid, device) - cipher_key = cipher.encrypt(result.key) - prekey = isinstance(cipher_key, PreKeyWhisperMessage) - encrypted_keys[device] = (cipher_key.serialize(), prekey) - else: - log.debug('Skipped Device because Trust is: %s', - self.isTrusted(jid, device)) + whisper_messages[jid_][device] = self._get_whipser_message( + jid_, device, result.key) except Exception: - log.warning('Failed to find key for device: %s', device) + log.exception('Failed to encrypt') + continue - if not encrypted_keys: + recipients = set(whisper_messages.keys()) - set([self._own_jid]) + if not recipients: log.error('Encrypted keys empty') - raise NoValidSessions('Encrypted keys empty') + return - 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(self.own_jid, device) == TRUSTED: - cipher = self._get_session_cipher(self.own_jid, device) - cipher_key = cipher.encrypt(result.key) - prekey = isinstance(cipher_key, PreKeyWhisperMessage) - encrypted_keys[device] = (cipher_key.serialize(), prekey) - else: - log.debug('Skipped own Device because Trust is: %s', - self.isTrusted(self.own_jid, device)) - except Exception: - log.warning('Failed to find key for device: %s', device) - - log.debug('Finished encrypting message') - return OMEMOMessage(sid=self.own_device_id, - keys=encrypted_keys, - iv=result.iv, - payload=result.payload) - - def create_gc_msg(self, from_jid, jid, plaintext): encrypted_keys = {} - room = jid - encrypted_jids = [] - - devices_list = self.device_list_for(jid, True) - - result = aes_encrypt(plaintext) - - 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.xmpp_con.groupchat[room]: - jid_to = self.xmpp_con.groupchat[room][nick] - if jid_to == self.own_jid: - continue - if jid_to in encrypted_jids: # We already encrypted to this JID - continue - if jid_to not in self._session_ciphers: - continue - for rid, cipher in self._session_ciphers[jid_to].items(): - try: - if self.isTrusted(jid_to, rid) == TRUSTED: - cipher_key = cipher.encrypt(result.key) - prekey = isinstance(cipher_key, PreKeyWhisperMessage) - encrypted_keys[rid] = (cipher_key.serialize(), prekey) - else: - log.debug('Skipped Device because Trust is: %s', - self.isTrusted(jid_to, rid)) - except Exception: - log.exception('ERROR:') - log.warning('Failed to find key for device %s', rid) - encrypted_jids.append(jid_to) - - my_other_devices = set(self.own_devices) - set({self.own_device_id}) - # Encrypt the message key with for each of our own devices - for dev in my_other_devices: - try: - cipher = self._get_session_cipher(from_jid, dev) - if self.isTrusted(from_jid, dev) == TRUSTED: - cipher_key = cipher.encrypt(result.key) - prekey = isinstance(cipher_key, PreKeyWhisperMessage) - encrypted_keys[dev] = (cipher_key.serialize(), prekey) - else: - log.debug('Skipped own Device because Trust is: %s', - self.isTrusted(from_jid, dev)) - except Exception: - log.exception('ERROR:') - log.warning('Failed to find key for device: %s', dev) - - if not encrypted_keys: - log.error('Encrypted keys empty') - raise NoValidSessions('Encrypted keys empty') + for jid_ in whisper_messages: + encrypted_keys.update(whisper_messages[jid_]) log.debug('Finished encrypting message') - return OMEMOMessage(sid=self.own_device_id, + return OMEMOMessage(sid=self.own_device, keys=encrypted_keys, iv=result.iv, payload=result.payload) - 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.xmpp_con.groupchat[room]: - jid_to = self.xmpp_con.groupchat[room][nick] - if jid_to == self.own_jid: - continue - try: - for device in self.device_ids[jid_to]: - devicelist.append((jid_to, device)) - except KeyError: - log.warning('no device ids found for %s', jid_to) - continue - 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) - trusted = self.store.getTrustedFingerprints(recipient_id) - trusted = set(trusted) - set(inactive) - - return trusted - - def getUndecidedFingerprints(self, recipient_id): - inactive = self.store.getInactiveSessionsKeys(recipient_id) - undecided = self.store.getUndecidedFingerprints(recipient_id) - undecided = set(undecided) - set(inactive) - - return undecided + def has_trusted_keys(self, jid): + inactive = self._storage.getInactiveSessionsKeys(jid) + trusted = self._storage.getTrustedFingerprints(jid) + return bool(set(trusted) - set(inactive)) def devices_without_sessions(self, jid): - """ List device_ids for the given jid which have no axolotl session. - - Parameters - ---------- - jid : string - The contacts jid - - Returns - ------- - [int] - A list of device_ids - """ - known_devices = self.device_list_for(jid) + known_devices = self.get_devices(jid, without_self=True) missing_devices = [dev for dev in known_devices - if not self.store.containsSession(jid, dev)] + if not self._storage.containsSession(jid, dev)] if missing_devices: log.info('%s => Missing device sessions for %s: %s', - self.account, jid, missing_devices) + self._account, jid, missing_devices) return missing_devices def _get_session_cipher(self, jid, device_id): try: return self._session_ciphers[jid][device_id] except KeyError: - cipher = SessionCipher(self.store, self.store, self.store, - self.store, jid, device_id) + cipher = SessionCipher(self._storage, self._storage, self._storage, + self._storage, jid, device_id) self._session_ciphers[jid][device_id] = cipher return cipher - def _process_pre_key_message(self, recipient_id, device_id, key): - preKeyWhisperMessage = PreKeyWhisperMessage(serialized=key) - if not preKeyWhisperMessage.getPreKeyId(): - raise Exception('Received PreKeyWhisperMessage ' - 'without PreKey => %s' % recipient_id) - sessionCipher = self._get_session_cipher(recipient_id, device_id) - try: - log.debug('%s => Received PreKeyWhisperMessage from %s', - self.account, recipient_id) - key = sessionCipher.decryptPkmsg(preKeyWhisperMessage) - # Publish new bundle after PreKey has been used - # for building a new Session - self.xmpp_con.set_bundle() - self.add_device(recipient_id, device_id) - return key - except UntrustedIdentityException as error: - log.info('%s => Received WhisperMessage ' - 'from Untrusted Fingerprint! => %s', - self.account, error.getName()) + def _process_pre_key_message(self, jid, device, key): + pre_key_message = PreKeyWhisperMessage(serialized=key) + if not pre_key_message.getPreKeyId(): + raise Exception('Received Pre Key Message ' + 'without PreKey => %s' % jid) - def _process_message(self, recipient_id, device_id, key): - whisperMessage = WhisperMessage(serialized=key) - log.debug('%s => Received WhisperMessage from %s', - self.account, recipient_id) - if self.isTrusted(recipient_id, device_id): - sessionCipher = self._get_session_cipher(recipient_id, device_id) - key = sessionCipher.decryptMsg(whisperMessage, textMsg=False) - self.add_device(recipient_id, device_id) - return key + if self._storage.isUntrusted(jid, device): + raise SenderNotTrusted - raise Exception('Received WhisperMessage ' - 'from Untrusted Fingerprint! => %s' % recipient_id) + session_cipher = self._get_session_cipher(jid, device) + + log.info('%s => Process pre key message from %s', + self._account, jid) + key = session_cipher.decryptPkmsg(pre_key_message) + fingerprint = get_fingerprint(pre_key_message.getIdentityKey()) + + self.xmpp_con.set_bundle() + self.add_device(jid, device) + return key, fingerprint + + def _process_message(self, jid, device, key): + message = WhisperMessage(serialized=key) + log.info('%s => Process message from %s', self._account, jid) + + session_record = self._storage.loadSession(jid, device) + identity_key = session_record.getSessionState().getRemoteIdentityKey() + fingerprint = get_fingerprint(identity_key) + + session_cipher = self._get_session_cipher(jid, device) + key = session_cipher.decryptMsg(message, textMsg=False) + + if self._storage.isUntrusted(jid, device): + raise SenderNotTrusted + + self.add_device(jid, device) + + return key, fingerprint def _check_pre_key_count(self): # Check if enough PreKeys are available - pre_key_count = self.store.getPreKeyCount() + pre_key_count = self._storage.getPreKeyCount() if pre_key_count < MIN_PREKEY_AMOUNT: missing_count = DEFAULT_PREKEY_AMOUNT - pre_key_count - self.store.generateNewPreKeys(missing_count) - log.info('%s => %s PreKeys created', self.account, missing_count) + self._storage.generateNewPreKeys(missing_count) + log.info('%s => %s PreKeys created', self._account, missing_count) def _cycle_signed_pre_key(self, ik_pair): # Publish every SPK_CYCLE_TIME a new SignedPreKey @@ -432,27 +270,27 @@ class OmemoState: # then SPK_ARCHIVE_TIME # Check if SignedPreKey exist and create if not - if not self.store.getCurrentSignedPreKeyId(): + if not self._storage.getCurrentSignedPreKeyId(): spk = KeyHelper.generateSignedPreKey( - ik_pair, self.store.getNextSignedPreKeyId()) - self.store.storeSignedPreKey(spk.getId(), spk) + ik_pair, self._storage.getNextSignedPreKeyId()) + self._storage.storeSignedPreKey(spk.getId(), spk) log.debug('%s => New SignedPreKey created, because none existed', - self.account) + self._account) # if SPK_CYCLE_TIME is reached, generate a new SignedPreKey now = int(time.time()) - timestamp = self.store.getSignedPreKeyTimestamp( - self.store.getCurrentSignedPreKeyId()) + timestamp = self._storage.getSignedPreKeyTimestamp( + self._storage.getCurrentSignedPreKeyId()) if int(timestamp) < now - SPK_CYCLE_TIME: spk = KeyHelper.generateSignedPreKey( - ik_pair, self.store.getNextSignedPreKeyId()) - self.store.storeSignedPreKey(spk.getId(), spk) - log.debug('%s => Cycled SignedPreKey', self.account) + ik_pair, self._storage.getNextSignedPreKeyId()) + self._storage.storeSignedPreKey(spk.getId(), spk) + log.debug('%s => Cycled SignedPreKey', self._account) # Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME timestamp = now - SPK_ARCHIVE_TIME - self.store.removeOldSignedPreKeys(timestamp) + self._storage.removeOldSignedPreKeys(timestamp) class NoValidSessions(Exception): @@ -481,3 +319,7 @@ class InvalidMessage(Exception): class DuplicateMessage(Exception): pass + + +class SenderNotTrusted(Exception): + pass diff --git a/omemo/backend/util.py b/omemo/backend/util.py new file mode 100644 index 0000000..dd3ea7a --- /dev/null +++ b/omemo/backend/util.py @@ -0,0 +1,44 @@ +# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov +# +# 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 . + +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() diff --git a/omemo/gtk/config.py b/omemo/gtk/config.py index 0122744..83ed9ce 100644 --- a/omemo/gtk/config.py +++ b/omemo/gtk/config.py @@ -16,11 +16,8 @@ # You should have received a copy of the GNU General Public License # along with OMEMO Gajim Plugin. If not, see . -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('%s' % deviceid) + omemo = self.plugin.get_omemo(account) + self._ui.ID.set_markup('%s' % 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('%s' - % human_ownfpr) + identity_key = omemo.backend.storage.getIdentityKeyPair() + fpr = get_fingerprint(identity_key, formatted=True) + self._ui.fingerprint_label.set_markup('%s' % 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() diff --git a/omemo/gtk/key.py b/omemo/gtk/key.py index b6f02d1..9eeae8b 100644 --- a/omemo/gtk/key.py +++ b/omemo/gtk/key.py @@ -15,8 +15,6 @@ # along with OMEMO Gajim Plugin. If not, see . 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) + sessions = self._omemo.backend.storage.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:] - trust = state.store.isTrustedIdentity(jid, identity_key) - - log.info('Load: %s %s', fpr, trust) - self._listbox.add(KeyRow(jid, deviceid, fpr, trust, active)) + for item in sessions: + active = bool(item.active) + session_record = SessionRecord(serialized=item.record) + identity_key = session_record.getSessionState().getRemoteIdentityKey() + trust = self._omemo.backend.storage.getTrustForIdentity( + item.recipient_id, identity_key) + self._listbox.add(KeyRow(item.recipient_id, + item.device_id, + identity_key, + trust, active)) def _on_destroy(self, *args): - del self._windowinstances['dialog'] + del self._windows['dialog'] class KeyRow(Gtk.ListBoxRow): - def __init__(self, jid, deviceid, fpr, trust, active): + def __init__(self, jid, device_id, identity_key, trust, active): Gtk.ListBoxRow.__init__(self) self.set_activatable(False) self.active = active self.trust = trust self.jid = jid - self.deviceid = deviceid + self.device_id = device_id box = Gtk.Box() box.set_spacing(12) @@ -175,7 +164,8 @@ class KeyRow(Gtk.ListBoxRow): jid_label.set_hexpand(True) label_box.add(jid_label) - fingerprint = Gtk.Label(label=self._format_fingerprint(fpr)) + fingerprint = Gtk.Label(label=get_fingerprint(identity_key, + formatted=True)) fingerprint.get_style_context().add_class('omemo-mono') if not active: fingerprint.get_style_context().add_class('omemo-inactive-color') @@ -192,12 +182,12 @@ class KeyRow(Gtk.ListBoxRow): def delete_fingerprint(self, *args): def _remove(): - state = self.get_toplevel().omemostate - record = state.store.loadSession(self.jid, self.deviceid) + backend = self.get_toplevel()._omemo.backend + record = backend.storage.loadSession(self.jid, self.device_id) identity_key = record.getSessionState().getRemoteIdentityKey() - state.store.deleteSession(self.jid, self.deviceid) - state.store.deleteIdentity(self.jid, identity_key) + backend.storage.deleteSession(self.jid, self.device_id) + backend.storage.deleteIdentity(self.jid, identity_key) self.get_parent().remove(self) self.destroy() @@ -221,20 +211,10 @@ class KeyRow(Gtk.ListBoxRow): image.get_style_context().add_class(css_class) image.set_tooltip_text(tooltip) - state = self.get_toplevel().omemostate - record = state.store.loadSession(self.jid, self.deviceid) + backend = self.get_toplevel()._omemo.backend + record = backend.storage.loadSession(self.jid, self.device_id) identity_key = record.getSessionState().getRemoteIdentityKey() - state.store.setTrust(identity_key, self.trust) - - @staticmethod - def _format_fingerprint(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() + backend.storage.setTrust(identity_key, self.trust) class TrustButton(Gtk.MenuButton): @@ -278,7 +258,7 @@ class TrustPopver(Gtk.Popover): self._listbox.connect('row-activated', self._activated) self.get_style_context().add_class('omemo-trust-popover') - def _activated(self, listbox, row): + def _activated(self, _listbox, row): self.popdown() if row.type_ is None: self._row.delete_fingerprint() @@ -289,7 +269,7 @@ class TrustPopver(Gtk.Popover): self.update() def update(self): - self._listbox.foreach(lambda row: self._listbox.remove(row)) + self._listbox.foreach(self._listbox.remove) if self._row.trust != Trust.VERIFIED: self._listbox.add(VerifiedOption()) if self._row.trust != Trust.NOT_TRUSTED: diff --git a/omemo/modules/omemo.py b/omemo/modules/omemo.py index 9f21604..981fb3a 100644 --- a/omemo/modules/omemo.py +++ b/omemo/modules/omemo.py @@ -22,9 +22,11 @@ import logging import nbxmpp from nbxmpp.protocol import NodeProcessed +from nbxmpp.protocol import JID from nbxmpp.util import is_error_result from nbxmpp.const import StatusCode from nbxmpp.const import PresenceType +from nbxmpp.const import Affiliation from nbxmpp.structs import StanzaHandler from nbxmpp.modules.omemo import create_omemo_message @@ -42,6 +44,7 @@ from omemo.backend.state import SelfMessage from omemo.backend.state import MessageNotForDevice from omemo.backend.state import DecryptionFailed from omemo.backend.state import DuplicateMessage +from omemo.backend.state import SenderNotTrusted from omemo.modules.util import prepare_stanza @@ -97,29 +100,35 @@ class OMEMO(BaseModule): self._register_pubsub_handler(self._devicelist_notification_received) self.available = True - # self.plugin = plugin - self.own_jid = self._con.get_own_jid().getStripped() - self.omemo = self.__get_omemo() - self.groupchat = {} - self.temp_groupchat = {} - self.gc_message = {} - self.query_for_bundles = [] - self.query_for_devicelists = [] + self._own_jid = self._con.get_own_jid().getStripped() + self._backend = self._get_backend() + + self._omemo_groupchats = set() + self._muc_temp_store = {} + self._query_for_bundles = [] + self._query_for_devicelists = [] def get_own_jid(self, stripped=False): if stripped: return self._con.get_own_jid().getStripped() return self._con.get_own_jid() - def __get_omemo(self): + @property + def backend(self): + return self._backend + + def _get_backend(self): data_dir = configpaths.get('MY_DATA') - db_path = os.path.join(data_dir, 'omemo_' + self.own_jid + '.db') - return OmemoState(self.own_jid, db_path, self._account, self) + db_path = os.path.join(data_dir, 'omemo_' + self._own_jid + '.db') + return OmemoState(self._own_jid, db_path, self._account, self) + + def is_omemo_groupchat(self, room_jid): + return room_jid in self._omemo_groupchats def on_signed_in(self): log.info('%s => Announce Support after Sign In', self._account) - self.query_for_bundles = [] + self._query_for_bundles = [] self.set_bundle() self.request_devicelist() @@ -133,14 +142,14 @@ class OMEMO(BaseModule): if app.account_is_connected(self._account): log.info('%s => Announce Support after Plugin Activation', self._account) - self.query_for_bundles = [] + self._query_for_bundles = [] self.set_bundle() self.request_devicelist() def deactivate(self): """ Method called when the Plugin is deactivated in the PluginManager """ - self.query_for_bundles = [] + self._query_for_bundles = [] @staticmethod def update_caps(account): @@ -148,6 +157,38 @@ class OMEMO(BaseModule): if node not in app.gajim_optional_features[account]: app.gajim_optional_features[account].append(node) + def encrypt_message(self, conn, event, callback, groupchat): + if not event.message: + callback(event) + return + + to_jid = app.get_jid_without_resource(event.jid) + + omemo_message = self.backend.encrypt(to_jid, event.message) + if omemo_message is None: + app.nec.push_incoming_event( + NetworkEvent('message-not-sent', + conn=conn, + jid=event.jid, + message=event.message, + error=_('Encryption error'), + time_=time.time(), + session=event.session)) + return + + create_omemo_message(event.msg_iq, omemo_message, + node_whitelist=ALLOWED_TAGS) + + if groupchat: + self._muc_temp_store[omemo_message.payload] = event.message + else: + event.xhtml = None + event.encrypted = ENCRYPTION_NAME + event.additional_data['encrypted'] = {'name': ENCRYPTION_NAME} + + self._debug_print_stanza(event.msg_iq) + callback(event) + def _message_received(self, _con, stanza, properties): if not properties.is_omemo: return @@ -165,28 +206,31 @@ class OMEMO(BaseModule): log.info('%s => Message received from: %s', self._account, from_jid) try: - return self.omemo.decrypt_message(properties.omemo, - from_jid) - except (KeyExchangeMessage, DuplicateMessage): + plaintext, fingerprint = self.backend.decrypt_message( + properties.omemo, from_jid) + except (KeyExchangeMessage, DuplicateMessage, SenderNotTrusted): raise NodeProcessed except SelfMessage: if properties.from_muc: - if properties.omemo.payload in self.gc_message: - plaintext = self.gc_message[properties.omemo.payload] - del self.gc_message[properties.omemo.payload] - return plaintext - - log.warning("%s => Can't decrypt own GroupChat Message", - self._account) - raise NodeProcessed + if properties.omemo.payload in self._muc_temp_store: + plaintext = self._muc_temp_store[properties.omemo.payload] + fingerprint = self.backend.own_fingerprint + del self._muc_temp_store[properties.omemo.payload] + else: + log.warning("%s => Can't decrypt own GroupChat Message", + self._account) + return + else: + raise NodeProcessed except (DecryptionFailed, MessageNotForDevice): return prepare_stanza(stanza, plaintext) self._debug_print_stanza(stanza) - properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME}) + properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME, + 'fingerprint': fingerprint}) def _process_muc_message(self, properties): room_jid = properties.jid.getBare() @@ -195,18 +239,18 @@ class OMEMO(BaseModule): # History Message from MUC return properties.muc_ofrom.getBare() - try: - return self.groupchat[room_jid][resource] - except KeyError: - log.info('%s => Groupchat: Last resort trying to ' - 'find SID in DB', self._account) - from_jid = self.omemo.store.getJidFromDevice(properties.omemo.sid) - if not from_jid: - log.error("%s => Can't decrypt GroupChat Message " - "from %s", self._account, resource) - return - self.groupchat[room_jid][resource] = from_jid - return from_jid + contact = app.contacts.get_gc_contact(self._account, room_jid, resource) + if contact is not None: + return JID(contact.jid).getBare() + + log.info('%s => Groupchat: Last resort trying to ' + 'find SID in DB', self._account) + from_jid = self.backend.storage.getJidFromDevice(properties.omemo.sid) + if not from_jid: + log.error("%s => Can't decrypt GroupChat Message " + "from %s", self._account, resource) + return + return from_jid def _process_mam_message(self, properties): log.info('%s => Message received, archive: %s', @@ -228,7 +272,6 @@ class OMEMO(BaseModule): return room = properties.jid.getBare() - nick = properties.muc_nickname status_codes = properties.muc_status_codes or [] jid = properties.muc_user.jid @@ -237,41 +280,13 @@ class OMEMO(BaseModule): return jid = jid.getBare() - - if properties.is_nickname_changed: - new_nick = properties.muc_user.nick - - if room in self.groupchat: - if nick in self.groupchat[room]: - del self.groupchat[room][nick] - self.groupchat[room][new_nick] = jid - log.debug('Nick Change: old: %s, new: %s, jid: %s ', - nick, 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][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 - + if properties.muc_user.affiliation in (Affiliation.OUTCAST, + Affiliation.NONE): + self.backend.remove_muc_member(room, jid) else: - # Check if we received JID over Memberlist - if jid in self.groupchat[room]: - del self.groupchat[room][jid] - - # Add JID with Nick - if nick not in self.groupchat[room]: - self.groupchat[room][nick] = jid - log.debug('JID Added: %s', jid) + self.backend.add_muc_member(room, jid) + if room in self._omemo_groupchats: if not self.is_contact_in_roster(jid): # Query Devicelists from JIDs not in our Roster log.info('%s not in Roster, query devicelist...', jid) @@ -280,8 +295,7 @@ class OMEMO(BaseModule): if properties.is_muc_self_presence: if StatusCode.NON_ANONYMOUS in status_codes: # non-anonymous Room (Full JID) - if room not in self.groupchat: - self.groupchat[room] = self.temp_groupchat[room] + self._omemo_groupchats.add(room) log.info('OMEMO capable Room found: %s', room) self.get_affiliation_list(room) @@ -299,16 +313,6 @@ class OMEMO(BaseModule): log.info('Affiliation request failed: %s', result) return - log.info('Room %s Memberlist received', room_jid) - if room_jid not in self.groupchat: - self.groupchat[room_jid] = {} - - def jid_known(jid): - for nick in self.groupchat[room_jid]: - if self.groupchat[room_jid][nick] == jid: - return True - return False - for user_jid in result.users: try: jid = helpers.parse_jid(user_jid) @@ -316,10 +320,7 @@ class OMEMO(BaseModule): log.warning('Invalid JID: %s, ignoring it', user_jid) continue - if not jid_known(jid): - # Add JID with JID because we have no Nick yet - self.groupchat[room_jid][jid] = jid - log.info('JID Added: %s', jid) + self.backend.add_muc_member(room_jid, jid) if not self.is_contact_in_roster(jid): # Query Devicelists from JIDs not in our Roster @@ -327,7 +328,7 @@ class OMEMO(BaseModule): self.request_devicelist(jid) def is_contact_in_roster(self, jid): - if jid == self.own_jid: + if jid == self._own_jid: return True contact = app.contacts.get_first_contact_from_jid(self._account, jid) if contact is None: @@ -338,73 +339,9 @@ class OMEMO(BaseModule): room = event.jid.getBare() status_codes = event.status_codes or [] if StatusCode.CONFIG_NON_ANONYMOUS in status_codes: - if room not in self.groupchat: - self.groupchat[room] = self.temp_groupchat[room] + self._omemo_groupchats.add(room) log.info('Room config change: non-anonymous') - def gc_encrypt_message(self, conn, event, callback): - if event.conn.name != self._account: - return - - if not event.message: - callback(event) - return - - to_jid = app.get_jid_without_resource(event.jid) - - try: - omemo_message = self.omemo.create_gc_msg( - self.own_jid, to_jid, event.message) - if omemo_message is None: - raise OMEMOError('Error while encrypting') - - except OMEMOError as error: - log.error(error) - app.nec.push_incoming_event( - NetworkEvent( - 'message-not-sent', conn=conn, jid=event.jid, message=event.message, - error=error, time_=time.time(), session=None)) - return - - self.gc_message[omemo_message.payload] = event.message - create_omemo_message(event.msg_iq, omemo_message, - node_whitelist=ALLOWED_TAGS) - - self._debug_print_stanza(event.msg_iq) - callback(event) - - def encrypt_message(self, conn, event, callback): - if event.conn.name != self._account: - return - - if not event.message: - callback(event) - return - - to_jid = app.get_jid_without_resource(event.jid) - - try: - omemo_message = self.omemo.create_msg(to_jid, event.message) - if omemo_message is None: - raise OMEMOError('Error while encrypting') - - except OMEMOError as error: - log.error(error) - app.nec.push_incoming_event( - NetworkEvent( - 'message-not-sent', conn=conn, jid=event.jid, message=event.message, - error=error, time_=time.time(), session=event.session)) - return - - create_omemo_message(event.msg_iq, omemo_message, - node_whitelist=ALLOWED_TAGS) - - self._debug_print_stanza(event.msg_iq) - event.xhtml = None - event.encrypted = ENCRYPTION_NAME - event.additional_data['encrypted'] = {'name': ENCRYPTION_NAME} - callback(event) - def are_keys_missing(self, contact_jid): """ Checks if devicekeys are missing and queries the bundles @@ -421,36 +358,36 @@ class OMEMO(BaseModule): """ # Fetch Bundles of own other Devices - if self.own_jid not in self.query_for_bundles: + if self._own_jid not in self._query_for_bundles: - devices_without_session = self.omemo \ - .devices_without_sessions(self.own_jid) + devices_without_session = self.backend \ + .devices_without_sessions(self._own_jid) - self.query_for_bundles.append(self.own_jid) + self._query_for_bundles.append(self._own_jid) if devices_without_session: for device_id in devices_without_session: - self.request_bundle(self.own_jid, device_id) + self.request_bundle(self._own_jid, device_id) # Fetch Bundles of contacts devices - if contact_jid not in self.query_for_bundles: + if contact_jid not in self._query_for_bundles: - devices_without_session = self.omemo \ + devices_without_session = self.backend \ .devices_without_sessions(contact_jid) - self.query_for_bundles.append(contact_jid) + self._query_for_bundles.append(contact_jid) if devices_without_session: for device_id in devices_without_session: self.request_bundle(contact_jid, device_id) - if self.omemo.getTrustedFingerprints(contact_jid): + if self.backend.has_trusted_keys(contact_jid): return False return True def set_bundle(self): - self._nbxmpp('OMEMO').set_bundle(self.omemo.bundle, - self.omemo.own_device_id) + self._nbxmpp('OMEMO').set_bundle(self.backend.bundle, + self.backend.own_device) def request_bundle(self, jid, device_id): log.info('%s => Fetch device bundle %s %s', @@ -469,7 +406,7 @@ class OMEMO(BaseModule): self._account, jid, device_id, bundle) return - if self.omemo.build_session(jid, device_id, bundle): + if self.backend.build_session(jid, device_id, bundle): log.info('%s => session created for: %s', self._account, jid) # Trigger dialog to trust new Fingerprints if @@ -480,35 +417,24 @@ class OMEMO(BaseModule): app.nec.push_incoming_event( NetworkEvent('omemo-new-fingerprint', chat_control=ctrl)) - def set_devicelist(self, new=False): - """ Get all currently known own active device ids and publish them - - Parameters - ---------- - new : bool - if True, a devicelist with only the id of this device - is published - """ - if new: - devicelist = [self.omemo.own_device_id] - else: - devicelist = self.omemo.own_devices - devicelist.append(self.omemo.own_device_id) - devicelist = list(set(devicelist)) - self.omemo.set_own_devices(devicelist) + def set_devicelist(self): log.info('%s => Publishing own devicelist: %s', - self._account, devicelist) - self._nbxmpp('OMEMO').set_devicelist(devicelist) + self._account, self.backend.devices_for_publish) + self._nbxmpp('OMEMO').set_devicelist(self.backend.devices_for_publish) + + def clear_devicelist(self): + self.backend.update_devicelist(self._own_jid, [self.backend.own_device]) + self.set_devicelist() def request_devicelist(self, jid=None, fetch_bundle=False): - if jid in self.query_for_devicelists: + if jid in self._query_for_devicelists: return self._nbxmpp('OMEMO').request_devicelist( jid, callback=self._devicelist_received, user_data=(jid, fetch_bundle)) - self.query_for_devicelists.append(jid) + self._query_for_devicelists.append(jid) def _devicelist_received(self, devicelist, user_data): jid, fetch_bundle = user_data @@ -528,35 +454,25 @@ class OMEMO(BaseModule): self._process_devicelist_update(str(properties.jid), devicelist, False) def _process_devicelist_update(self, jid, devicelist, fetch_bundle): - if jid is None or self._con.get_own_jid().bareMatch(jid): - log.info('%s => Received own device list: %s', - self._account, devicelist) - self.omemo.set_own_devices(devicelist) - self.omemo.store.setActiveState(devicelist, self.own_jid) + own_devices = jid is None or self._con.get_own_jid().bareMatch(jid) + if own_devices: + jid = self._own_jid - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if jid in self.query_for_bundles: - self.query_for_bundles.remove(jid) + log.info('%s => Received device list for %s: %s', + self._account, jid, devicelist) + self.backend.update_devicelist(jid, devicelist) - if not self.omemo.own_device_id_published(): + if jid in self._query_for_bundles: + self._query_for_bundles.remove(jid) + + if own_devices: + if not self.backend.is_own_device_published: # Our own device_id is not in the list, it could be # overwritten by some other client self.set_devicelist() - else: - log.info('%s => Received device list for %s: %s', - self._account, jid, devicelist) - self.omemo.set_devices(jid, devicelist) - self.omemo.store.setActiveState(devicelist, jid) - - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if jid in self.query_for_bundles: - self.query_for_bundles.remove(jid) - - if fetch_bundle: - self.are_keys_missing(jid) + elif fetch_bundle: + self.are_keys_missing(jid) @staticmethod def _debug_print_stanza(stanza): @@ -567,9 +483,5 @@ class OMEMO(BaseModule): log.debug('-'*15) -class OMEMOError(Exception): - pass - - def get_instance(*args, **kwargs): return OMEMO(*args, **kwargs), 'OMEMO' diff --git a/omemo/plugin.py b/omemo/plugin.py index 1f222fe..c3a3f5d 100644 --- a/omemo/plugin.py +++ b/omemo/plugin.py @@ -79,6 +79,7 @@ class UserMessages(IntEnum): class OmemoPlugin(GajimPlugin): def init(self): + # pylint: disable=attribute-defined-outside-init if ERROR_MSG: self.activatable = False self.available_text = ERROR_MSG @@ -101,13 +102,13 @@ class OmemoPlugin(GajimPlugin): 'send_message' + self.encryption_name: ( self.before_sendmessage, None), 'encryption_dialog' + self.encryption_name: ( - self.on_encryption_button_clicked, None), + self._on_encryption_button_clicked, None), 'encryption_state' + self.encryption_name: ( self.encryption_state, None), 'update_caps': (self._update_caps, None)} self.disabled_accounts = [] - self.windowinstances = {} + self._windows = {} self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), } @@ -122,11 +123,12 @@ class OmemoPlugin(GajimPlugin): self._load_css() - def _load_css(self): + @staticmethod + def _load_css(): path = Path(__file__).parent / 'gtk' / 'style.css' try: - with open(path, "r") as f: - css = f.read() + with open(path, "r") as file: + css = file.read() except Exception as exc: log.error('Error loading css: %s', exc) return @@ -159,7 +161,8 @@ class OmemoPlugin(GajimPlugin): continue app.connections[account].get_module('OMEMO').activate() - def deactivate(self): + @staticmethod + def deactivate(): """ Method called when the Plugin is deactivated in the PluginManager """ for account in app.connections: @@ -167,15 +170,17 @@ class OmemoPlugin(GajimPlugin): continue app.connections[account].get_module('OMEMO').deactivate() - def _update_caps(self, account): + @staticmethod + def _update_caps(account): if account == 'Local': return app.connections[account].get_module('OMEMO').update_caps(account) - def activate_encryption(self, chat_control): + @staticmethod + def activate_encryption(chat_control): if isinstance(chat_control, GroupchatControl): omemo_con = app.connections[chat_control.account].get_module('OMEMO') - if chat_control.room_jid not in omemo_con.groupchat: + if not omemo_con.is_omemo_groupchat(chat_control.room_jid): dialogs.ErrorDialog( _('Bad Configuration'), _('To use OMEMO in a Groupchat, the Groupchat should be' @@ -183,21 +188,25 @@ class OmemoPlugin(GajimPlugin): return False return True - def _gc_encrypt_message(self, conn, obj, callback): + @staticmethod + def _gc_encrypt_message(conn, obj, callback): if conn.name == 'Local': return - app.connections[conn.name].get_module('OMEMO').gc_encrypt_message(conn, obj, callback) + app.connections[conn.name].get_module('OMEMO').encrypt_message( + conn, obj, callback, True) - def _encrypt_message(self, conn, obj, callback): + @staticmethod + def _encrypt_message(conn, obj, callback): if conn.name == 'Local': return - app.connections[conn.name].get_module('OMEMO').encrypt_message(conn, obj, callback) + app.connections[conn.name].get_module('OMEMO').encrypt_message( + conn, obj, callback, False) def _file_decryption(self, url, kind, instance, window): file_crypto.FileDecryption(self).hyperlink_handler( url, kind, instance, window) - def encrypt_file(self, file, account, callback): + def encrypt_file(self, file, _account, callback): thread = threading.Thread(target=self._encrypt_file_thread, args=(file, callback)) thread.daemon = True @@ -215,29 +224,29 @@ class OmemoPlugin(GajimPlugin): GLib.idle_add(callback, file) @staticmethod - def encryption_state(chat_control, state): + def encryption_state(_chat_control, state): state['visible'] = True state['authenticated'] = True - def on_encryption_button_clicked(self, chat_control): + def _on_encryption_button_clicked(self, chat_control): self.show_fingerprint_window(chat_control) - def get_omemo(self, account): - return app.connections[account].get_module('OMEMO').omemo + @staticmethod + def get_omemo(account): + return app.connections[account].get_module('OMEMO') def before_sendmessage(self, chat_control): account = chat_control.account if account == 'Local': return contact = chat_control.contact - con = app.connections[account].get_module('OMEMO') + omemo = self.get_omemo(account) self.new_fingerprints_available(chat_control) if isinstance(chat_control, GroupchatControl): room = chat_control.room_jid missing = True - for nick in con.groupchat[room]: - real_jid = con.groupchat[room][nick] - if not con.are_keys_missing(real_jid): + for jid in omemo.backend.get_muc_members(room): + if not omemo.are_keys_missing(jid): missing = False if missing: log.info('%s => No Trusted Fingerprints for %s', @@ -245,13 +254,13 @@ class OmemoPlugin(GajimPlugin): self.print_message(chat_control, UserMessages.NO_FINGERPRINTS) else: # check if we have devices for the contact - if not self.get_omemo(account).device_list_for(contact.jid): - con.request_devicelist(contact.jid, True) + if not omemo.backend.get_devices(contact.jid): + omemo.request_devicelist(contact.jid, True) self.print_message(chat_control, UserMessages.QUERY_DEVICES) chat_control.sendmessage = False return # check if bundles are missing for some devices - if con.are_keys_missing(contact.jid): + if omemo.are_keys_missing(contact.jid): log.info('%s => No Trusted Fingerprints for %s', account, contact.jid) self.print_message(chat_control, UserMessages.NO_FINGERPRINTS) @@ -266,20 +275,17 @@ class OmemoPlugin(GajimPlugin): def new_fingerprints_available(self, chat_control): jid = chat_control.contact.jid account = chat_control.account - con = app.connections[account].get_module('OMEMO') omemo = self.get_omemo(account) if isinstance(chat_control, GroupchatControl): - room_jid = chat_control.room_jid - if room_jid in con.groupchat: - for nick in con.groupchat[room_jid]: - real_jid = con.groupchat[room_jid][nick] - fingerprints = omemo.store. \ - getNewFingerprints(real_jid) - if fingerprints: - self.show_fingerprint_window( - chat_control, fingerprints) + for jid_ in omemo.backend.get_muc_members(chat_control.room_jid, + without_self=False): + fingerprints = omemo.backend.storage.getNewFingerprints(jid_) + if fingerprints: + self.show_fingerprint_window( + chat_control, fingerprints) + break elif not isinstance(chat_control, GroupchatControl): - fingerprints = omemo.store.getNewFingerprints(jid) + fingerprints = omemo.backend.storage.getNewFingerprints(jid) if fingerprints: self.show_fingerprint_window( chat_control, fingerprints) @@ -289,20 +295,21 @@ class OmemoPlugin(GajimPlugin): account = chat_control.account omemo = self.get_omemo(account) transient = chat_control.parent_win.window - if 'dialog' not in self.windowinstances: + + if 'dialog' not in self._windows: is_groupchat = isinstance(chat_control, GroupchatControl) - self.windowinstances['dialog'] = \ + self._windows['dialog'] = \ KeyDialog(self, contact, transient, - self.windowinstances, groupchat=is_groupchat) + self._windows, groupchat=is_groupchat) if fingerprints: log.debug('%s => Showing Fingerprint Prompt for %s', account, contact.jid) - omemo.store.setShownFingerprints(fingerprints) + omemo.backend.storage.setShownFingerprints(fingerprints) else: - self.windowinstances['dialog'].present() - self.windowinstances['dialog'].update() + self._windows['dialog'].present() + self._windows['dialog'].update() if fingerprints: - omemo.store.setShownFingerprints(fingerprints) + omemo.backend.storage.setShownFingerprints(fingerprints) @staticmethod def print_message(chat_control, kind):