[omemo] Refactor Plugin
This commit is contained in:
124
omemo/backend/devices.py
Normal file
124
omemo/backend/devices.py
Normal 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
|
||||
@@ -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)'''
|
||||
|
||||
@@ -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
|
||||
|
||||
44
omemo/backend/util.py
Normal file
44
omemo/backend/util.py
Normal 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()
|
||||
@@ -16,11 +16,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import binascii
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
from enum import IntEnum, unique
|
||||
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
@@ -29,6 +26,8 @@ from gajim.common import configpaths
|
||||
from gajim.plugins.gui import GajimPluginConfigDialog
|
||||
from gajim.plugins.helpers import get_builder
|
||||
|
||||
from omemo.backend.util import get_fingerprint
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.omemo')
|
||||
|
||||
PILLOW = False
|
||||
@@ -40,13 +39,6 @@ except ImportError as error:
|
||||
log.error('python-qrcode or dependencies of it are not available')
|
||||
|
||||
|
||||
@unique
|
||||
class State(IntEnum):
|
||||
UNTRUSTED = 0
|
||||
TRUSTED = 1
|
||||
UNDECIDED = 2
|
||||
|
||||
|
||||
class OMEMOConfigDialog(GajimPluginConfigDialog):
|
||||
def init(self):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
@@ -62,19 +54,8 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
|
||||
self.plugin.config['DISABLED_ACCOUNTS'] = []
|
||||
self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS']
|
||||
|
||||
log.debug('Disabled Accounts:')
|
||||
log.debug(self.disabled_accounts)
|
||||
|
||||
self.device_model = self._ui.get_object('deviceid_store')
|
||||
|
||||
self.disabled_acc_store = self._ui.get_object('disabled_account_store')
|
||||
self.account_store = self._ui.get_object('account_store')
|
||||
|
||||
self.active_acc_view = self._ui.get_object('active_accounts_view')
|
||||
self.disabled_acc_view = self._ui.get_object('disabled_accounts_view')
|
||||
|
||||
box = self.get_content_area()
|
||||
box.pack_start(self._ui.get_object('notebook1'), True, True, 0)
|
||||
box.pack_start(self._ui.notebook1, True, True, 0)
|
||||
|
||||
self._ui.connect_signals(self)
|
||||
|
||||
@@ -91,7 +72,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
|
||||
self.update_disabled_account_view()
|
||||
|
||||
def is_in_accountstore(self, account):
|
||||
for row in self.account_store:
|
||||
for row in self._ui.account_store:
|
||||
if row[0] == account:
|
||||
return True
|
||||
return False
|
||||
@@ -103,32 +84,34 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
|
||||
if account == 'Local':
|
||||
continue
|
||||
if not self.is_in_accountstore(account):
|
||||
self.account_store.append(row=(account,))
|
||||
self._ui.account_store.append(row=(account,))
|
||||
|
||||
def update_account_combobox(self):
|
||||
if self.plugin_active is False:
|
||||
return
|
||||
if len(self.account_store) > 0:
|
||||
self._ui.get_object('account_combobox').set_active(0)
|
||||
if self._ui.account_store:
|
||||
self._ui.account_combobox.set_active(0)
|
||||
else:
|
||||
self.account_combobox_changed_cb(
|
||||
self._ui.get_object('account_combobox'))
|
||||
self.account_combobox_changed_cb(self._ui.account_combobox)
|
||||
|
||||
def account_combobox_changed_cb(self, box, *args):
|
||||
self.update_context_list()
|
||||
|
||||
def get_qrcode(self, jid, sid, fingerprint):
|
||||
@staticmethod
|
||||
def _get_qrcode(jid, sid, identity_key):
|
||||
fingerprint = get_fingerprint(identity_key)
|
||||
file_name = 'omemo_{}.png'.format(jid)
|
||||
path = os.path.join(
|
||||
configpaths.get('MY_DATA'), file_name)
|
||||
|
||||
ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint)
|
||||
log.debug('Verification String: ' + ver_string)
|
||||
log.debug('Verification String: %s', ver_string)
|
||||
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
qr = qrcode.QRCode(version=None, error_correction=2, box_size=4, border=1)
|
||||
qr = qrcode.QRCode(version=None, error_correction=2,
|
||||
box_size=4, border=1)
|
||||
qr.add_data(ver_string)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image()
|
||||
@@ -136,99 +119,89 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
|
||||
return path
|
||||
|
||||
def update_disabled_account_view(self):
|
||||
self.disabled_acc_store.clear()
|
||||
self._ui.disabled_account_store.clear()
|
||||
for account in self.disabled_accounts:
|
||||
self.disabled_acc_store.append(row=(account,))
|
||||
self._ui.disabled_account_store.append(row=(account,))
|
||||
|
||||
def activate_accounts_btn_clicked(self, button, *args):
|
||||
mod, paths = self.disabled_acc_view.get_selection().get_selected_rows()
|
||||
def activate_accounts_btn_clicked(self, _button, *args):
|
||||
selection = self._ui.disabled_accounts_view.get_selection()
|
||||
mod, paths = selection.get_selected_rows()
|
||||
for path in paths:
|
||||
it = mod.get_iter(path)
|
||||
account = mod.get(it, 0)
|
||||
if account[0] in self.disabled_accounts and \
|
||||
not self.is_in_accountstore(account[0]):
|
||||
self.account_store.append(row=(account[0],))
|
||||
self._ui.account_store.append(row=(account[0],))
|
||||
self.disabled_accounts.remove(account[0])
|
||||
self.update_disabled_account_view()
|
||||
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
|
||||
self.update_account_combobox()
|
||||
|
||||
def disable_accounts_btn_clicked(self, button, *args):
|
||||
mod, paths = self.active_acc_view.get_selection().get_selected_rows()
|
||||
def disable_accounts_btn_clicked(self, _button, *args):
|
||||
selection = self._ui.active_accounts_view.get_selection()
|
||||
mod, paths = selection.get_selected_rows()
|
||||
for path in paths:
|
||||
it = mod.get_iter(path)
|
||||
account = mod.get(it, 0)
|
||||
if account[0] not in self.disabled_accounts and \
|
||||
self.is_in_accountstore(account[0]):
|
||||
self.disabled_accounts.append(account[0])
|
||||
self.account_store.remove(it)
|
||||
self._ui.account_store.remove(it)
|
||||
self.update_disabled_account_view()
|
||||
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
|
||||
self.update_account_combobox()
|
||||
|
||||
def cleardevice_button_clicked_cb(self, button, *args):
|
||||
active = self._ui.get_object('account_combobox').get_active()
|
||||
account = self.account_store[active][0]
|
||||
app.connections[account].get_module('OMEMO').set_devicelist(new=True)
|
||||
active = self._ui.account_combobox.get_active()
|
||||
account = self._ui.account_store[active][0]
|
||||
app.connections[account].get_module('OMEMO').clear_devicelist()
|
||||
self.update_context_list()
|
||||
|
||||
def refresh_button_clicked_cb(self, button, *args):
|
||||
self.update_context_list()
|
||||
|
||||
def update_context_list(self):
|
||||
self.device_model.clear()
|
||||
self.qrcode = self._ui.get_object('qrcode')
|
||||
self.qrinfo = self._ui.get_object('qrinfo')
|
||||
if len(self.account_store) == 0:
|
||||
self._ui.get_object('ID').set_markup('')
|
||||
self._ui.get_object('fingerprint_label').set_markup('')
|
||||
self._ui.get_object('refresh').set_sensitive(False)
|
||||
self._ui.get_object('cleardevice_button').set_sensitive(False)
|
||||
self._ui.get_object('qrcode').clear()
|
||||
self._ui.deviceid_store.clear()
|
||||
|
||||
if not self._ui.account_store:
|
||||
self._ui.ID.set_markup('')
|
||||
self._ui.fingerprint_label.set_markup('')
|
||||
self._ui.refresh.set_sensitive(False)
|
||||
self._ui.cleardevice_button.set_sensitive(False)
|
||||
self._ui.qrcode.clear()
|
||||
return
|
||||
active = self._ui.get_object('account_combobox').get_active()
|
||||
account = self.account_store[active][0]
|
||||
active = self._ui.account_combobox.get_active()
|
||||
account = self._ui.account_store[active][0]
|
||||
|
||||
# Set buttons active
|
||||
self._ui.get_object('refresh').set_sensitive(True)
|
||||
self._ui.refresh.set_sensitive(True)
|
||||
if account == 'Local':
|
||||
self._ui.get_object('cleardevice_button').set_sensitive(False)
|
||||
self._ui.cleardevice_button.set_sensitive(False)
|
||||
else:
|
||||
self._ui.get_object('cleardevice_button').set_sensitive(True)
|
||||
self._ui.cleardevice_button.set_sensitive(True)
|
||||
|
||||
# Set FPR Label and DeviceID
|
||||
state = self.plugin.get_omemo(account)
|
||||
deviceid = state.own_device_id
|
||||
self._ui.get_object('ID').set_markup('<tt>%s</tt>' % deviceid)
|
||||
omemo = self.plugin.get_omemo(account)
|
||||
self._ui.ID.set_markup('<tt>%s</tt>' % omemo.backend.own_device)
|
||||
|
||||
ownfpr = binascii.hexlify(state.store.getIdentityKeyPair()
|
||||
.getPublicKey().serialize()).decode('utf-8')
|
||||
human_ownfpr = self.human_hash(ownfpr[2:])
|
||||
self._ui.get_object('fingerprint_label').set_markup('<tt>%s</tt>'
|
||||
% human_ownfpr)
|
||||
identity_key = omemo.backend.storage.getIdentityKeyPair()
|
||||
fpr = get_fingerprint(identity_key, formatted=True)
|
||||
self._ui.fingerprint_label.set_markup('<tt>%s</tt>' % fpr)
|
||||
|
||||
own_jid = app.get_jid_from_account(account)
|
||||
# Set Device ID List
|
||||
for item in state.own_devices:
|
||||
self.device_model.append([item])
|
||||
for item in omemo.backend.get_devices(own_jid, without_self=True):
|
||||
self._ui.deviceid_store.append([item])
|
||||
|
||||
# Set QR Verification Code
|
||||
if PILLOW:
|
||||
path = self.get_qrcode(
|
||||
app.get_jid_from_account(account), deviceid, ownfpr[2:])
|
||||
path = self._get_qrcode(own_jid,
|
||||
omemo.backend.own_device,
|
||||
identity_key)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
|
||||
self.qrcode.set_from_pixbuf(pixbuf)
|
||||
self.qrcode.show()
|
||||
self.qrinfo.hide()
|
||||
self._ui.qrcode.set_from_pixbuf(pixbuf)
|
||||
self._ui.qrcode.show()
|
||||
self._ui.qrinfo.hide()
|
||||
else:
|
||||
self.qrcode.hide()
|
||||
self.qrinfo.show()
|
||||
|
||||
def human_hash(self, fpr):
|
||||
fpr = fpr.upper()
|
||||
fplen = len(fpr)
|
||||
wordsize = fplen // 8
|
||||
buf = ''
|
||||
for w in range(0, fplen, wordsize):
|
||||
buf += '{0} '.format(fpr[w:w + wordsize])
|
||||
buf = textwrap.fill(buf, width=36)
|
||||
return buf.rstrip()
|
||||
self._ui.qrcode.hide()
|
||||
self._ui.qrinfo.show()
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import binascii
|
||||
import textwrap
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
@@ -27,6 +25,7 @@ from gajim.plugins.plugins_i18n import _
|
||||
from omemo.gtk.util import DialogButton, ButtonAction
|
||||
from omemo.gtk.util import NewConfirmationDialog
|
||||
from omemo.gtk.util import Trust
|
||||
from omemo.backend.util import get_fingerprint
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system.omemo')
|
||||
|
||||
@@ -44,23 +43,23 @@ TRUST_DATA = {
|
||||
|
||||
|
||||
class KeyDialog(Gtk.Dialog):
|
||||
def __init__(self, plugin, contact, transient, windowinstances,
|
||||
def __init__(self, plugin, contact, transient, windows,
|
||||
groupchat=False):
|
||||
super().__init__(title=_('OMEMO Fingerprints'), destroy_with_parent=True)
|
||||
super().__init__(title=_('OMEMO Fingerprints'),
|
||||
destroy_with_parent=True)
|
||||
|
||||
self.set_transient_for(transient)
|
||||
self.set_resizable(True)
|
||||
self.set_default_size(-1, 400)
|
||||
self.set_default_size(500, 450)
|
||||
|
||||
self.get_style_context().add_class('omemo-key-dialog')
|
||||
|
||||
self._groupchat = groupchat
|
||||
self._contact = contact
|
||||
self._windowinstances = windowinstances
|
||||
self._windows = windows
|
||||
self._account = self._contact.account.name
|
||||
self._plugin = plugin
|
||||
self._con = app.connections[self._account].get_module('OMEMO')
|
||||
self.omemostate = self._plugin.get_omemo(self._account)
|
||||
self._omemo = self._plugin.get_omemo(self._account)
|
||||
self._own_jid = app.get_jid_from_account(self._account)
|
||||
|
||||
# Header
|
||||
@@ -88,9 +87,8 @@ class KeyDialog(Gtk.Dialog):
|
||||
omemo_pixbuf = GdkPixbuf.Pixbuf.new_from_file(omemo_img_path)
|
||||
self._omemo_logo.set_from_pixbuf(omemo_pixbuf)
|
||||
|
||||
ownfpr = binascii.hexlify(self.omemostate.store.getIdentityKeyPair()
|
||||
.getPublicKey().serialize()).decode('utf-8')
|
||||
ownfpr_format = KeyRow._format_fingerprint(ownfpr[2:])
|
||||
identity_key = self._omemo.backend.storage.getIdentityKeyPair()
|
||||
ownfpr_format = get_fingerprint(identity_key, formatted=True)
|
||||
self._ownfpr = Gtk.Label(label=ownfpr_format)
|
||||
self._ownfpr.get_style_context().add_class('omemo-mono')
|
||||
self._ownfpr.set_selectable(True)
|
||||
@@ -113,52 +111,43 @@ class KeyDialog(Gtk.Dialog):
|
||||
self.show_all()
|
||||
|
||||
def update(self):
|
||||
self._listbox.foreach(lambda row: self._listbox.remove(row))
|
||||
self._listbox.foreach(self._listbox.remove)
|
||||
self._load_fingerprints(self._own_jid)
|
||||
self._load_fingerprints(self._contact.jid, self._groupchat is True)
|
||||
|
||||
def _load_fingerprints(self, contact_jid, groupchat=False):
|
||||
from axolotl.state.sessionrecord import SessionRecord
|
||||
state = self.omemostate
|
||||
|
||||
if groupchat:
|
||||
contact_jids = []
|
||||
for nick in self._con.groupchat[contact_jid]:
|
||||
real_jid = self._con.groupchat[contact_jid][nick]
|
||||
if real_jid == self._own_jid:
|
||||
continue
|
||||
contact_jids.append(real_jid)
|
||||
session_db = state.store.getSessionsFromJids(contact_jids)
|
||||
members = list(self._omemo.backend.get_muc_members(contact_jid))
|
||||
sessions = self._omemo.backend.storage.getSessionsFromJids(members)
|
||||
else:
|
||||
session_db = state.store.getSessionsFromJid(contact_jid)
|
||||
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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user