[omemo] Refactor Plugin

This commit is contained in:
Philipp Hörist
2019-02-17 16:58:34 +01:00
parent 87ece2397e
commit 6fd32591fc
8 changed files with 579 additions and 699 deletions

124
omemo/backend/devices.py Normal file
View File

@@ -0,0 +1,124 @@
# 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

View File

@@ -32,18 +32,12 @@ from axolotl.identitykeypair import IdentityKeyPair
from axolotl.util.medium import Medium from axolotl.util.medium import Medium
from axolotl.util.keyhelper import KeyHelper 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') 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): class LiteAxolotlStore(AxolotlStore):
def __init__(self, db_path): def __init__(self, db_path):
@@ -323,7 +317,7 @@ class LiteAxolotlStore(AxolotlStore):
', '.join(['?'] * len(recipientIds))) ', '.join(['?'] * len(recipientIds)))
return self._con.execute(query, recipientIds).fetchall() return self._con.execute(query, recipientIds).fetchall()
def setActiveState(self, deviceList, jid): def setActiveState(self, jid, deviceList):
query = '''UPDATE sessions SET active = 1 query = '''UPDATE sessions SET active = 1
WHERE recipient_id = ? AND device_id IN ({})'''.format( WHERE recipient_id = ? AND device_id IN ({})'''.format(
', '.join(['?'] * len(deviceList))) ', '.join(['?'] * len(deviceList)))
@@ -421,7 +415,7 @@ class LiteAxolotlStore(AxolotlStore):
if not self.containsIdentity(recipientId, identityKey): if not self.containsIdentity(recipientId, identityKey):
self._con.execute(query, (recipientId, self._con.execute(query, (recipientId,
identityKey.getPublicKey().serialize(), identityKey.getPublicKey().serialize(),
UNDECIDED)) Trust.UNDECIDED))
self._con.commit() self._con.commit()
def containsIdentity(self, recipientId, identityKey): def containsIdentity(self, recipientId, identityKey):
@@ -442,17 +436,14 @@ class LiteAxolotlStore(AxolotlStore):
self._con.commit() self._con.commit()
def isTrustedIdentity(self, recipientId, identityKey): def isTrustedIdentity(self, recipientId, identityKey):
return True
def getTrustForIdentity(self, recipientId, identityKey):
query = '''SELECT trust FROM identities WHERE recipient_id = ? query = '''SELECT trust FROM identities WHERE recipient_id = ?
AND public_key = ?''' AND public_key = ?'''
public_key = identityKey.getPublicKey().serialize() public_key = identityKey.getPublicKey().serialize()
result = self._con.execute(query, (recipientId, public_key)).fetchone() result = self._con.execute(query, (recipientId, public_key)).fetchone()
if result is None: return result.trust if result is not None else None
return True
states = [UNTRUSTED, TRUSTED, UNDECIDED]
if result.trust in states:
return result.trust
return False
def getAllFingerprints(self): def getAllFingerprints(self):
query = '''SELECT _id, recipient_id, public_key, trust FROM identities query = '''SELECT _id, recipient_id, public_key, trust FROM identities
@@ -467,14 +458,9 @@ class LiteAxolotlStore(AxolotlStore):
def getTrustedFingerprints(self, jid): def getTrustedFingerprints(self, jid):
query = '''SELECT public_key FROM identities query = '''SELECT public_key FROM identities
WHERE recipient_id = ? AND trust = ?''' 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] 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): def getNewFingerprints(self, jid):
query = '''SELECT _id FROM identities WHERE shown = 0 query = '''SELECT _id FROM identities WHERE shown = 0
AND recipient_id = ?''' AND recipient_id = ?'''
@@ -494,6 +480,18 @@ class LiteAxolotlStore(AxolotlStore):
self._con.execute(query, (trust, public_key)) self._con.execute(query, (trust, public_key))
self._con.commit() 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): def activate(self, jid):
query = '''INSERT OR REPLACE INTO encryption_state (jid, encryption) query = '''INSERT OR REPLACE INTO encryption_state (jid, encryption)
VALUES (?, 1)''' VALUES (?, 1)'''

View File

@@ -24,7 +24,6 @@ from nbxmpp.structs import OMEMOMessage
from axolotl.ecc.djbec import DjbECPublicKey from axolotl.ecc.djbec import DjbECPublicKey
from axolotl.identitykey import IdentityKey from axolotl.identitykey import IdentityKey
from axolotl.untrustedidentityexception import UntrustedIdentityException
from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage
from axolotl.protocol.whispermessage import WhisperMessage from axolotl.protocol.whispermessage import WhisperMessage
@@ -35,50 +34,39 @@ from axolotl.util.keyhelper import KeyHelper
from axolotl.duplicatemessagexception import DuplicateMessageException from axolotl.duplicatemessagexception import DuplicateMessageException
from omemo.backend.aes import aes_decrypt, aes_encrypt 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 LiteAxolotlStore
from omemo.backend.liteaxolotlstore import DEFAULT_PREKEY_AMOUNT from omemo.backend.util import get_fingerprint
from omemo.backend.liteaxolotlstore import MIN_PREKEY_AMOUNT from omemo.backend.util import DEFAULT_PREKEY_AMOUNT
from omemo.backend.liteaxolotlstore import SPK_CYCLE_TIME from omemo.backend.util import MIN_PREKEY_AMOUNT
from omemo.backend.liteaxolotlstore import SPK_ARCHIVE_TIME from omemo.backend.util import SPK_CYCLE_TIME
from omemo.backend.util import SPK_ARCHIVE_TIME
log = logging.getLogger('gajim.plugin_system.omemo') log = logging.getLogger('gajim.plugin_system.omemo')
UNTRUSTED = 0 class OmemoState(DeviceManager):
TRUSTED = 1
UNDECIDED = 2
class OmemoState:
def __init__(self, own_jid, db_path, account, xmpp_con): def __init__(self, own_jid, db_path, account, xmpp_con):
self.account = account self._account = account
self.xmpp_con = xmpp_con self._own_jid = own_jid
self._session_ciphers = defaultdict(dict) self._session_ciphers = defaultdict(dict)
self.own_jid = own_jid self._storage = LiteAxolotlStore(db_path)
self.device_ids = {}
self.own_devices = []
self.store = LiteAxolotlStore(db_path) DeviceManager.__init__(self)
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)
log.info('%s => Roster devices after boot: %s', self.xmpp_con = xmpp_con
self.account, self.device_ids)
log.info('%s => Own devices after boot: %s', log.info('%s => %s PreKeys available',
self.account, self.own_devices) self._account,
log.debug('%s => %s PreKeys available', self._storage.getPreKeyCount())
self.account,
self.store.getPreKeyCount())
def build_session(self, jid, device_id, bundle): def build_session(self, jid, device_id, bundle):
session = SessionBuilder(self.store, self.store, self.store, session = SessionBuilder(self._storage, self._storage, self._storage,
self.store, jid, device_id) self._storage, jid, device_id)
registration_id = self.store.getLocalRegistrationId() registration_id = self._storage.getLocalRegistrationId()
prekey = bundle.pick_prekey() prekey = bundle.pick_prekey()
otpk = DjbECPublicKey(prekey['key'][1:]) otpk = DjbECPublicKey(prekey['key'][1:])
@@ -98,62 +86,30 @@ class OmemoState:
session.processPreKeyBundle(prekey_bundle) session.processPreKeyBundle(prekey_bundle)
return self._get_session_cipher(jid, device_id) return self._get_session_cipher(jid, device_id)
def set_devices(self, name, devices): @property
self.device_ids[name] = devices def storage(self):
log.info('%s => Saved devices for %s', self.account, name) return self._storage
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 @property
def own_device_id(self): def own_fingerprint(self):
reg_id = self.store.getLocalRegistrationId() return get_fingerprint(self._storage.getIdentityKeyPair())
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
@property @property
def bundle(self): def bundle(self):
self._check_pre_key_count() self._check_pre_key_count()
bundle = {'otpks': []} bundle = {'otpks': []}
for k in self.store.loadPendingPreKeys(): for k in self._storage.loadPendingPreKeys():
key = k.getKeyPair().getPublicKey().serialize() key = k.getKeyPair().getPublicKey().serialize()
bundle['otpks'].append({'key': key, 'id': k.getId()}) bundle['otpks'].append({'key': key, 'id': k.getId()})
ik_pair = self.store.getIdentityKeyPair() ik_pair = self._storage.getIdentityKeyPair()
bundle['ik'] = ik_pair.getPublicKey().serialize() bundle['ik'] = ik_pair.getPublicKey().serialize()
self._cycle_signed_pre_key(ik_pair) self._cycle_signed_pre_key(ik_pair)
spk = self.store.loadSignedPreKey( spk = self._storage.loadSignedPreKey(
self.store.getCurrentSignedPreKeyId()) self._storage.getCurrentSignedPreKeyId())
bundle['spk_signature'] = spk.getSignature() bundle['spk_signature'] = spk.getSignature()
bundle['spk'] = {'key': spk.getKeyPair().getPublicKey().serialize(), bundle['spk'] = {'key': spk.getKeyPair().getPublicKey().serialize(),
'id': spk.getId()} 'id': spk.getId()}
@@ -161,24 +117,28 @@ class OmemoState:
return OMEMOBundle(**bundle) return OMEMOBundle(**bundle)
def decrypt_message(self, omemo_message, jid): 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') log.info('Received previously sent message by us')
raise SelfMessage raise SelfMessage
try: try:
encrypted_key, prekey = omemo_message.keys[self.own_device_id] encrypted_key, prekey = omemo_message.keys[self.own_device]
except KeyError: except KeyError:
log.info('Received message not for our device') log.info('Received message not for our device')
raise MessageNotForDevice raise MessageNotForDevice
try: try:
if prekey: if prekey:
key = self._process_pre_key_message( key, fingerprint = self._process_pre_key_message(
jid, omemo_message.sid, encrypted_key) jid, omemo_message.sid, encrypted_key)
else: else:
key = self._process_message( key, fingerprint = self._process_message(
jid, omemo_message.sid, encrypted_key) jid, omemo_message.sid, encrypted_key)
except SenderNotTrusted:
log.info('Sender not trusted, ignore message')
raise
except DuplicateMessageException: except DuplicateMessageException:
log.info('Received duplicated message') log.info('Received duplicated message')
raise DuplicateMessage raise DuplicateMessage
@@ -193,238 +153,116 @@ class OmemoState:
result = aes_decrypt(key, omemo_message.iv, omemo_message.payload) result = aes_decrypt(key, omemo_message.iv, omemo_message.payload)
log.debug("Decrypted Message => %s", result) log.debug("Decrypted Message => %s", result)
return result return result, fingerprint
def create_msg(self, jid, plaintext): def _get_whipser_message(self, jid, device, key):
encrypted_keys = {} 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) def encrypt(self, jid, plaintext):
if not devices_list: try:
log.error('No known devices') devices_for_encryption = self.get_devices_for_encryption(jid)
except NoDevicesFound:
log.warning('No devices for encryption found for: %s', jid)
return return
result = aes_encrypt(plaintext) result = aes_encrypt(plaintext)
whisper_messages = defaultdict(dict)
# Encrypt the message key with for each of receivers devices for jid_, device in devices_for_encryption:
for device in devices_list:
try: try:
if self.isTrusted(jid, device) == TRUSTED: whisper_messages[jid_][device] = self._get_whipser_message(
cipher = self._get_session_cipher(jid, device) jid_, device, result.key)
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))
except Exception: 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') 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 = {} encrypted_keys = {}
room = jid for jid_ in whisper_messages:
encrypted_jids = [] encrypted_keys.update(whisper_messages[jid_])
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')
log.debug('Finished encrypting message') log.debug('Finished encrypting message')
return OMEMOMessage(sid=self.own_device_id, return OMEMOMessage(sid=self.own_device,
keys=encrypted_keys, keys=encrypted_keys,
iv=result.iv, iv=result.iv,
payload=result.payload) payload=result.payload)
def device_list_for(self, jid, gc=False): def has_trusted_keys(self, jid):
""" Return a list of known device ids for the specified jid. inactive = self._storage.getInactiveSessionsKeys(jid)
Parameters trusted = self._storage.getTrustedFingerprints(jid)
---------- return bool(set(trusted) - set(inactive))
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 devices_without_sessions(self, jid): def devices_without_sessions(self, jid):
""" List device_ids for the given jid which have no axolotl session. known_devices = self.get_devices(jid, without_self=True)
Parameters
----------
jid : string
The contacts jid
Returns
-------
[int]
A list of device_ids
"""
known_devices = self.device_list_for(jid)
missing_devices = [dev missing_devices = [dev
for dev in known_devices for dev in known_devices
if not self.store.containsSession(jid, dev)] if not self._storage.containsSession(jid, dev)]
if missing_devices: if missing_devices:
log.info('%s => Missing device sessions for %s: %s', log.info('%s => Missing device sessions for %s: %s',
self.account, jid, missing_devices) self._account, jid, missing_devices)
return missing_devices return missing_devices
def _get_session_cipher(self, jid, device_id): def _get_session_cipher(self, jid, device_id):
try: try:
return self._session_ciphers[jid][device_id] return self._session_ciphers[jid][device_id]
except KeyError: except KeyError:
cipher = SessionCipher(self.store, self.store, self.store, cipher = SessionCipher(self._storage, self._storage, self._storage,
self.store, jid, device_id) self._storage, jid, device_id)
self._session_ciphers[jid][device_id] = cipher self._session_ciphers[jid][device_id] = cipher
return cipher return cipher
def _process_pre_key_message(self, recipient_id, device_id, key): def _process_pre_key_message(self, jid, device, key):
preKeyWhisperMessage = PreKeyWhisperMessage(serialized=key) pre_key_message = PreKeyWhisperMessage(serialized=key)
if not preKeyWhisperMessage.getPreKeyId(): if not pre_key_message.getPreKeyId():
raise Exception('Received PreKeyWhisperMessage ' raise Exception('Received Pre Key Message '
'without PreKey => %s' % recipient_id) 'without PreKey => %s' % jid)
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_message(self, recipient_id, device_id, key): if self._storage.isUntrusted(jid, device):
whisperMessage = WhisperMessage(serialized=key) raise SenderNotTrusted
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
raise Exception('Received WhisperMessage ' session_cipher = self._get_session_cipher(jid, device)
'from Untrusted Fingerprint! => %s' % recipient_id)
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): def _check_pre_key_count(self):
# Check if enough PreKeys are available # 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: if pre_key_count < MIN_PREKEY_AMOUNT:
missing_count = DEFAULT_PREKEY_AMOUNT - pre_key_count missing_count = DEFAULT_PREKEY_AMOUNT - pre_key_count
self.store.generateNewPreKeys(missing_count) self._storage.generateNewPreKeys(missing_count)
log.info('%s => %s PreKeys created', self.account, missing_count) log.info('%s => %s PreKeys created', self._account, missing_count)
def _cycle_signed_pre_key(self, ik_pair): def _cycle_signed_pre_key(self, ik_pair):
# Publish every SPK_CYCLE_TIME a new SignedPreKey # Publish every SPK_CYCLE_TIME a new SignedPreKey
@@ -432,27 +270,27 @@ class OmemoState:
# then SPK_ARCHIVE_TIME # then SPK_ARCHIVE_TIME
# Check if SignedPreKey exist and create if not # Check if SignedPreKey exist and create if not
if not self.store.getCurrentSignedPreKeyId(): if not self._storage.getCurrentSignedPreKeyId():
spk = KeyHelper.generateSignedPreKey( spk = KeyHelper.generateSignedPreKey(
ik_pair, self.store.getNextSignedPreKeyId()) ik_pair, self._storage.getNextSignedPreKeyId())
self.store.storeSignedPreKey(spk.getId(), spk) self._storage.storeSignedPreKey(spk.getId(), spk)
log.debug('%s => New SignedPreKey created, because none existed', log.debug('%s => New SignedPreKey created, because none existed',
self.account) self._account)
# if SPK_CYCLE_TIME is reached, generate a new SignedPreKey # if SPK_CYCLE_TIME is reached, generate a new SignedPreKey
now = int(time.time()) now = int(time.time())
timestamp = self.store.getSignedPreKeyTimestamp( timestamp = self._storage.getSignedPreKeyTimestamp(
self.store.getCurrentSignedPreKeyId()) self._storage.getCurrentSignedPreKeyId())
if int(timestamp) < now - SPK_CYCLE_TIME: if int(timestamp) < now - SPK_CYCLE_TIME:
spk = KeyHelper.generateSignedPreKey( spk = KeyHelper.generateSignedPreKey(
ik_pair, self.store.getNextSignedPreKeyId()) ik_pair, self._storage.getNextSignedPreKeyId())
self.store.storeSignedPreKey(spk.getId(), spk) self._storage.storeSignedPreKey(spk.getId(), spk)
log.debug('%s => Cycled SignedPreKey', self.account) log.debug('%s => Cycled SignedPreKey', self._account)
# Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME # Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME
timestamp = now - SPK_ARCHIVE_TIME timestamp = now - SPK_ARCHIVE_TIME
self.store.removeOldSignedPreKeys(timestamp) self._storage.removeOldSignedPreKeys(timestamp)
class NoValidSessions(Exception): class NoValidSessions(Exception):
@@ -481,3 +319,7 @@ class InvalidMessage(Exception):
class DuplicateMessage(Exception): class DuplicateMessage(Exception):
pass pass
class SenderNotTrusted(Exception):
pass

44
omemo/backend/util.py Normal file
View File

@@ -0,0 +1,44 @@
# 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()

View File

@@ -16,11 +16,8 @@
# You should have received a copy of the GNU General Public License # 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/>. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import binascii
import logging import logging
import os import os
import textwrap
from enum import IntEnum, unique
from gi.repository import GdkPixbuf from gi.repository import GdkPixbuf
@@ -29,6 +26,8 @@ from gajim.common import configpaths
from gajim.plugins.gui import GajimPluginConfigDialog from gajim.plugins.gui import GajimPluginConfigDialog
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from omemo.backend.util import get_fingerprint
log = logging.getLogger('gajim.plugin_system.omemo') log = logging.getLogger('gajim.plugin_system.omemo')
PILLOW = False PILLOW = False
@@ -40,13 +39,6 @@ except ImportError as error:
log.error('python-qrcode or dependencies of it are not available') log.error('python-qrcode or dependencies of it are not available')
@unique
class State(IntEnum):
UNTRUSTED = 0
TRUSTED = 1
UNDECIDED = 2
class OMEMOConfigDialog(GajimPluginConfigDialog): class OMEMOConfigDialog(GajimPluginConfigDialog):
def init(self): def init(self):
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
@@ -62,19 +54,8 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
self.plugin.config['DISABLED_ACCOUNTS'] = [] self.plugin.config['DISABLED_ACCOUNTS'] = []
self.disabled_accounts = 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 = 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) self._ui.connect_signals(self)
@@ -91,7 +72,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
self.update_disabled_account_view() self.update_disabled_account_view()
def is_in_accountstore(self, account): def is_in_accountstore(self, account):
for row in self.account_store: for row in self._ui.account_store:
if row[0] == account: if row[0] == account:
return True return True
return False return False
@@ -103,32 +84,34 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
if account == 'Local': if account == 'Local':
continue continue
if not self.is_in_accountstore(account): 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): def update_account_combobox(self):
if self.plugin_active is False: if self.plugin_active is False:
return return
if len(self.account_store) > 0: if self._ui.account_store:
self._ui.get_object('account_combobox').set_active(0) self._ui.account_combobox.set_active(0)
else: else:
self.account_combobox_changed_cb( self.account_combobox_changed_cb(self._ui.account_combobox)
self._ui.get_object('account_combobox'))
def account_combobox_changed_cb(self, box, *args): def account_combobox_changed_cb(self, box, *args):
self.update_context_list() 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) file_name = 'omemo_{}.png'.format(jid)
path = os.path.join( path = os.path.join(
configpaths.get('MY_DATA'), file_name) configpaths.get('MY_DATA'), file_name)
ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint) 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): if os.path.exists(path):
return 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.add_data(ver_string)
qr.make(fit=True) qr.make(fit=True)
img = qr.make_image() img = qr.make_image()
@@ -136,99 +119,89 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
return path return path
def update_disabled_account_view(self): def update_disabled_account_view(self):
self.disabled_acc_store.clear() self._ui.disabled_account_store.clear()
for account in self.disabled_accounts: 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): def activate_accounts_btn_clicked(self, _button, *args):
mod, paths = self.disabled_acc_view.get_selection().get_selected_rows() selection = self._ui.disabled_accounts_view.get_selection()
mod, paths = selection.get_selected_rows()
for path in paths: for path in paths:
it = mod.get_iter(path) it = mod.get_iter(path)
account = mod.get(it, 0) account = mod.get(it, 0)
if account[0] in self.disabled_accounts and \ if account[0] in self.disabled_accounts and \
not self.is_in_accountstore(account[0]): 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.disabled_accounts.remove(account[0])
self.update_disabled_account_view() self.update_disabled_account_view()
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
self.update_account_combobox() self.update_account_combobox()
def disable_accounts_btn_clicked(self, button, *args): def disable_accounts_btn_clicked(self, _button, *args):
mod, paths = self.active_acc_view.get_selection().get_selected_rows() selection = self._ui.active_accounts_view.get_selection()
mod, paths = selection.get_selected_rows()
for path in paths: for path in paths:
it = mod.get_iter(path) it = mod.get_iter(path)
account = mod.get(it, 0) account = mod.get(it, 0)
if account[0] not in self.disabled_accounts and \ if account[0] not in self.disabled_accounts and \
self.is_in_accountstore(account[0]): self.is_in_accountstore(account[0]):
self.disabled_accounts.append(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.update_disabled_account_view()
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
self.update_account_combobox() self.update_account_combobox()
def cleardevice_button_clicked_cb(self, button, *args): def cleardevice_button_clicked_cb(self, button, *args):
active = self._ui.get_object('account_combobox').get_active() active = self._ui.account_combobox.get_active()
account = self.account_store[active][0] account = self._ui.account_store[active][0]
app.connections[account].get_module('OMEMO').set_devicelist(new=True) app.connections[account].get_module('OMEMO').clear_devicelist()
self.update_context_list() self.update_context_list()
def refresh_button_clicked_cb(self, button, *args): def refresh_button_clicked_cb(self, button, *args):
self.update_context_list() self.update_context_list()
def update_context_list(self): def update_context_list(self):
self.device_model.clear() self._ui.deviceid_store.clear()
self.qrcode = self._ui.get_object('qrcode')
self.qrinfo = self._ui.get_object('qrinfo') if not self._ui.account_store:
if len(self.account_store) == 0: self._ui.ID.set_markup('')
self._ui.get_object('ID').set_markup('') self._ui.fingerprint_label.set_markup('')
self._ui.get_object('fingerprint_label').set_markup('') self._ui.refresh.set_sensitive(False)
self._ui.get_object('refresh').set_sensitive(False) self._ui.cleardevice_button.set_sensitive(False)
self._ui.get_object('cleardevice_button').set_sensitive(False) self._ui.qrcode.clear()
self._ui.get_object('qrcode').clear()
return return
active = self._ui.get_object('account_combobox').get_active() active = self._ui.account_combobox.get_active()
account = self.account_store[active][0] account = self._ui.account_store[active][0]
# Set buttons active # Set buttons active
self._ui.get_object('refresh').set_sensitive(True) self._ui.refresh.set_sensitive(True)
if account == 'Local': if account == 'Local':
self._ui.get_object('cleardevice_button').set_sensitive(False) self._ui.cleardevice_button.set_sensitive(False)
else: else:
self._ui.get_object('cleardevice_button').set_sensitive(True) self._ui.cleardevice_button.set_sensitive(True)
# Set FPR Label and DeviceID # Set FPR Label and DeviceID
state = self.plugin.get_omemo(account) omemo = self.plugin.get_omemo(account)
deviceid = state.own_device_id self._ui.ID.set_markup('<tt>%s</tt>' % omemo.backend.own_device)
self._ui.get_object('ID').set_markup('<tt>%s</tt>' % deviceid)
ownfpr = binascii.hexlify(state.store.getIdentityKeyPair() identity_key = omemo.backend.storage.getIdentityKeyPair()
.getPublicKey().serialize()).decode('utf-8') fpr = get_fingerprint(identity_key, formatted=True)
human_ownfpr = self.human_hash(ownfpr[2:]) self._ui.fingerprint_label.set_markup('<tt>%s</tt>' % fpr)
self._ui.get_object('fingerprint_label').set_markup('<tt>%s</tt>'
% human_ownfpr)
own_jid = app.get_jid_from_account(account)
# Set Device ID List # Set Device ID List
for item in state.own_devices: for item in omemo.backend.get_devices(own_jid, without_self=True):
self.device_model.append([item]) self._ui.deviceid_store.append([item])
# Set QR Verification Code # Set QR Verification Code
if PILLOW: if PILLOW:
path = self.get_qrcode( path = self._get_qrcode(own_jid,
app.get_jid_from_account(account), deviceid, ownfpr[2:]) omemo.backend.own_device,
identity_key)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path) pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
self.qrcode.set_from_pixbuf(pixbuf) self._ui.qrcode.set_from_pixbuf(pixbuf)
self.qrcode.show() self._ui.qrcode.show()
self.qrinfo.hide() self._ui.qrinfo.hide()
else: else:
self.qrcode.hide() self._ui.qrcode.hide()
self.qrinfo.show() self._ui.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()

View File

@@ -15,8 +15,6 @@
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging import logging
import binascii
import textwrap
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import GdkPixbuf 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 DialogButton, ButtonAction
from omemo.gtk.util import NewConfirmationDialog from omemo.gtk.util import NewConfirmationDialog
from omemo.gtk.util import Trust from omemo.gtk.util import Trust
from omemo.backend.util import get_fingerprint
log = logging.getLogger('gajim.plugin_system.omemo') log = logging.getLogger('gajim.plugin_system.omemo')
@@ -44,23 +43,23 @@ TRUST_DATA = {
class KeyDialog(Gtk.Dialog): class KeyDialog(Gtk.Dialog):
def __init__(self, plugin, contact, transient, windowinstances, def __init__(self, plugin, contact, transient, windows,
groupchat=False): 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_transient_for(transient)
self.set_resizable(True) 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.get_style_context().add_class('omemo-key-dialog')
self._groupchat = groupchat self._groupchat = groupchat
self._contact = contact self._contact = contact
self._windowinstances = windowinstances self._windows = windows
self._account = self._contact.account.name self._account = self._contact.account.name
self._plugin = plugin self._plugin = plugin
self._con = app.connections[self._account].get_module('OMEMO') self._omemo = self._plugin.get_omemo(self._account)
self.omemostate = self._plugin.get_omemo(self._account)
self._own_jid = app.get_jid_from_account(self._account) self._own_jid = app.get_jid_from_account(self._account)
# Header # Header
@@ -88,9 +87,8 @@ class KeyDialog(Gtk.Dialog):
omemo_pixbuf = GdkPixbuf.Pixbuf.new_from_file(omemo_img_path) omemo_pixbuf = GdkPixbuf.Pixbuf.new_from_file(omemo_img_path)
self._omemo_logo.set_from_pixbuf(omemo_pixbuf) self._omemo_logo.set_from_pixbuf(omemo_pixbuf)
ownfpr = binascii.hexlify(self.omemostate.store.getIdentityKeyPair() identity_key = self._omemo.backend.storage.getIdentityKeyPair()
.getPublicKey().serialize()).decode('utf-8') ownfpr_format = get_fingerprint(identity_key, formatted=True)
ownfpr_format = KeyRow._format_fingerprint(ownfpr[2:])
self._ownfpr = Gtk.Label(label=ownfpr_format) self._ownfpr = Gtk.Label(label=ownfpr_format)
self._ownfpr.get_style_context().add_class('omemo-mono') self._ownfpr.get_style_context().add_class('omemo-mono')
self._ownfpr.set_selectable(True) self._ownfpr.set_selectable(True)
@@ -113,52 +111,43 @@ class KeyDialog(Gtk.Dialog):
self.show_all() self.show_all()
def update(self): 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._own_jid)
self._load_fingerprints(self._contact.jid, self._groupchat is True) self._load_fingerprints(self._contact.jid, self._groupchat is True)
def _load_fingerprints(self, contact_jid, groupchat=False): def _load_fingerprints(self, contact_jid, groupchat=False):
from axolotl.state.sessionrecord import SessionRecord from axolotl.state.sessionrecord import SessionRecord
state = self.omemostate
if groupchat: if groupchat:
contact_jids = [] members = list(self._omemo.backend.get_muc_members(contact_jid))
for nick in self._con.groupchat[contact_jid]: sessions = self._omemo.backend.storage.getSessionsFromJids(members)
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)
else: else:
session_db = state.store.getSessionsFromJid(contact_jid) sessions = self._omemo.backend.storage.getSessionsFromJid(contact_jid)
for item in session_db: for item in sessions:
_id, jid, deviceid, record, active = item active = bool(item.active)
session_record = SessionRecord(serialized=item.record)
active = bool(active) identity_key = session_record.getSessionState().getRemoteIdentityKey()
trust = self._omemo.backend.storage.getTrustForIdentity(
identity_key = SessionRecord(serialized=record). \ item.recipient_id, identity_key)
getSessionState().getRemoteIdentityKey() self._listbox.add(KeyRow(item.recipient_id,
fpr = binascii.hexlify(identity_key.getPublicKey().serialize()).decode('utf-8') item.device_id,
fpr = fpr[2:] identity_key,
trust = state.store.isTrustedIdentity(jid, identity_key) trust, active))
log.info('Load: %s %s', fpr, trust)
self._listbox.add(KeyRow(jid, deviceid, fpr, trust, active))
def _on_destroy(self, *args): def _on_destroy(self, *args):
del self._windowinstances['dialog'] del self._windows['dialog']
class KeyRow(Gtk.ListBoxRow): 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) Gtk.ListBoxRow.__init__(self)
self.set_activatable(False) self.set_activatable(False)
self.active = active self.active = active
self.trust = trust self.trust = trust
self.jid = jid self.jid = jid
self.deviceid = deviceid self.device_id = device_id
box = Gtk.Box() box = Gtk.Box()
box.set_spacing(12) box.set_spacing(12)
@@ -175,7 +164,8 @@ class KeyRow(Gtk.ListBoxRow):
jid_label.set_hexpand(True) jid_label.set_hexpand(True)
label_box.add(jid_label) 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') fingerprint.get_style_context().add_class('omemo-mono')
if not active: if not active:
fingerprint.get_style_context().add_class('omemo-inactive-color') fingerprint.get_style_context().add_class('omemo-inactive-color')
@@ -192,12 +182,12 @@ class KeyRow(Gtk.ListBoxRow):
def delete_fingerprint(self, *args): def delete_fingerprint(self, *args):
def _remove(): def _remove():
state = self.get_toplevel().omemostate backend = self.get_toplevel()._omemo.backend
record = state.store.loadSession(self.jid, self.deviceid) record = backend.storage.loadSession(self.jid, self.device_id)
identity_key = record.getSessionState().getRemoteIdentityKey() identity_key = record.getSessionState().getRemoteIdentityKey()
state.store.deleteSession(self.jid, self.deviceid) backend.storage.deleteSession(self.jid, self.device_id)
state.store.deleteIdentity(self.jid, identity_key) backend.storage.deleteIdentity(self.jid, identity_key)
self.get_parent().remove(self) self.get_parent().remove(self)
self.destroy() self.destroy()
@@ -221,20 +211,10 @@ class KeyRow(Gtk.ListBoxRow):
image.get_style_context().add_class(css_class) image.get_style_context().add_class(css_class)
image.set_tooltip_text(tooltip) image.set_tooltip_text(tooltip)
state = self.get_toplevel().omemostate backend = self.get_toplevel()._omemo.backend
record = state.store.loadSession(self.jid, self.deviceid) record = backend.storage.loadSession(self.jid, self.device_id)
identity_key = record.getSessionState().getRemoteIdentityKey() identity_key = record.getSessionState().getRemoteIdentityKey()
state.store.setTrust(identity_key, self.trust) backend.storage.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()
class TrustButton(Gtk.MenuButton): class TrustButton(Gtk.MenuButton):
@@ -278,7 +258,7 @@ class TrustPopver(Gtk.Popover):
self._listbox.connect('row-activated', self._activated) self._listbox.connect('row-activated', self._activated)
self.get_style_context().add_class('omemo-trust-popover') self.get_style_context().add_class('omemo-trust-popover')
def _activated(self, listbox, row): def _activated(self, _listbox, row):
self.popdown() self.popdown()
if row.type_ is None: if row.type_ is None:
self._row.delete_fingerprint() self._row.delete_fingerprint()
@@ -289,7 +269,7 @@ class TrustPopver(Gtk.Popover):
self.update() self.update()
def update(self): def update(self):
self._listbox.foreach(lambda row: self._listbox.remove(row)) self._listbox.foreach(self._listbox.remove)
if self._row.trust != Trust.VERIFIED: if self._row.trust != Trust.VERIFIED:
self._listbox.add(VerifiedOption()) self._listbox.add(VerifiedOption())
if self._row.trust != Trust.NOT_TRUSTED: if self._row.trust != Trust.NOT_TRUSTED:

View File

@@ -22,9 +22,11 @@ import logging
import nbxmpp import nbxmpp
from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import NodeProcessed
from nbxmpp.protocol import JID
from nbxmpp.util import is_error_result from nbxmpp.util import is_error_result
from nbxmpp.const import StatusCode from nbxmpp.const import StatusCode
from nbxmpp.const import PresenceType from nbxmpp.const import PresenceType
from nbxmpp.const import Affiliation
from nbxmpp.structs import StanzaHandler from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.omemo import create_omemo_message 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 MessageNotForDevice
from omemo.backend.state import DecryptionFailed from omemo.backend.state import DecryptionFailed
from omemo.backend.state import DuplicateMessage from omemo.backend.state import DuplicateMessage
from omemo.backend.state import SenderNotTrusted
from omemo.modules.util import prepare_stanza from omemo.modules.util import prepare_stanza
@@ -97,29 +100,35 @@ class OMEMO(BaseModule):
self._register_pubsub_handler(self._devicelist_notification_received) self._register_pubsub_handler(self._devicelist_notification_received)
self.available = True self.available = True
# self.plugin = plugin
self.own_jid = self._con.get_own_jid().getStripped()
self.omemo = self.__get_omemo()
self.groupchat = {} self._own_jid = self._con.get_own_jid().getStripped()
self.temp_groupchat = {} self._backend = self._get_backend()
self.gc_message = {}
self.query_for_bundles = [] self._omemo_groupchats = set()
self.query_for_devicelists = [] self._muc_temp_store = {}
self._query_for_bundles = []
self._query_for_devicelists = []
def get_own_jid(self, stripped=False): def get_own_jid(self, stripped=False):
if stripped: if stripped:
return self._con.get_own_jid().getStripped() return self._con.get_own_jid().getStripped()
return self._con.get_own_jid() 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') data_dir = configpaths.get('MY_DATA')
db_path = os.path.join(data_dir, 'omemo_' + self.own_jid + '.db') db_path = os.path.join(data_dir, 'omemo_' + self._own_jid + '.db')
return OmemoState(self.own_jid, db_path, self._account, self) 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): def on_signed_in(self):
log.info('%s => Announce Support after Sign In', self._account) log.info('%s => Announce Support after Sign In', self._account)
self.query_for_bundles = [] self._query_for_bundles = []
self.set_bundle() self.set_bundle()
self.request_devicelist() self.request_devicelist()
@@ -133,14 +142,14 @@ class OMEMO(BaseModule):
if app.account_is_connected(self._account): if app.account_is_connected(self._account):
log.info('%s => Announce Support after Plugin Activation', log.info('%s => Announce Support after Plugin Activation',
self._account) self._account)
self.query_for_bundles = [] self._query_for_bundles = []
self.set_bundle() self.set_bundle()
self.request_devicelist() self.request_devicelist()
def deactivate(self): def deactivate(self):
""" Method called when the Plugin is deactivated in the PluginManager """ Method called when the Plugin is deactivated in the PluginManager
""" """
self.query_for_bundles = [] self._query_for_bundles = []
@staticmethod @staticmethod
def update_caps(account): def update_caps(account):
@@ -148,6 +157,38 @@ class OMEMO(BaseModule):
if node not in app.gajim_optional_features[account]: if node not in app.gajim_optional_features[account]:
app.gajim_optional_features[account].append(node) 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): def _message_received(self, _con, stanza, properties):
if not properties.is_omemo: if not properties.is_omemo:
return return
@@ -165,28 +206,31 @@ class OMEMO(BaseModule):
log.info('%s => Message received from: %s', self._account, from_jid) log.info('%s => Message received from: %s', self._account, from_jid)
try: try:
return self.omemo.decrypt_message(properties.omemo, plaintext, fingerprint = self.backend.decrypt_message(
from_jid) properties.omemo, from_jid)
except (KeyExchangeMessage, DuplicateMessage): except (KeyExchangeMessage, DuplicateMessage, SenderNotTrusted):
raise NodeProcessed raise NodeProcessed
except SelfMessage: except SelfMessage:
if properties.from_muc: if properties.from_muc:
if properties.omemo.payload in self.gc_message: if properties.omemo.payload in self._muc_temp_store:
plaintext = self.gc_message[properties.omemo.payload] plaintext = self._muc_temp_store[properties.omemo.payload]
del self.gc_message[properties.omemo.payload] fingerprint = self.backend.own_fingerprint
return plaintext del self._muc_temp_store[properties.omemo.payload]
else:
log.warning("%s => Can't decrypt own GroupChat Message", log.warning("%s => Can't decrypt own GroupChat Message",
self._account) self._account)
raise NodeProcessed return
else:
raise NodeProcessed
except (DecryptionFailed, MessageNotForDevice): except (DecryptionFailed, MessageNotForDevice):
return return
prepare_stanza(stanza, plaintext) prepare_stanza(stanza, plaintext)
self._debug_print_stanza(stanza) 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): def _process_muc_message(self, properties):
room_jid = properties.jid.getBare() room_jid = properties.jid.getBare()
@@ -195,18 +239,18 @@ class OMEMO(BaseModule):
# History Message from MUC # History Message from MUC
return properties.muc_ofrom.getBare() return properties.muc_ofrom.getBare()
try: contact = app.contacts.get_gc_contact(self._account, room_jid, resource)
return self.groupchat[room_jid][resource] if contact is not None:
except KeyError: return JID(contact.jid).getBare()
log.info('%s => Groupchat: Last resort trying to '
'find SID in DB', self._account) log.info('%s => Groupchat: Last resort trying to '
from_jid = self.omemo.store.getJidFromDevice(properties.omemo.sid) 'find SID in DB', self._account)
if not from_jid: from_jid = self.backend.storage.getJidFromDevice(properties.omemo.sid)
log.error("%s => Can't decrypt GroupChat Message " if not from_jid:
"from %s", self._account, resource) log.error("%s => Can't decrypt GroupChat Message "
return "from %s", self._account, resource)
self.groupchat[room_jid][resource] = from_jid return
return from_jid return from_jid
def _process_mam_message(self, properties): def _process_mam_message(self, properties):
log.info('%s => Message received, archive: %s', log.info('%s => Message received, archive: %s',
@@ -228,7 +272,6 @@ class OMEMO(BaseModule):
return return
room = properties.jid.getBare() room = properties.jid.getBare()
nick = properties.muc_nickname
status_codes = properties.muc_status_codes or [] status_codes = properties.muc_status_codes or []
jid = properties.muc_user.jid jid = properties.muc_user.jid
@@ -237,41 +280,13 @@ class OMEMO(BaseModule):
return return
jid = jid.getBare() jid = jid.getBare()
if properties.muc_user.affiliation in (Affiliation.OUTCAST,
if properties.is_nickname_changed: Affiliation.NONE):
new_nick = properties.muc_user.nick self.backend.remove_muc_member(room, jid)
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
else: else:
# Check if we received JID over Memberlist self.backend.add_muc_member(room, jid)
if jid in self.groupchat[room]:
del self.groupchat[room][jid]
# Add JID with Nick
if nick not in self.groupchat[room]:
self.groupchat[room][nick] = jid
log.debug('JID Added: %s', jid)
if room in self._omemo_groupchats:
if not self.is_contact_in_roster(jid): if not self.is_contact_in_roster(jid):
# Query Devicelists from JIDs not in our Roster # Query Devicelists from JIDs not in our Roster
log.info('%s not in Roster, query devicelist...', jid) log.info('%s not in Roster, query devicelist...', jid)
@@ -280,8 +295,7 @@ class OMEMO(BaseModule):
if properties.is_muc_self_presence: if properties.is_muc_self_presence:
if StatusCode.NON_ANONYMOUS in status_codes: if StatusCode.NON_ANONYMOUS in status_codes:
# non-anonymous Room (Full JID) # non-anonymous Room (Full JID)
if room not in self.groupchat: self._omemo_groupchats.add(room)
self.groupchat[room] = self.temp_groupchat[room]
log.info('OMEMO capable Room found: %s', room) log.info('OMEMO capable Room found: %s', room)
self.get_affiliation_list(room) self.get_affiliation_list(room)
@@ -299,16 +313,6 @@ class OMEMO(BaseModule):
log.info('Affiliation request failed: %s', result) log.info('Affiliation request failed: %s', result)
return 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: for user_jid in result.users:
try: try:
jid = helpers.parse_jid(user_jid) jid = helpers.parse_jid(user_jid)
@@ -316,10 +320,7 @@ class OMEMO(BaseModule):
log.warning('Invalid JID: %s, ignoring it', user_jid) log.warning('Invalid JID: %s, ignoring it', user_jid)
continue continue
if not jid_known(jid): self.backend.add_muc_member(room_jid, jid)
# Add JID with JID because we have no Nick yet
self.groupchat[room_jid][jid] = jid
log.info('JID Added: %s', jid)
if not self.is_contact_in_roster(jid): if not self.is_contact_in_roster(jid):
# Query Devicelists from JIDs not in our Roster # Query Devicelists from JIDs not in our Roster
@@ -327,7 +328,7 @@ class OMEMO(BaseModule):
self.request_devicelist(jid) self.request_devicelist(jid)
def is_contact_in_roster(self, jid): def is_contact_in_roster(self, jid):
if jid == self.own_jid: if jid == self._own_jid:
return True return True
contact = app.contacts.get_first_contact_from_jid(self._account, jid) contact = app.contacts.get_first_contact_from_jid(self._account, jid)
if contact is None: if contact is None:
@@ -338,73 +339,9 @@ class OMEMO(BaseModule):
room = event.jid.getBare() room = event.jid.getBare()
status_codes = event.status_codes or [] status_codes = event.status_codes or []
if StatusCode.CONFIG_NON_ANONYMOUS in status_codes: if StatusCode.CONFIG_NON_ANONYMOUS in status_codes:
if room not in self.groupchat: self._omemo_groupchats.add(room)
self.groupchat[room] = self.temp_groupchat[room]
log.info('Room config change: non-anonymous') 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): def are_keys_missing(self, contact_jid):
""" Checks if devicekeys are missing and queries the """ Checks if devicekeys are missing and queries the
bundles bundles
@@ -421,36 +358,36 @@ class OMEMO(BaseModule):
""" """
# Fetch Bundles of own other Devices # 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_session = self.backend \
.devices_without_sessions(self.own_jid) .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: if devices_without_session:
for device_id in 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 # 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) .devices_without_sessions(contact_jid)
self.query_for_bundles.append(contact_jid) self._query_for_bundles.append(contact_jid)
if devices_without_session: if devices_without_session:
for device_id in devices_without_session: for device_id in devices_without_session:
self.request_bundle(contact_jid, device_id) self.request_bundle(contact_jid, device_id)
if self.omemo.getTrustedFingerprints(contact_jid): if self.backend.has_trusted_keys(contact_jid):
return False return False
return True return True
def set_bundle(self): def set_bundle(self):
self._nbxmpp('OMEMO').set_bundle(self.omemo.bundle, self._nbxmpp('OMEMO').set_bundle(self.backend.bundle,
self.omemo.own_device_id) self.backend.own_device)
def request_bundle(self, jid, device_id): def request_bundle(self, jid, device_id):
log.info('%s => Fetch device bundle %s %s', log.info('%s => Fetch device bundle %s %s',
@@ -469,7 +406,7 @@ class OMEMO(BaseModule):
self._account, jid, device_id, bundle) self._account, jid, device_id, bundle)
return 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', log.info('%s => session created for: %s',
self._account, jid) self._account, jid)
# Trigger dialog to trust new Fingerprints if # Trigger dialog to trust new Fingerprints if
@@ -480,35 +417,24 @@ class OMEMO(BaseModule):
app.nec.push_incoming_event( app.nec.push_incoming_event(
NetworkEvent('omemo-new-fingerprint', chat_control=ctrl)) NetworkEvent('omemo-new-fingerprint', chat_control=ctrl))
def set_devicelist(self, new=False): def set_devicelist(self):
""" 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)
log.info('%s => Publishing own devicelist: %s', log.info('%s => Publishing own devicelist: %s',
self._account, devicelist) self._account, self.backend.devices_for_publish)
self._nbxmpp('OMEMO').set_devicelist(devicelist) 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): def request_devicelist(self, jid=None, fetch_bundle=False):
if jid in self.query_for_devicelists: if jid in self._query_for_devicelists:
return return
self._nbxmpp('OMEMO').request_devicelist( self._nbxmpp('OMEMO').request_devicelist(
jid, jid,
callback=self._devicelist_received, callback=self._devicelist_received,
user_data=(jid, fetch_bundle)) user_data=(jid, fetch_bundle))
self.query_for_devicelists.append(jid) self._query_for_devicelists.append(jid)
def _devicelist_received(self, devicelist, user_data): def _devicelist_received(self, devicelist, user_data):
jid, fetch_bundle = user_data jid, fetch_bundle = user_data
@@ -528,35 +454,25 @@ class OMEMO(BaseModule):
self._process_devicelist_update(str(properties.jid), devicelist, False) self._process_devicelist_update(str(properties.jid), devicelist, False)
def _process_devicelist_update(self, jid, devicelist, fetch_bundle): def _process_devicelist_update(self, jid, devicelist, fetch_bundle):
if jid is None or self._con.get_own_jid().bareMatch(jid): own_devices = jid is None or self._con.get_own_jid().bareMatch(jid)
log.info('%s => Received own device list: %s', if own_devices:
self._account, devicelist) jid = self._own_jid
self.omemo.set_own_devices(devicelist)
self.omemo.store.setActiveState(devicelist, self.own_jid)
# remove contact from list, so on send button pressed log.info('%s => Received device list for %s: %s',
# we query for bundle and build a session self._account, jid, devicelist)
if jid in self.query_for_bundles: self.backend.update_devicelist(jid, devicelist)
self.query_for_bundles.remove(jid)
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 # Our own device_id is not in the list, it could be
# overwritten by some other client # overwritten by some other client
self.set_devicelist() self.set_devicelist()
else: elif fetch_bundle:
log.info('%s => Received device list for %s: %s', self.are_keys_missing(jid)
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)
@staticmethod @staticmethod
def _debug_print_stanza(stanza): def _debug_print_stanza(stanza):
@@ -567,9 +483,5 @@ class OMEMO(BaseModule):
log.debug('-'*15) log.debug('-'*15)
class OMEMOError(Exception):
pass
def get_instance(*args, **kwargs): def get_instance(*args, **kwargs):
return OMEMO(*args, **kwargs), 'OMEMO' return OMEMO(*args, **kwargs), 'OMEMO'

View File

@@ -79,6 +79,7 @@ class UserMessages(IntEnum):
class OmemoPlugin(GajimPlugin): class OmemoPlugin(GajimPlugin):
def init(self): def init(self):
# pylint: disable=attribute-defined-outside-init
if ERROR_MSG: if ERROR_MSG:
self.activatable = False self.activatable = False
self.available_text = ERROR_MSG self.available_text = ERROR_MSG
@@ -101,13 +102,13 @@ class OmemoPlugin(GajimPlugin):
'send_message' + self.encryption_name: ( 'send_message' + self.encryption_name: (
self.before_sendmessage, None), self.before_sendmessage, None),
'encryption_dialog' + self.encryption_name: ( 'encryption_dialog' + self.encryption_name: (
self.on_encryption_button_clicked, None), self._on_encryption_button_clicked, None),
'encryption_state' + self.encryption_name: ( 'encryption_state' + self.encryption_name: (
self.encryption_state, None), self.encryption_state, None),
'update_caps': (self._update_caps, None)} 'update_caps': (self._update_caps, None)}
self.disabled_accounts = [] self.disabled_accounts = []
self.windowinstances = {} self._windows = {}
self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), } self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), }
@@ -122,11 +123,12 @@ class OmemoPlugin(GajimPlugin):
self._load_css() self._load_css()
def _load_css(self): @staticmethod
def _load_css():
path = Path(__file__).parent / 'gtk' / 'style.css' path = Path(__file__).parent / 'gtk' / 'style.css'
try: try:
with open(path, "r") as f: with open(path, "r") as file:
css = f.read() css = file.read()
except Exception as exc: except Exception as exc:
log.error('Error loading css: %s', exc) log.error('Error loading css: %s', exc)
return return
@@ -159,7 +161,8 @@ class OmemoPlugin(GajimPlugin):
continue continue
app.connections[account].get_module('OMEMO').activate() app.connections[account].get_module('OMEMO').activate()
def deactivate(self): @staticmethod
def deactivate():
""" Method called when the Plugin is deactivated in the PluginManager """ Method called when the Plugin is deactivated in the PluginManager
""" """
for account in app.connections: for account in app.connections:
@@ -167,15 +170,17 @@ class OmemoPlugin(GajimPlugin):
continue continue
app.connections[account].get_module('OMEMO').deactivate() app.connections[account].get_module('OMEMO').deactivate()
def _update_caps(self, account): @staticmethod
def _update_caps(account):
if account == 'Local': if account == 'Local':
return return
app.connections[account].get_module('OMEMO').update_caps(account) 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): if isinstance(chat_control, GroupchatControl):
omemo_con = app.connections[chat_control.account].get_module('OMEMO') 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( dialogs.ErrorDialog(
_('Bad Configuration'), _('Bad Configuration'),
_('To use OMEMO in a Groupchat, the Groupchat should be' _('To use OMEMO in a Groupchat, the Groupchat should be'
@@ -183,21 +188,25 @@ class OmemoPlugin(GajimPlugin):
return False return False
return True return True
def _gc_encrypt_message(self, conn, obj, callback): @staticmethod
def _gc_encrypt_message(conn, obj, callback):
if conn.name == 'Local': if conn.name == 'Local':
return 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': if conn.name == 'Local':
return 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): def _file_decryption(self, url, kind, instance, window):
file_crypto.FileDecryption(self).hyperlink_handler( file_crypto.FileDecryption(self).hyperlink_handler(
url, kind, instance, window) 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, thread = threading.Thread(target=self._encrypt_file_thread,
args=(file, callback)) args=(file, callback))
thread.daemon = True thread.daemon = True
@@ -215,29 +224,29 @@ class OmemoPlugin(GajimPlugin):
GLib.idle_add(callback, file) GLib.idle_add(callback, file)
@staticmethod @staticmethod
def encryption_state(chat_control, state): def encryption_state(_chat_control, state):
state['visible'] = True state['visible'] = True
state['authenticated'] = 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) self.show_fingerprint_window(chat_control)
def get_omemo(self, account): @staticmethod
return app.connections[account].get_module('OMEMO').omemo def get_omemo(account):
return app.connections[account].get_module('OMEMO')
def before_sendmessage(self, chat_control): def before_sendmessage(self, chat_control):
account = chat_control.account account = chat_control.account
if account == 'Local': if account == 'Local':
return return
contact = chat_control.contact contact = chat_control.contact
con = app.connections[account].get_module('OMEMO') omemo = self.get_omemo(account)
self.new_fingerprints_available(chat_control) self.new_fingerprints_available(chat_control)
if isinstance(chat_control, GroupchatControl): if isinstance(chat_control, GroupchatControl):
room = chat_control.room_jid room = chat_control.room_jid
missing = True missing = True
for nick in con.groupchat[room]: for jid in omemo.backend.get_muc_members(room):
real_jid = con.groupchat[room][nick] if not omemo.are_keys_missing(jid):
if not con.are_keys_missing(real_jid):
missing = False missing = False
if missing: if missing:
log.info('%s => No Trusted Fingerprints for %s', log.info('%s => No Trusted Fingerprints for %s',
@@ -245,13 +254,13 @@ class OmemoPlugin(GajimPlugin):
self.print_message(chat_control, UserMessages.NO_FINGERPRINTS) self.print_message(chat_control, UserMessages.NO_FINGERPRINTS)
else: else:
# check if we have devices for the contact # check if we have devices for the contact
if not self.get_omemo(account).device_list_for(contact.jid): if not omemo.backend.get_devices(contact.jid):
con.request_devicelist(contact.jid, True) omemo.request_devicelist(contact.jid, True)
self.print_message(chat_control, UserMessages.QUERY_DEVICES) self.print_message(chat_control, UserMessages.QUERY_DEVICES)
chat_control.sendmessage = False chat_control.sendmessage = False
return return
# check if bundles are missing for some devices # 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', log.info('%s => No Trusted Fingerprints for %s',
account, contact.jid) account, contact.jid)
self.print_message(chat_control, UserMessages.NO_FINGERPRINTS) self.print_message(chat_control, UserMessages.NO_FINGERPRINTS)
@@ -266,20 +275,17 @@ class OmemoPlugin(GajimPlugin):
def new_fingerprints_available(self, chat_control): def new_fingerprints_available(self, chat_control):
jid = chat_control.contact.jid jid = chat_control.contact.jid
account = chat_control.account account = chat_control.account
con = app.connections[account].get_module('OMEMO')
omemo = self.get_omemo(account) omemo = self.get_omemo(account)
if isinstance(chat_control, GroupchatControl): if isinstance(chat_control, GroupchatControl):
room_jid = chat_control.room_jid for jid_ in omemo.backend.get_muc_members(chat_control.room_jid,
if room_jid in con.groupchat: without_self=False):
for nick in con.groupchat[room_jid]: fingerprints = omemo.backend.storage.getNewFingerprints(jid_)
real_jid = con.groupchat[room_jid][nick] if fingerprints:
fingerprints = omemo.store. \ self.show_fingerprint_window(
getNewFingerprints(real_jid) chat_control, fingerprints)
if fingerprints: break
self.show_fingerprint_window(
chat_control, fingerprints)
elif not isinstance(chat_control, GroupchatControl): elif not isinstance(chat_control, GroupchatControl):
fingerprints = omemo.store.getNewFingerprints(jid) fingerprints = omemo.backend.storage.getNewFingerprints(jid)
if fingerprints: if fingerprints:
self.show_fingerprint_window( self.show_fingerprint_window(
chat_control, fingerprints) chat_control, fingerprints)
@@ -289,20 +295,21 @@ class OmemoPlugin(GajimPlugin):
account = chat_control.account account = chat_control.account
omemo = self.get_omemo(account) omemo = self.get_omemo(account)
transient = chat_control.parent_win.window transient = chat_control.parent_win.window
if 'dialog' not in self.windowinstances:
if 'dialog' not in self._windows:
is_groupchat = isinstance(chat_control, GroupchatControl) is_groupchat = isinstance(chat_control, GroupchatControl)
self.windowinstances['dialog'] = \ self._windows['dialog'] = \
KeyDialog(self, contact, transient, KeyDialog(self, contact, transient,
self.windowinstances, groupchat=is_groupchat) self._windows, groupchat=is_groupchat)
if fingerprints: if fingerprints:
log.debug('%s => Showing Fingerprint Prompt for %s', log.debug('%s => Showing Fingerprint Prompt for %s',
account, contact.jid) account, contact.jid)
omemo.store.setShownFingerprints(fingerprints) omemo.backend.storage.setShownFingerprints(fingerprints)
else: else:
self.windowinstances['dialog'].present() self._windows['dialog'].present()
self.windowinstances['dialog'].update() self._windows['dialog'].update()
if fingerprints: if fingerprints:
omemo.store.setShownFingerprints(fingerprints) omemo.backend.storage.setShownFingerprints(fingerprints)
@staticmethod @staticmethod
def print_message(chat_control, kind): def print_message(chat_control, kind):