From a58ff4f6e850b77b13b52418eb7be6a57fb2741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 24 Jan 2017 11:29:58 +0100 Subject: [PATCH 1/5] [omemo] Port python-omemo changes from master --- omemo/omemo/aes_gcm.py | 7 +- omemo/omemo/aes_gcm_fallback.py | 24 ++-- omemo/omemo/aes_gcm_native.py | 20 +++- omemo/omemo/liteaxolotlstore.py | 18 +++ omemo/omemo/liteidentitykeystore.py | 13 +- omemo/omemo/litesessionstore.py | 57 +++++---- omemo/omemo/sql.py | 8 ++ omemo/omemo/state.py | 180 ++++++++++++++++++++-------- 8 files changed, 241 insertions(+), 86 deletions(-) diff --git a/omemo/omemo/aes_gcm.py b/omemo/omemo/aes_gcm.py index 67e7328..38394bd 100644 --- a/omemo/omemo/aes_gcm.py +++ b/omemo/omemo/aes_gcm.py @@ -18,6 +18,7 @@ # +import sys import logging log = logging.getLogger('gajim.plugin_system.omemo') try: @@ -35,7 +36,11 @@ def encrypt(key, iv, plaintext): def decrypt(key, iv, ciphertext): - return aes_decrypt(key, iv, ciphertext) + plaintext = aes_decrypt(key, iv, ciphertext).decode('utf-8') + if sys.version_info < (3, 0): + return unicode(plaintext) + else: + return plaintext class NoValidSessions(Exception): diff --git a/omemo/omemo/aes_gcm_fallback.py b/omemo/omemo/aes_gcm_fallback.py index f157a22..0117c9f 100644 --- a/omemo/omemo/aes_gcm_fallback.py +++ b/omemo/omemo/aes_gcm_fallback.py @@ -29,11 +29,14 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import logging from struct import pack, unpack from Crypto.Cipher import AES from Crypto.Util import strxor +log = logging.getLogger('gajim.plugin_system.omemo') + def gcm_rightshift(vec): for x in range(15, 0, -1): @@ -140,13 +143,20 @@ def gcm_encrypt(k, iv, plaintext, auth_data): def aes_encrypt(key, nonce, plaintext): """ Use AES128 GCM with the given key and iv to encrypt the payload. """ - c, t = gcm_encrypt(key, nonce, plaintext, '') - result = c + t - return result + return gcm_encrypt(key, nonce, plaintext, '') - -def aes_decrypt(key, nonce, payload): +def aes_decrypt(_key, nonce, payload): """ Use AES128 GCM with the given key and iv to decrypt the payload. """ - ciphertext = payload[:-16] - mac = payload[-16:] + if len(_key) >= 32: + # XEP-0384 + log.debug('XEP Compliant Key/Tag') + ciphertext = payload + key = _key[:16] + mac = _key[16:] + else: + # Legacy + log.debug('Legacy Key/Tag') + ciphertext = payload[:-16] + key = _key + mac = payload[-16:] return gcm_decrypt(key, nonce, ciphertext, '', mac) diff --git a/omemo/omemo/aes_gcm_native.py b/omemo/omemo/aes_gcm_native.py index 7781573..fc80c31 100644 --- a/omemo/omemo/aes_gcm_native.py +++ b/omemo/omemo/aes_gcm_native.py @@ -19,6 +19,7 @@ import os +import logging from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers.modes import GCM @@ -32,11 +33,22 @@ if os.name == 'nt': else: from cryptography.hazmat.backends import default_backend +log = logging.getLogger('gajim.plugin_system.omemo') -def aes_decrypt(key, iv, payload): +def aes_decrypt(_key, iv, payload): """ Use AES128 GCM with the given key and iv to decrypt the payload. """ - data = payload[:-16] - tag = payload[-16:] + if len(_key) >= 32: + # XEP-0384 + log.debug('XEP Compliant Key/Tag') + data = payload + key = _key[:16] + tag = _key[16:] + else: + # Legacy + log.debug('Legacy Key/Tag') + data = payload[:-16] + key = _key + tag = payload[-16:] if os.name == 'nt': _backend = backend else: @@ -58,4 +70,4 @@ def aes_encrypt(key, iv, plaintext): algorithms.AES(key), GCM(iv), backend=_backend).encryptor() - return encryptor.update(plaintext) + encryptor.finalize() + encryptor.tag + return encryptor.update(plaintext) + encryptor.finalize(), encryptor.tag diff --git a/omemo/omemo/liteaxolotlstore.py b/omemo/omemo/liteaxolotlstore.py index 64f14b3..968faee 100644 --- a/omemo/omemo/liteaxolotlstore.py +++ b/omemo/omemo/liteaxolotlstore.py @@ -83,10 +83,16 @@ class LiteAxolotlStore(AxolotlStore): def saveIdentity(self, recepientId, identityKey): self.identityKeyStore.saveIdentity(recepientId, identityKey) + def deleteIdentity(self, recipientId, identityKey): + self.identityKeyStore.deleteIdentity(recipientId, identityKey) + def isTrustedIdentity(self, recepientId, identityKey): return self.identityKeyStore.isTrustedIdentity(recepientId, identityKey) + def setTrust(self, identityKey, trust): + return self.identityKeyStore.setTrust(identityKey, trust) + def getTrustedFingerprints(self, jid): return self.identityKeyStore.getTrustedFingerprints(jid) @@ -127,6 +133,9 @@ class LiteAxolotlStore(AxolotlStore): # TODO Reuse this return self.sessionStore.getSubDeviceSessions(recepientId) + def getJidFromDevice(self, device_id): + return self.sessionStore.getJidFromDevice(device_id) + def storeSession(self, recepientId, deviceId, sessionRecord): self.sessionStore.storeSession(recepientId, deviceId, sessionRecord) @@ -139,6 +148,15 @@ class LiteAxolotlStore(AxolotlStore): def deleteAllSessions(self, recepientId): self.sessionStore.deleteAllSessions(recepientId) + def getSessionsFromJid(self, recipientId): + return self.sessionStore.getSessionsFromJid(recipientId) + + def getSessionsFromJids(self, recipientId): + return self.sessionStore.getSessionsFromJids(recipientId) + + def getAllSessions(self): + return self.sessionStore.getAllSessions() + def loadSignedPreKey(self, signedPreKeyId): return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId) diff --git a/omemo/omemo/liteidentitykeystore.py b/omemo/omemo/liteidentitykeystore.py index 29974bc..6d51e17 100644 --- a/omemo/omemo/liteidentitykeystore.py +++ b/omemo/omemo/liteidentitykeystore.py @@ -86,6 +86,13 @@ class LiteIdentityKeyStore(IdentityKeyStore): return result is not None + def deleteIdentity(self, recipientId, identityKey): + q = "DELETE FROM identities WHERE recipient_id = ? AND public_key = ?" + c = self.dbConn.cursor() + c.execute(q, (recipientId, + identityKey.getPublicKey().serialize())) + self.dbConn.commit() + def isTrustedIdentity(self, recipientId, identityKey): q = "SELECT trust FROM identities WHERE recipient_id = ? " \ "AND public_key = ?" @@ -160,8 +167,8 @@ class LiteIdentityKeyStore(IdentityKeyStore): c.execute(q, fingerprints) self.dbConn.commit() - def setTrust(self, _id, trust): - q = "UPDATE identities SET trust = ? WHERE _id = ?" + def setTrust(self, identityKey, trust): + q = "UPDATE identities SET trust = ? WHERE public_key = ?" c = self.dbConn.cursor() - c.execute(q, (trust, _id)) + c.execute(q, (trust, identityKey.getPublicKey().serialize())) self.dbConn.commit() diff --git a/omemo/omemo/litesessionstore.py b/omemo/omemo/litesessionstore.py index d8ef66c..062f517 100644 --- a/omemo/omemo/litesessionstore.py +++ b/omemo/omemo/litesessionstore.py @@ -48,6 +48,14 @@ class LiteSessionStore(SessionStore): deviceIds = [r[0] for r in result] return deviceIds + def getJidFromDevice(self, device_id): + q = "SELECT recipient_id from sessions WHERE device_id = ?" + c = self.dbConn.cursor() + c.execute(q, (device_id, )) + result = c.fetchone() + + return result[0].decode('utf-8') + def getActiveDeviceTuples(self): q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1" c = self.dbConn.cursor() @@ -82,6 +90,33 @@ class LiteSessionStore(SessionStore): self.dbConn.cursor().execute(q, (recipientId, )) self.dbConn.commit() + def getAllSessions(self): + q = "SELECT _id, recipient_id, device_id, record, active from sessions" + c = self.dbConn.cursor() + result = [] + for row in c.execute(q): + result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4])) + return result + + def getSessionsFromJid(self, recipientId): + q = "SELECT _id, recipient_id, device_id, record, active from sessions" \ + " WHERE recipient_id = ?" + c = self.dbConn.cursor() + result = [] + for row in c.execute(q, (recipientId,)): + result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4])) + return result + + def getSessionsFromJids(self, recipientId): + q = "SELECT _id, recipient_id, device_id, record, active from sessions" \ + " WHERE recipient_id IN ({})" \ + .format(', '.join(['?'] * len(recipientId))) + c = self.dbConn.cursor() + result = [] + for row in c.execute(q, recipientId): + result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4])) + return result + def setActiveState(self, deviceList, jid): c = self.dbConn.cursor() @@ -96,28 +131,6 @@ class LiteSessionStore(SessionStore): c.execute(q, deviceList) self.dbConn.commit() - def getActiveSessionsKeys(self, recipientId): - q = "SELECT record FROM sessions WHERE active = 1 AND recipient_id = ?" - c = self.dbConn.cursor() - result = [] - for row in c.execute(q, (recipientId,)): - public_key = (SessionRecord(serialized=row[0]). - getSessionState().getRemoteIdentityKey(). - getPublicKey()) - result.append(public_key.serialize()) - return result - - def getAllActiveSessionsKeys(self): - q = "SELECT record FROM sessions WHERE active = 1" - c = self.dbConn.cursor() - result = [] - for row in c.execute(q): - public_key = (SessionRecord(serialized=row[0]). - getSessionState().getRemoteIdentityKey(). - getPublicKey()) - result.append(public_key.serialize()) - return result - def getInactiveSessionsKeys(self, recipientId): q = "SELECT record FROM sessions WHERE active = 0 AND recipient_id = ?" c = self.dbConn.cursor() diff --git a/omemo/omemo/sql.py b/omemo/omemo/sql.py index 25571f8..6b941cf 100644 --- a/omemo/omemo/sql.py +++ b/omemo/omemo/sql.py @@ -29,6 +29,14 @@ class SQLDatabase(): self.dbConn = dbConn self.createDb() self.migrateDb() + c = self.dbConn.cursor() + c.execute("PRAGMA synchronous=NORMAL;") + c.execute("PRAGMA journal_mode;") + mode = c.fetchone()[0] + # WAL is a persistent DB mode, dont override it if user has set it + if mode != 'wal': + c.execute("PRAGMA journal_mode=MEMORY;") + self.dbConn.commit() def createDb(self): if user_version(self.dbConn) == 0: diff --git a/omemo/omemo/state.py b/omemo/omemo/state.py index dd06e05..2a9d5d0 100644 --- a/omemo/omemo/state.py +++ b/omemo/omemo/state.py @@ -200,8 +200,8 @@ class OmemoState: key = self.handleWhisperMessage(sender_jid, sid, encrypted_key) except (NoSessionException, InvalidMessageException) as e: log.warning('No Session found ' + e.message) - log.warning('sender_jid => ' + str(sender_jid) + - ' sid =>' + sid) + log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' + + str(sid)) return except (DuplicateMessageException) as e: log.warning('Duplicate message found ' + str(e.args)) @@ -211,7 +211,7 @@ class OmemoState: log.warning('Duplicate message found ' + str(e.args)) return - result = decrypt(key, iv, payload).decode('utf-8') + result = decrypt(key, iv, payload) log.debug("Decrypted Message => " + result) return result @@ -226,43 +226,44 @@ class OmemoState: log.error('No known devices') return - for dev in devices_list: - self.get_session_cipher(jid, dev) - session_ciphers = self.session_ciphers[jid] - if not session_ciphers: - log.warning('No session ciphers for ' + jid) - return + payload, tag = encrypt(key, iv, plaintext) + + # for XEP-384 Compliance uncomment + # key += tag + payload += tag # Encrypt the message key with for each of receivers devices - for rid, cipher in session_ciphers.items(): + for device in devices_list: try: - if self.isTrusted(cipher) == TRUSTED: - encrypted_keys[rid] = cipher.encrypt(key).serialize() + if self.isTrusted(jid, device) == TRUSTED: + cipher = self.get_session_cipher(jid, device) + cipher_key = cipher.encrypt(key) + prekey = isinstance(cipher_key, PreKeyWhisperMessage) + encrypted_keys[device] = (cipher_key.serialize(), prekey) else: log.debug('Skipped Device because Trust is: ' + - str(self.isTrusted(cipher))) + str(self.isTrusted(jid, device))) except: - log.warning('Failed to find key for device ' + str(rid)) + log.warning('Failed to find key for device ' + str(device)) if len(encrypted_keys) == 0: - log_msg = 'Encrypted keys empty' - log.error(log_msg) - raise NoValidSessions(log_msg) + log.error('Encrypted keys empty') + raise NoValidSessions('Encrypted keys empty') 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: + for device in my_other_devices: try: - cipher = self.get_session_cipher(from_jid, dev) - if self.isTrusted(cipher) == TRUSTED: - encrypted_keys[dev] = cipher.encrypt(key).serialize() + if self.isTrusted(from_jid, device) == TRUSTED: + cipher = self.get_session_cipher(from_jid, device) + cipher_key = cipher.encrypt(key) + prekey = isinstance(cipher_key, PreKeyWhisperMessage) + encrypted_keys[device] = (cipher_key.serialize(), prekey) else: log.debug('Skipped own Device because Trust is: ' + - str(self.isTrusted(cipher))) + str(self.isTrusted(from_jid, device))) except: - log.warning('Failed to find key for device ' + str(dev)) - - payload = encrypt(key, iv, plaintext) + log.warning('Failed to find key for device ' + str(device)) result = {'sid': self.own_device_id, 'keys': encrypted_keys, @@ -273,14 +274,109 @@ class OmemoState: log.debug('Finished encrypting message') return result - def isTrusted(self, cipher): - self.cipher = cipher - self.state = self.cipher.sessionStore. \ - loadSession(self.cipher.recipientId, self.cipher.deviceId). \ - getSessionState() - self.key = self.state.getRemoteIdentityKey() - return self.store.identityKeyStore. \ - isTrustedIdentity(self.cipher.recipientId, self.key) + def create_gc_msg(self, from_jid, jid, plaintext): + key = get_random_bytes(16) + iv = get_random_bytes(16) + encrypted_keys = {} + room = jid + encrypted_jids = [] + + devices_list = self.device_list_for(jid, True) + + if len(devices_list) == 0: + log.error('No known devices') + return + + payload, tag = encrypt(key, iv, plaintext) + + # for XEP-384 Compliance uncomment + # key += tag + payload += tag + + 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.plugin.groupchat[room]: + jid_to = self.plugin.groupchat[room][nick] + if jid_to == self.own_jid: + continue + if jid_to in encrypted_jids: # We already encrypted to this JID + continue + for rid, cipher in self.session_ciphers[jid_to].items(): + try: + if self.isTrusted(jid_to, rid) == TRUSTED: + cipher_key = cipher.encrypt(key) + prekey = isinstance(cipher_key, PreKeyWhisperMessage) + encrypted_keys[rid] = (cipher_key.serialize(), prekey) + else: + log.debug('Skipped Device because Trust is: ' + + str(self.isTrusted(jid_to, rid))) + except: + log.exception('ERROR:') + log.warning('Failed to find key for device ' + + str(rid)) + encrypted_jids.append(jid_to) + if len(encrypted_keys) == 0: + log_msg = 'Encrypted keys empty' + log.error(log_msg) + raise NoValidSessions(log_msg) + + 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(key) + prekey = isinstance(cipher_key, PreKeyWhisperMessage) + encrypted_keys[dev] = (cipher_key.serialize(), prekey) + else: + log.debug('Skipped own Device because Trust is: ' + + str(self.isTrusted(from_jid, dev))) + except: + log.exception('ERROR:') + log.warning('Failed to find key for device ' + str(dev)) + + result = {'sid': self.own_device_id, + 'keys': encrypted_keys, + 'jid': jid, + 'iv': iv, + 'payload': payload} + + log.debug('Finished encrypting message') + return result + + 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.plugin.groupchat[room]: + jid_to = self.plugin.groupchat[room][nick] + if jid_to == self.own_jid: + continue + for device in self.device_ids[jid_to]: + devicelist.append((jid_to, device)) + 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) @@ -296,20 +392,6 @@ class OmemoState: return undecided - def device_list_for(self, jid): - """ Return a list of known device ids for the specified jid. - - Parameters - ---------- - jid : string - The contacts jid - """ - 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 devices_without_sessions(self, jid): """ List device_ids for the given jid which have no axolotl session. @@ -364,10 +446,10 @@ class OmemoState: def handleWhisperMessage(self, recipient_id, device_id, key): whisperMessage = WhisperMessage(serialized=key) - sessionCipher = self.get_session_cipher(recipient_id, device_id) log.debug(self.account + " => Received WhisperMessage from " + recipient_id) - if self.isTrusted(sessionCipher) >= TRUSTED: + if self.isTrusted(recipient_id, device_id): + sessionCipher = self.get_session_cipher(recipient_id, device_id) key = sessionCipher.decryptMsg(whisperMessage, textMsg=False) return key else: From cc04e3ac5ccb28f4cfa5f11d36afb852bd93af83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 24 Jan 2017 11:37:59 +0100 Subject: [PATCH 2/5] [omemo] Port omemoplugin changes from master --- omemo/__init__.py | 884 +------------------------------- omemo/omemoplugin.py | 1135 ++++++++++++++++++++++++++++++++++++++++++ omemo/xmpp.py | 12 +- 3 files changed, 1144 insertions(+), 887 deletions(-) create mode 100644 omemo/omemoplugin.py diff --git a/omemo/__init__.py b/omemo/__init__.py index 0220169..47757fc 100644 --- a/omemo/__init__.py +++ b/omemo/__init__.py @@ -1,883 +1 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Bahtiar `kalkin-` Gadimov -# Copyright 2015 Daniel Gultsch -# -# This file is part of Gajim-OMEMO plugin. -# -# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# the Gajim-OMEMO plugin. If not, see . -# - -import logging -import os -import sqlite3 - -from common import caps_cache, gajim, ged -from common.pep import SUPPORTED_PERSONAL_USER_EVENTS -from plugins import GajimPlugin -from plugins.helpers import log_calls -from nbxmpp.simplexml import Node -from nbxmpp import NS_CORRECT - -from . import ui -from .ui import Ui -from .xmpp import ( - NS_NOTIFY, NS_OMEMO, NS_EME, BundleInformationAnnouncement, - BundleInformationQuery, DeviceListAnnouncement, DevicelistQuery, - DevicelistPEP, OmemoMessage, successful, unpack_device_bundle, - unpack_device_list_update, unpack_encrypted) - -# from common import demandimport -# demandimport.enable() -# demandimport.ignore += ['_imp'] - - -IQ_CALLBACK = {} - -AXOLOTL_MISSING = 'You are missing Python-Axolotl or use an outdated version' -PROTOBUF_MISSING = 'OMEMO cant import Google Protobuf, you can find help in ' \ - 'the GitHub Wiki' -GAJIM_VERSION = 'OMEMO only works with the latest Gajim version, get the ' \ - 'latest version from gajim.org' -ERROR_MSG = '' - -NS_HINTS = 'urn:xmpp:hints' -NS_PGP = 'urn:xmpp:openpgp:0' -DB_DIR = gajim.gajimpaths.data_root - -log = logging.getLogger('gajim.plugin_system.omemo') - - -try: - from .omemo.state import OmemoState -except Exception as e: - log.error(e) - ERROR_MSG = 'Error: {}'.format(e) - -try: - import google.protobuf -except Exception as e: - log.error(e) - ERROR_MSG = PROTOBUF_MISSING - -try: - SETUPTOOLS_MISSING = False - from pkg_resources import parse_version -except Exception as e: - SETUPTOOLS_MISSING = True - ERROR_MSG = 'You are missing the Setuptools package.' - -if not SETUPTOOLS_MISSING: - try: - import axolotl - if parse_version(axolotl.__version__) < parse_version('0.1.35'): - ERROR_MSG = AXOLOTL_MISSING - except Exception as e: - log.error(e) - ERROR_MSG = AXOLOTL_MISSING - -# pylint: disable=no-init -# pylint: disable=attribute-defined-outside-init - - -class OmemoPlugin(GajimPlugin): - - omemo_states = {} - ui_list = {} - - @log_calls('OmemoPlugin') - def init(self): - """ Init """ - if ERROR_MSG: - self.activatable = False - self.available_text = ERROR_MSG - return - self.events_handlers = { - 'mam-message-received': (ged.PRECORE, self.mam_message_received), - 'message-received': (ged.PRECORE, self.message_received), - 'pep-received': (ged.PRECORE, self.handle_device_list_update), - 'raw-iq-received': (ged.PRECORE, self.handle_iq_received), - 'signed-in': (ged.PRECORE, self.signed_in), - 'stanza-message-outgoing': - (ged.PRECORE, self.handle_outgoing_stanza), - 'message-outgoing': - (ged.PRECORE, self.handle_outgoing_event), - } - self.config_dialog = ui.OMEMOConfigDialog(self) - self.gui_extension_points = {'chat_control': (self.connect_ui, - self.disconnect_ui)} - SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP) - self.plugin = self - self.announced = [] - self.query_for_bundles = [] - - @log_calls('OmemoPlugin') - def get_omemo_state(self, account): - """ Returns the the OmemoState for the specified account. - Creates the OmemoState if it does not exist yet. - - Parameters - ---------- - account : str - the account name - - Returns - ------- - OmemoState - """ - if account not in self.omemo_states: - self.deactivate_gajim_e2e(account) - db_path = os.path.join(DB_DIR, 'omemo_' + account + '.db') - conn = sqlite3.connect(db_path, check_same_thread=False) - - my_jid = gajim.get_jid_from_account(account) - - self.omemo_states[account] = OmemoState(my_jid, conn, account, - self.plugin) - - return self.omemo_states[account] - - @staticmethod - def deactivate_gajim_e2e(account): - """ Deativates E2E encryption in Gajim """ - gajim.config.set_per('accounts', account, - 'autonegotiate_esessions', False) - gajim.config.set_per('accounts', account, - 'enable_esessions', False) - log.info(str(account) + " => Gajim E2E encryption disabled") - - @log_calls('OmemoPlugin') - def signed_in(self, event): - """ Method called on SignIn - - Parameters - ---------- - event : SignedInEvent - """ - account = event.conn.name - log.debug(account + - ' => Announce Support after Sign In') - self.query_for_bundles = [] - self.announced = [] - self.announced.append(account) - self.publish_bundle(account) - self.query_own_devicelist(account) - - @log_calls('OmemoPlugin') - def activate(self): - """ Method called when the Plugin is activated in the PluginManager - """ - self.query_for_bundles = [] - if NS_NOTIFY not in gajim.gajim_common_features: - gajim.gajim_common_features.append(NS_NOTIFY) - self._compute_caps_hash() - # Publish bundle information - for account in gajim.connections: - if account not in self.announced: - if gajim.account_is_connected(account): - log.debug(account + - ' => Announce Support after Plugin Activation') - self.announced.append(account) - self.publish_bundle(account) - self.query_own_devicelist(account) - - @log_calls('OmemoPlugin') - def deactivate(self): - """ Method called when the Plugin is deactivated in the PluginManager - - Removes OMEMO from the Entity Capabilities list - """ - if NS_NOTIFY in gajim.gajim_common_features: - gajim.gajim_common_features.remove(NS_NOTIFY) - self._compute_caps_hash() - - @staticmethod - def _compute_caps_hash(): - """ Computes the hash for Entity Capabilities and publishes it """ - for acc in gajim.connections: - gajim.caps_hash[acc] = caps_cache.compute_caps_hash( - [gajim.gajim_identity], - gajim.gajim_common_features + - gajim.gajim_optional_features[acc]) - # re-send presence with new hash - connected = gajim.connections[acc].connected - if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible': - gajim.connections[acc].change_status( - gajim.SHOW_LIST[connected], gajim.connections[acc].status) - - @log_calls('OmemoPlugin') - def mam_message_received(self, msg): - """ Handles an incoming MAM message - - Payload is decrypted and the plaintext is written into the - event object. Afterwards the event is passed on further to Gajim. - - Parameters - ---------- - msg : MamMessageReceivedEvent - - Returns - ------- - Return means that the Event is passed on to Gajim - """ - if msg.msg_.getTag('openpgp', namespace=NS_PGP): - return - - omemo_encrypted_tag = msg.msg_.getTag('encrypted', namespace=NS_OMEMO) - if omemo_encrypted_tag: - account = msg.conn.name - log.debug(account + ' => OMEMO MAM msg received') - - state = self.get_omemo_state(account) - - from_jid = str(msg.msg_.getAttr('from')) - from_jid = gajim.get_jid_without_resource(from_jid) - - msg_dict = unpack_encrypted(omemo_encrypted_tag) - - msg_dict['sender_jid'] = from_jid - - plaintext = state.decrypt_msg(msg_dict) - - if not plaintext: - return - - self.print_msg_to_log(msg.msg_) - - msg.msgtxt = plaintext - - contact_jid = msg.with_ - - if account in self.ui_list and \ - contact_jid in self.ui_list[account]: - self.ui_list[account][contact_jid].activate_omemo() - return False - - elif msg.msg_.getTag('body'): - account = msg.conn.name - - jid = msg.with_ - state = self.get_omemo_state(account) - omemo_enabled = state.encryption.is_active(jid) - - if omemo_enabled: - msg.msgtxt = '**Unencrypted** ' + msg.msgtxt - - @log_calls('OmemoPlugin') - def message_received(self, msg): - """ Handles an incoming message - - Payload is decrypted and the plaintext is written into the - event object. Afterwards the event is passed on further to Gajim. - - Parameters - ---------- - msg : MessageReceivedEvent - - Returns - ------- - Return means that the Event is passed on to Gajim - """ - - if msg.stanza.getTag('openpgp', namespace=NS_PGP): - return - - if msg.stanza.getTag('encrypted', namespace=NS_OMEMO) and \ - msg.mtype == 'chat': - account = msg.conn.name - log.debug(account + ' => OMEMO msg received') - - state = self.get_omemo_state(account) - if msg.forwarded and msg.sent: - from_jid = str(msg.stanza.getTo()) # why gajim? why? - log.debug('message was forwarded doing magic') - else: - from_jid = str(msg.stanza.getFrom()) - self.print_msg_to_log(msg.stanza) - msg_dict = unpack_encrypted(msg.stanza.getTag - ('encrypted', namespace=NS_OMEMO)) - msg_dict['sender_jid'] = gajim.get_jid_without_resource(from_jid) - plaintext = state.decrypt_msg(msg_dict) - - if not plaintext: - return - - msg.msgtxt = plaintext - # Gajim bug: there must be a body or the message - # gets dropped from history - msg.stanza.setBody(plaintext) - - contact_jid = gajim.get_jid_without_resource(from_jid) - if account in self.ui_list and \ - contact_jid in self.ui_list[account]: - self.ui_list[account][contact_jid].activate_omemo() - return False - - elif msg.stanza.getTag('body') and msg.mtype == 'chat': - account = msg.conn.name - - from_jid = str(msg.stanza.getFrom()) - jid = gajim.get_jid_without_resource(from_jid) - state = self.get_omemo_state(account) - omemo_enabled = state.encryption.is_active(jid) - - if omemo_enabled: - msg.msgtxt = '**Unencrypted** ' + msg.msgtxt - # msg.stanza.setBody(msg.msgtxt) - - try: - gui = self.ui_list[account].get(jid, None) - if gui and gui.encryption_active(): - gui.plain_warning() - except KeyError: - log.debug('No Ui present for ' + jid + - ', Ui Warning not shown') - - @log_calls('OmemoPlugin') - def handle_outgoing_event(self, event): - """ Handles a message outgoing event - - In this event we have no stanza. XHTML is set to None - so that it doesnt make its way into the stanza - - Parameters - ---------- - event : MessageOutgoingEvent - - Returns - ------- - Return if encryption is not activated - """ - account = event.account - state = self.get_omemo_state(account) - - if not state.encryption.is_active(event.jid): - return False - - event.xhtml = None - - @log_calls('OmemoPlugin') - def handle_outgoing_stanza(self, event): - """ Manipulates the outgoing stanza - - The body is getting encrypted - - Parameters - ---------- - event : StanzaMessageOutgoingEvent - - Returns - ------- - Return if encryption is not activated or any other - exception or error occurs - """ - try: - if not event.msg_iq.getTag('body'): - return - - account = event.conn.name - state = self.get_omemo_state(account) - full_jid = str(event.msg_iq.getAttr('to')) - to_jid = gajim.get_jid_without_resource(full_jid) - if not state.encryption.is_active(to_jid): - return - - # Delete previous Message out of Correction Message Stanza - if event.msg_iq.getTag('replace', namespace=NS_CORRECT): - event.msg_iq.delChild('encrypted', attrs={'xmlns': NS_OMEMO}) - - plaintext = event.msg_iq.getBody().encode('utf-8') - - msg_dict = state.create_msg( - gajim.get_jid_from_account(account), to_jid, plaintext) - - if not msg_dict: - return True - - encrypted_node = OmemoMessage(msg_dict) - - # Check if non-OMEMO resource is online - contacts = gajim.contacts.get_contacts(account, to_jid) - non_omemo_resource_online = False - for contact in contacts: - if contact.show == 'offline': - continue - if not contact.supports(NS_NOTIFY): - log.debug(contact.get_full_jid() + - ' => Contact doesnt support OMEMO, ' - 'adding Info Message to Body') - support_msg = 'You received a message encrypted with ' \ - 'OMEMO but your client doesnt support OMEMO.' - event.msg_iq.setBody(support_msg) - non_omemo_resource_online = True - if not non_omemo_resource_online: - event.msg_iq.delChild('body') - - event.msg_iq.addChild(node=encrypted_node) - - # XEP-xxxx: Explicit Message Encryption - if not event.msg_iq.getTag('encrypted', attrs={'xmlns': NS_EME}): - eme_node = Node('encrypted', attrs={'xmlns': NS_EME, - 'name': 'OMEMO', - 'namespace': NS_OMEMO}) - event.msg_iq.addChild(node=eme_node) - - # Store Hint for MAM - store = Node('store', attrs={'xmlns': NS_HINTS}) - event.msg_iq.addChild(node=store) - self.print_msg_to_log(event.msg_iq) - except Exception as e: - log.debug(e) - return True - - @log_calls('OmemoPlugin') - def handle_device_list_update(self, event): - """ Check if the passed event is a device list update and store the new - device ids. - - Parameters - ---------- - event : PEPReceivedEvent - - Returns - ------- - bool - True if the given event was a valid device list update event - - - See also - -------- - 4.2 Discovering peer support - http://conversations.im/xeps/multi-end.html#usecases-discovering - """ - if event.pep_type != 'headline': - return False - - devices_list = list(set(unpack_device_list_update(event.stanza, - event.conn.name))) - if len(devices_list) == 0: - return False - account = event.conn.name - contact_jid = gajim.get_jid_without_resource(event.fjid) - state = self.get_omemo_state(account) - my_jid = gajim.get_jid_from_account(account) - - if contact_jid == my_jid: - log.info(account + ' => Received own device list:' + str( - devices_list)) - state.set_own_devices(devices_list) - state.store.sessionStore.setActiveState(devices_list, my_jid) - - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if contact_jid in self.query_for_bundles: - self.query_for_bundles.remove(contact_jid) - - if not state.own_device_id_published(): - # Our own device_id is not in the list, it could be - # overwritten by some other client - self.publish_own_devices_list(account) - else: - log.info(account + ' => Received device list for ' + - contact_jid + ':' + str(devices_list)) - state.set_devices(contact_jid, devices_list) - state.store.sessionStore.setActiveState(devices_list, contact_jid) - - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if contact_jid in self.query_for_bundles: - self.query_for_bundles.remove(contact_jid) - - # Enable Encryption on receiving first Device List - if not state.encryption.exist(contact_jid): - if account in self.ui_list and \ - contact_jid in self.ui_list[account]: - log.debug(account + - ' => Switch encryption ON automatically ...') - self.ui_list[account][contact_jid].activate_omemo() - else: - log.debug(account + - ' => Switch encryption ON automatically ...') - self.omemo_enable_for(contact_jid, account) - - if account in self.ui_list and \ - contact_jid not in self.ui_list[account]: - - chat_control = gajim.interface.msg_win_mgr.get_control( - contact_jid, account) - - if chat_control: - self.connect_ui(chat_control) - - return True - - @log_calls('OmemoPlugin') - def publish_own_devices_list(self, account): - """ Check if the passed event is a device list update and store the new - device ids. - - Parameters - ---------- - account : str - the account name - """ - state = self.get_omemo_state(account) - devices_list = state.own_devices - devices_list.append(state.own_device_id) - devices_list = list(set(devices_list)) - state.set_own_devices(devices_list) - - log.debug(account + ' => Publishing own Devices: ' + str( - devices_list)) - iq = DeviceListAnnouncement(devices_list) - gajim.connections[account].connection.send(iq) - id_ = str(iq.getAttr('id')) - IQ_CALLBACK[id_] = lambda event: log.debug(event) - - @log_calls('OmemoPlugin') - def connect_ui(self, chat_control): - """ Method called from Gajim when a Chat Window is opened - - Parameters - ---------- - chat_control : ChatControl - Gajim ChatControl object - """ - account = chat_control.contact.account.name - contact_jid = chat_control.contact.jid - if account not in self.ui_list: - self.ui_list[account] = {} - state = self.get_omemo_state(account) - my_jid = gajim.get_jid_from_account(account) - omemo_enabled = state.encryption.is_active(contact_jid) - if omemo_enabled: - log.debug(account + " => Adding OMEMO ui for " + contact_jid) - self.ui_list[account][contact_jid] = Ui(self, chat_control, - omemo_enabled, state) - self.ui_list[account][contact_jid].new_fingerprints_available() - return - if contact_jid in state.device_ids or contact_jid == my_jid: - log.debug(account + " => Adding OMEMO ui for " + contact_jid) - self.ui_list[account][contact_jid] = Ui(self, chat_control, - omemo_enabled, state) - self.ui_list[account][contact_jid].new_fingerprints_available() - else: - log.warning(account + " => No devices for " + contact_jid) - - @log_calls('OmemoPlugin') - def disconnect_ui(self, chat_control): - """ Calls the removeUi method to remove all relatad UI objects. - - Parameters - ---------- - chat_control : ChatControl - Gajim ChatControl object - """ - contact_jid = chat_control.contact.jid - account = chat_control.contact.account.name - self.ui_list[account][contact_jid].removeUi() - - def are_keys_missing(self, account, contact_jid): - """ Checks if devicekeys are missing and querys the - bundles - - Parameters - ---------- - account : str - the account name - contact_jid : str - bare jid of the contact - - Returns - ------- - bool - Returns True if there are no trusted Fingerprints - """ - state = self.get_omemo_state(account) - my_jid = gajim.get_jid_from_account(account) - - # Fetch Bundles of own other Devices - if my_jid not in self.query_for_bundles: - - devices_without_session = state \ - .devices_without_sessions(my_jid) - - self.query_for_bundles.append(my_jid) - - if devices_without_session: - for device_id in devices_without_session: - self.fetch_device_bundle_information(account, my_jid, - device_id) - - # Fetch Bundles of contacts devices - if contact_jid not in self.query_for_bundles: - - devices_without_session = state \ - .devices_without_sessions(contact_jid) - - self.query_for_bundles.append(contact_jid) - - if devices_without_session: - for device_id in devices_without_session: - self.fetch_device_bundle_information(account, contact_jid, - device_id) - - if state.getTrustedFingerprints(contact_jid): - return False - else: - return True - - @staticmethod - def handle_iq_received(event): - """ Method called when an IQ is received - - Parameters - ---------- - event : RawIqReceived - """ - id_ = str(event.stanza.getAttr("id")) - if id_ in IQ_CALLBACK: - try: - IQ_CALLBACK[id_](event.stanza) - except: - raise - finally: - del IQ_CALLBACK[id_] - - @log_calls('OmemoPlugin') - def fetch_device_bundle_information(self, account, jid, device_id): - """ Fetch bundle information for specified jid, key, and create axolotl - session on success. - - Parameters - ---------- - account : str - The account name - jid : str - The jid to query for bundle information - device_id : int - The device_id for which we are missing an axolotl session - """ - log.info(account + ' => Fetch bundle device ' + str(device_id) + - '#' + jid) - iq = BundleInformationQuery(jid, device_id) - iq_id = str(iq.getAttr('id')) - IQ_CALLBACK[iq_id] = \ - lambda stanza: self.session_from_prekey_bundle(account, - stanza, jid, - device_id) - gajim.connections[account].connection.send(iq) - - @log_calls('OmemoPlugin') - def session_from_prekey_bundle(self, account, stanza, - recipient_id, device_id): - """ Starts a session from a PreKey bundle. - - This method tries to build an axolotl session when a PreKey bundle - is fetched. - - If a session can not be build it will fail silently but log the a - warning. - - See also - -------- - - 4.4 Building a session: - http://conversations.im/xeps/multi-end.html#usecases-building - - Parameters: - ----------- - account : str - The account name - stanza - The stanza object received from callback - recipient_id : str - The recipient jid - device_id : int - The device_id for which the bundle was queried - - """ - state = self.get_omemo_state(account) - bundle_dict = unpack_device_bundle(stanza, device_id) - if not bundle_dict: - log.warning('Failed to build Session with ' + recipient_id) - return - - if state.build_session(recipient_id, device_id, bundle_dict): - log.info(account + ' => session created for: ' + recipient_id) - # Trigger dialog to trust new Fingerprints if - # the Chat Window is Open - if account in self.ui_list and \ - recipient_id in self.ui_list[account]: - self.ui_list[account][recipient_id]. \ - new_fingerprints_available() - - @log_calls('OmemoPlugin') - def query_own_devicelist(self, account): - """ Query own devicelist from the server. - - Parameters - ---------- - account : str - the account name - """ - my_jid = gajim.get_jid_from_account(account) - iq = DevicelistQuery(my_jid) - gajim.connections[account].connection.send(iq) - log.info(account + ' => Querry own devicelist ...') - id_ = str(iq.getAttr("id")) - IQ_CALLBACK[id_] = lambda stanza: \ - self.handle_devicelist_result(account, stanza) - - @log_calls('OmemoPlugin') - def publish_bundle(self, account): - """ Publish our bundle information to the PEP node. - - Parameters - ---------- - account : str - the account name - - See also - -------- - 4.3 Announcing bundle information: - http://conversations.im/xeps/multi-end.html#usecases-announcing - """ - state = self.get_omemo_state(account) - iq = BundleInformationAnnouncement(state.bundle, state.own_device_id) - gajim.connections[account].connection.send(iq) - id_ = str(iq.getAttr("id")) - log.info(account + " => Publishing bundle ...") - IQ_CALLBACK[id_] = lambda stanza: \ - self.handle_publish_result(account, stanza) - - @staticmethod - def handle_publish_result(account, stanza): - """ Log if publishing our bundle was successful - - Parameters - ---------- - account : str - the account name - stanza - The stanza object received from callback - """ - if successful(stanza): - log.info(account + ' => Publishing bundle was successful') - else: - log.error(account + ' => Publishing bundle was NOT successful') - - @log_calls('OmemoPlugin') - def handle_devicelist_result(self, account, stanza): - """ If query was successful add own device to the list. - - Parameters - ---------- - account : str - the account name - stanza - The stanza object received from callback - """ - - my_jid = gajim.get_jid_from_account(account) - state = self.get_omemo_state(account) - - if successful(stanza): - log.info(account + ' => Devicelistquery was successful') - devices_list = list(set(unpack_device_list_update(stanza, account))) - if len(devices_list) == 0: - return False - contact_jid = stanza.getAttr('from') - if contact_jid == my_jid: - state.set_own_devices(devices_list) - state.store.sessionStore.setActiveState(devices_list, my_jid) - - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if contact_jid in self.query_for_bundles: - self.query_for_bundles.remove(contact_jid) - - if not state.own_device_id_published(): - # Our own device_id is not in the list, it could be - # overwritten by some other client - self.publish_own_devices_list(account) - else: - log.error(account + ' => Devicelistquery was NOT successful') - self.publish_own_devices_list(account) - - @log_calls('OmemoPlugin') - def clear_device_list(self, account): - """ Clears the local devicelist of our own devices and publishes - a new one including only the current ID of this device - - Parameters - ---------- - account : str - the account name - """ - connection = gajim.connections[account].connection - if not connection: - return - state = self.get_omemo_state(account) - devices_list = [state.own_device_id] - state.set_own_devices(devices_list) - - log.info(account + ' => Clearing devices_list ' + str(devices_list)) - iq = DeviceListAnnouncement(devices_list) - connection.send(iq) - id_ = str(iq.getAttr('id')) - IQ_CALLBACK[id_] = lambda event: log.info(event) - - @staticmethod - def print_msg_to_log(stanza): - """ Prints a stanza in a fancy way to the log """ - log.debug('-'*15) - stanzastr = '\n' + stanza.__str__(fancy=True) - stanzastr = stanzastr[0:-1] - log.debug(stanzastr) - log.debug('-'*15) - - @log_calls('OmemoPlugin') - def omemo_enable_for(self, jid, account): - """ Used by the UI to enable OMEMO for a specified contact. - - To activate OMEMO check first if a Ui Object exists for the - Contact. If it exists use Ui.activate_omemo(). Only if there - is no Ui Object for the contact this method is to be used. - - Parameters - ---------- - jid : str - bare jid - account : str - the account name - """ - state = self.get_omemo_state(account) - state.encryption.activate(jid) - - @log_calls('OmemoPlugin') - def omemo_disable_for(self, jid, account): - """ Used by the UI to disable OMEMO for a specified contact. - - WARNING - OMEMO should only be disabled through - User interaction with the UI. - - Parameters - ---------- - jid : str - bare jid - account : str - the account name - """ - state = self.get_omemo_state(account) - state.encryption.deactivate(jid) +from .omemoplugin import OmemoPlugin diff --git a/omemo/omemoplugin.py b/omemo/omemoplugin.py new file mode 100644 index 0000000..49fc012 --- /dev/null +++ b/omemo/omemoplugin.py @@ -0,0 +1,1135 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2015 Bahtiar `kalkin-` Gadimov +# Copyright 2015 Daniel Gultsch +# +# This file is part of Gajim-OMEMO plugin. +# +# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# the Gajim-OMEMO plugin. If not, see . +# + +import logging +import os +import sqlite3 +import shutil +import message_control + +from common import caps_cache, gajim, ged, configpaths +from common.pep import SUPPORTED_PERSONAL_USER_EVENTS +from plugins import GajimPlugin +from plugins.helpers import log_calls +from nbxmpp.simplexml import Node +from nbxmpp import NS_CORRECT, NS_ADDRESS + +from .xmpp import ( + NS_NOTIFY, NS_OMEMO, NS_EME, BundleInformationAnnouncement, + BundleInformationQuery, DeviceListAnnouncement, DevicelistQuery, + DevicelistPEP, OmemoMessage, successful, unpack_device_bundle, + unpack_device_list_update, unpack_encrypted) + + +IQ_CALLBACK = {} + +AXOLOTL_MISSING = 'You are missing Python-Axolotl or use an outdated version' +PROTOBUF_MISSING = 'OMEMO cant import Google Protobuf, you can find help in ' \ + 'the GitHub Wiki' +GAJIM_VERSION = 'OMEMO only works with the latest Gajim version, get the ' \ + 'latest version from gajim.org' +ERROR_MSG = '' + +NS_HINTS = 'urn:xmpp:hints' +NS_PGP = 'urn:xmpp:openpgp:0' +DB_DIR_OLD = gajim.gajimpaths.data_root +DB_DIR_NEW = configpaths.gajimpaths['MY_DATA'] + +log = logging.getLogger('gajim.plugin_system.omemo') + +try: + import google.protobuf +except Exception as e: + log.error(e) + ERROR_MSG = PROTOBUF_MISSING + +try: + import axolotl +except Exception as e: + log.error(e) + ERROR_MSG = AXOLOTL_MISSING + +if not ERROR_MSG: + try: + from .omemo.state import OmemoState + from .ui import Ui, OMEMOConfigDialog + except Exception as e: + log.error(e) + ERROR_MSG = 'Error: ' + str(e) + +# pylint: disable=no-init +# pylint: disable=attribute-defined-outside-init + + +class OmemoPlugin(GajimPlugin): + + omemo_states = {} + ui_list = {} + groupchat = {} + temp_groupchat = {} + + @log_calls('OmemoPlugin') + def init(self): + """ Init """ + if ERROR_MSG: + self.activatable = False + self.available_text = ERROR_MSG + self.config_dialog = None + return + self.events_handlers = { + 'mam-message-received': (ged.PRECORE, self.mam_message_received), + 'message-received': (ged.PRECORE, self.message_received), + 'pep-received': (ged.PRECORE, self.handle_device_list_update), + 'raw-iq-received': (ged.PRECORE, self.handle_iq_received), + 'signed-in': (ged.PRECORE, self.signed_in), + 'stanza-message-outgoing': + (ged.PRECORE, self.handle_outgoing_stanza), + 'message-outgoing': + (ged.PRECORE, self.handle_outgoing_event), + 'gc-stanza-message-outgoing': + (ged.PRECORE, self.handle_outgoing_gc_stanza), + 'gc-presence-received': (ged.PRECORE, self.gc_presence_received), + 'gc-config-changed-received': + (ged.PRECORE, self.gc_config_changed_received), + 'muc-admin-received': (ged.PRECORE, self.room_memberlist_received), + } + + self.config_dialog = OMEMOConfigDialog(self) + self.gui_extension_points = {'chat_control': (self.connect_ui, + self.disconnect_ui), + 'groupchat_control': (self.connect_ui, + self.disconnect_ui)} + SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP) + self.plugin = self + self.announced = [] + self.query_for_bundles = [] + self.disabled_accounts = [] + self.gc_message = {} + + self.config_default_values = {'DISABLED_ACCOUNTS': ([], ''), } + + for account in self.plugin.config['DISABLED_ACCOUNTS']: + self.disabled_accounts.append(account) + + def migrate_dbpath(self, account, my_jid): + old_dbpath = os.path.join(DB_DIR_OLD, 'omemo_' + account + '.db') + new_dbpath = os.path.join(DB_DIR_NEW, 'omemo_' + my_jid + '.db') + + if os.path.exists(old_dbpath): + log.debug('Migrating DBName and Path ..') + try: + shutil.move(old_dbpath, new_dbpath) + return new_dbpath + except Exception: + log.exception('Migration Error:') + return old_dbpath + + return new_dbpath + + @log_calls('OmemoPlugin') + def get_omemo_state(self, account): + """ Returns the the OmemoState for the specified account. + Creates the OmemoState if it does not exist yet. + + Parameters + ---------- + account : str + the account name + + Returns + ------- + OmemoState + """ + if account in self.disabled_accounts: + return + if account not in self.omemo_states: + self.deactivate_gajim_e2e(account) + my_jid = gajim.get_jid_from_account(account) + db_path = self.migrate_dbpath(account, my_jid) + + conn = sqlite3.connect(db_path, check_same_thread=False) + self.omemo_states[account] = OmemoState(my_jid, conn, account, + self.plugin) + + return self.omemo_states[account] + + @staticmethod + def deactivate_gajim_e2e(account): + """ Deativates E2E encryption in Gajim """ + gajim.config.set_per('accounts', account, + 'autonegotiate_esessions', False) + gajim.config.set_per('accounts', account, + 'enable_esessions', False) + log.info(str(account) + " => Gajim E2E encryption disabled") + + @log_calls('OmemoPlugin') + def signed_in(self, event): + """ Method called on SignIn + + Parameters + ---------- + event : SignedInEvent + """ + account = event.conn.name + if account in self.disabled_accounts: + return + log.debug(account + + ' => Announce Support after Sign In') + self.query_for_bundles = [] + self.announced = [] + self.announced.append(account) + self.publish_bundle(account) + self.query_own_devicelist(account) + + @log_calls('OmemoPlugin') + def activate(self): + """ Method called when the Plugin is activated in the PluginManager + """ + self.query_for_bundles = [] + # Publish bundle information and Entity Caps + for account in gajim.connections: + if account in self.disabled_accounts: + log.debug(account + + ' => Account is disabled') + continue + if NS_NOTIFY not in gajim.gajim_optional_features[account]: + gajim.gajim_optional_features[account].append(NS_NOTIFY) + self._compute_caps_hash(account) + if account not in self.announced: + if gajim.account_is_connected(account): + log.debug(account + + ' => Announce Support after Plugin Activation') + self.announced.append(account) + self.publish_bundle(account) + self.query_own_devicelist(account) + + @log_calls('OmemoPlugin') + def deactivate(self): + """ Method called when the Plugin is deactivated in the PluginManager + + Removes OMEMO from the Entity Capabilities list + """ + for account in gajim.connections: + if account in self.disabled_accounts: + continue + if NS_NOTIFY in gajim.gajim_optional_features[account]: + gajim.gajim_optional_features[account].remove(NS_NOTIFY) + self._compute_caps_hash(account) + + @staticmethod + def _compute_caps_hash(account): + """ Computes the hash for Entity Capabilities and publishes it """ + gajim.caps_hash[account] = caps_cache.compute_caps_hash( + [gajim.gajim_identity], + gajim.gajim_common_features + + gajim.gajim_optional_features[account]) + # re-send presence with new hash + connected = gajim.connections[account].connected + if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible': + gajim.connections[account].change_status( + gajim.SHOW_LIST[connected], gajim.connections[account].status) + + @log_calls('OmemoPlugin') + def mam_message_received(self, msg): + """ Handles an incoming MAM message + + Payload is decrypted and the plaintext is written into the + event object. Afterwards the event is passed on further to Gajim. + + Parameters + ---------- + msg : MamMessageReceivedEvent + + Returns + ------- + Return means that the Event is passed on to Gajim + """ + account = msg.conn.name + if account in self.disabled_accounts: + return + + if msg.msg_.getTag('openpgp', namespace=NS_PGP): + return + + omemo_encrypted_tag = msg.msg_.getTag('encrypted', namespace=NS_OMEMO) + if omemo_encrypted_tag: + log.debug(account + ' => OMEMO MAM msg received') + + state = self.get_omemo_state(account) + + from_jid = str(msg.msg_.getAttr('from')) + from_jid = gajim.get_jid_without_resource(from_jid) + + msg_dict = unpack_encrypted(omemo_encrypted_tag) + + msg_dict['sender_jid'] = from_jid + + plaintext = state.decrypt_msg(msg_dict) + + if not plaintext: + return + + self.print_msg_to_log(msg.msg_) + + msg.msgtxt = plaintext + + contact_jid = msg.with_ + + if account in self.ui_list and \ + contact_jid in self.ui_list[account]: + self.ui_list[account][contact_jid].activate_omemo() + return False + + elif msg.msg_.getTag('body'): + account = msg.conn.name + + jid = msg.with_ + state = self.get_omemo_state(account) + omemo_enabled = state.encryption.is_active(jid) + + if omemo_enabled: + msg.msgtxt = '**Unencrypted** ' + msg.msgtxt + + @log_calls('OmemoPlugin') + def message_received(self, msg): + """ Handles an incoming message + + Payload is decrypted and the plaintext is written into the + event object. Afterwards the event is passed on further to Gajim. + + Parameters + ---------- + msg : MessageReceivedEvent + + Returns + ------- + Return means that the Event is passed on to Gajim + """ + account = msg.conn.name + if account in self.disabled_accounts: + return + + if msg.stanza.getTag('openpgp', namespace=NS_PGP): + return + + if msg.stanza.getTag('encrypted', namespace=NS_OMEMO): + log.debug(account + ' => OMEMO msg received') + + state = self.get_omemo_state(account) + if msg.forwarded and msg.sent: + from_jid = str(msg.stanza.getTo()) # why gajim? why? + log.debug('message was forwarded doing magic') + else: + from_jid = str(msg.stanza.getFrom()) + + self.print_msg_to_log(msg.stanza) + msg_dict = unpack_encrypted(msg.stanza.getTag + ('encrypted', namespace=NS_OMEMO)) + + if msg.mtype == 'groupchat': + address_tag = msg.stanza.getTag('addresses', + namespace=NS_ADDRESS) + if address_tag: # History Message from MUC + from_jid = address_tag.getTag( + 'address', attrs={'type': 'ofrom'}).getAttr('jid') + else: + try: + from_jid = self.groupchat[msg.jid][msg.resource] + except KeyError: + log.debug('Groupchat: Last resort trying to ' + 'find SID in DB') + from_jid = state.store. \ + getJidFromDevice(msg_dict['sid']) + if not from_jid: + log.error(account + + ' => Cant decrypt GroupChat Message ' + 'from ' + msg.resource) + return True + self.groupchat[msg.jid][msg.resource] = from_jid + + log.debug('GroupChat Message from: %s', from_jid) + + plaintext = '' + if msg_dict['sid'] == state.own_device_id: + if msg_dict['payload'] in self.gc_message: + plaintext = self.gc_message[msg_dict['payload']] + del self.gc_message[msg_dict['payload']] + else: + log.error(account + ' => Cant decrypt own GroupChat ' + 'Message') + else: + msg_dict['sender_jid'] = gajim. \ + get_jid_without_resource(from_jid) + plaintext = state.decrypt_msg(msg_dict) + + if not plaintext: + return True + + msg.msgtxt = plaintext + # Gajim bug: there must be a body or the message + # gets dropped from history + msg.stanza.setBody(plaintext) + + if msg.mtype != 'groupchat': + contact_jid = gajim.get_jid_without_resource(from_jid) + if account in self.ui_list and \ + contact_jid in self.ui_list[account]: + self.ui_list[account][contact_jid].activate_omemo() + return False + + elif msg.stanza.getTag('body'): + account = msg.conn.name + + from_jid = str(msg.stanza.getFrom()) + jid = gajim.get_jid_without_resource(from_jid) + state = self.get_omemo_state(account) + omemo_enabled = state.encryption.is_active(jid) + + if omemo_enabled: + msg.msgtxt = '**Unencrypted** ' + msg.msgtxt + # msg.stanza.setBody(msg.msgtxt) + + try: + gui = self.ui_list[account].get(jid, None) + if gui and gui.encryption_active(): + gui.plain_warning() + except KeyError: + log.debug('No Ui present for ' + jid + + ', Ui Warning not shown') + + def room_memberlist_received(self, event): + account = event.conn.name + if account in self.disabled_accounts: + return + log.debug('Room %s Memberlist received: %s', + event.fjid, event.users_dict) + room = event.fjid + + def jid_known(jid): + for nick in self.groupchat[room]: + if self.groupchat[room][nick] == jid: + return True + return False + + for jid in event.users_dict: + if not jid_known(jid): + # Add JID with JID because we have no Nick yet + self.groupchat[room][jid] = jid + log.debug('JID Added: ' + jid) + + @log_calls('OmemoPlugin') + def gc_presence_received(self, event): + account = event.conn.name + if account in self.disabled_accounts: + return + if not hasattr(event, 'real_jid') or not event.real_jid: + return + + room = event.room_jid + jid = gajim.get_jid_without_resource(event.real_jid) + nick = event.nick + + if '303' in event.status_code: # Nick Changed + if room in self.groupchat: + if nick in self.groupchat[room]: + del self.groupchat[room][nick] + self.groupchat[room][event.new_nick] = jid + log.debug('Nick Change: old: %s, new: %s, jid: %s ', + nick, event.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][event.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: + # 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: ' + jid) + + if '100' in event.status_code: # non-anonymous Room (Full JID) + + if room not in self.groupchat: + self.groupchat[room] = self.temp_groupchat[room] + + log.debug('OMEMO capable Room found: %s', room) + + gajim.connections[account].get_affiliation_list(room, 'owner') + gajim.connections[account].get_affiliation_list(room, 'admin') + gajim.connections[account].get_affiliation_list(room, 'member') + + self.ui_list[account][room].sensitive(True) + + @log_calls('OmemoPlugin') + def gc_config_changed_received(self, event): + account = event.conn.name + if account in self.disabled_accounts: + return + log.debug('CONFIG CHANGE') + log.debug(event.room_jid) + log.debug(event.status_code) + + def handle_outgoing_gc_stanza(self, event): + """ Manipulates the outgoing groupchat stanza + + The body is getting encrypted + + Parameters + ---------- + event : StanzaMessageOutgoingEvent + + Returns + ------- + Return if encryption is not activated or any other + exception or error occurs + """ + account = event.conn.name + if account in self.disabled_accounts: + return + try: + # If we send a correction msg, the stanza is saved + # in correction_msg + if event.correction_msg: + event.msg_iq = event.correction_msg + if not event.msg_iq.getTag('body'): + return + state = self.get_omemo_state(account) + full_jid = str(event.msg_iq.getAttr('to')) + to_jid = gajim.get_jid_without_resource(full_jid) + if to_jid not in self.groupchat: + return + if not state.encryption.is_active(to_jid): + return + # Delete previous Message out of Correction Message Stanza + if event.msg_iq.getTag('replace', namespace=NS_CORRECT): + event.msg_iq.delChild('encrypted', attrs={'xmlns': NS_OMEMO}) + + plaintext = event.msg_iq.getBody() + msg_dict = state.create_gc_msg( + gajim.get_jid_from_account(account), + to_jid, + plaintext.encode('utf8')) + if not msg_dict: + return True + + self.gc_message[msg_dict['payload']] = plaintext + encrypted_node = OmemoMessage(msg_dict) + event.msg_iq.delChild('body') + event.msg_iq.addChild(node=encrypted_node) + + # XEP-xxxx: Explicit Message Encryption + if not event.msg_iq.getTag('encrypted', attrs={'xmlns': NS_EME}): + eme_node = Node('encrypted', attrs={'xmlns': NS_EME, + 'name': 'OMEMO', + 'namespace': NS_OMEMO}) + event.msg_iq.addChild(node=eme_node) + + # Add Message for devices that dont support OMEMO + support_msg = 'You received a message encrypted with ' \ + 'OMEMO but your client doesnt support OMEMO.' + event.msg_iq.setBody(support_msg) + + # Store Hint for MAM + store = Node('store', attrs={'xmlns': NS_HINTS}) + event.msg_iq.addChild(node=store) + if event.correction_msg: + event.correction_msg = event.msg_iq + event.msg_iq = None + self.print_msg_to_log(event.correction_msg) + else: + self.print_msg_to_log(event.msg_iq) + except Exception as e: + log.debug(e) + return True + + @log_calls('OmemoPlugin') + def handle_outgoing_event(self, event): + """ Handles a message outgoing event + + In this event we have no stanza. XHTML is set to None + so that it doesnt make its way into the stanza + + Parameters + ---------- + event : MessageOutgoingEvent + + Returns + ------- + Return if encryption is not activated + """ + account = event.account + if account in self.disabled_accounts: + return + state = self.get_omemo_state(account) + + if not state.encryption.is_active(event.jid): + return False + + event.xhtml = None + + @log_calls('OmemoPlugin') + def handle_outgoing_stanza(self, event): + """ Manipulates the outgoing stanza + + The body is getting encrypted + + Parameters + ---------- + event : StanzaMessageOutgoingEvent + + Returns + ------- + Return if encryption is not activated or any other + exception or error occurs + """ + account = event.conn.name + if account in self.disabled_accounts: + return + try: + if not event.msg_iq.getTag('body'): + return + + state = self.get_omemo_state(account) + full_jid = str(event.msg_iq.getAttr('to')) + to_jid = gajim.get_jid_without_resource(full_jid) + if not state.encryption.is_active(to_jid): + return + + # Delete previous Message out of Correction Message Stanza + if event.msg_iq.getTag('replace', namespace=NS_CORRECT): + event.msg_iq.delChild('encrypted', attrs={'xmlns': NS_OMEMO}) + + plaintext = event.msg_iq.getBody().encode('utf8') + + msg_dict = state.create_msg( + gajim.get_jid_from_account(account), to_jid, plaintext) + if not msg_dict: + return True + + encrypted_node = OmemoMessage(msg_dict) + + # Check if non-OMEMO resource is online + contacts = gajim.contacts.get_contacts(account, to_jid) + non_omemo_resource_online = False + for contact in contacts: + if contact.show == 'offline': + continue + if not contact.supports(NS_NOTIFY): + log.debug(contact.get_full_jid() + + ' => Contact doesnt support OMEMO, ' + 'adding Info Message to Body') + support_msg = 'You received a message encrypted with ' \ + 'OMEMO but your client doesnt support OMEMO.' + event.msg_iq.setBody(support_msg) + non_omemo_resource_online = True + if not non_omemo_resource_online: + event.msg_iq.delChild('body') + + event.msg_iq.addChild(node=encrypted_node) + + # XEP-xxxx: Explicit Message Encryption + if not event.msg_iq.getTag('encrypted', attrs={'xmlns': NS_EME}): + eme_node = Node('encrypted', attrs={'xmlns': NS_EME, + 'name': 'OMEMO', + 'namespace': NS_OMEMO}) + event.msg_iq.addChild(node=eme_node) + + # Store Hint for MAM + store = Node('store', attrs={'xmlns': NS_HINTS}) + event.msg_iq.addChild(node=store) + self.print_msg_to_log(event.msg_iq) + except Exception as e: + log.debug(e) + return True + + @log_calls('OmemoPlugin') + def handle_device_list_update(self, event): + """ Check if the passed event is a device list update and store the new + device ids. + + Parameters + ---------- + event : PEPReceivedEvent + + Returns + ------- + bool + True if the given event was a valid device list update event + + + See also + -------- + 4.2 Discovering peer support + http://conversations.im/xeps/multi-end.html#usecases-discovering + """ + + account = event.conn.name + if account in self.disabled_accounts: + return False + + if event.pep_type != 'headline': + return False + + devices_list = list(set(unpack_device_list_update(event.stanza, + event.conn.name))) + contact_jid = gajim.get_jid_without_resource(event.fjid) + if len(devices_list) == 0: + log.error(account + + ' => Received empty or invalid Devicelist from: ' + + contact_jid) + return False + + state = self.get_omemo_state(account) + my_jid = gajim.get_jid_from_account(account) + + if contact_jid == my_jid: + log.info(account + ' => Received own device list:' + str( + devices_list)) + state.set_own_devices(devices_list) + state.store.sessionStore.setActiveState(devices_list, my_jid) + + # remove contact from list, so on send button pressed + # we query for bundle and build a session + if contact_jid in self.query_for_bundles: + self.query_for_bundles.remove(contact_jid) + + if not state.own_device_id_published(): + # Our own device_id is not in the list, it could be + # overwritten by some other client + self.publish_own_devices_list(account) + else: + log.info(account + ' => Received device list for ' + + contact_jid + ':' + str(devices_list)) + state.set_devices(contact_jid, devices_list) + state.store.sessionStore.setActiveState(devices_list, contact_jid) + + # remove contact from list, so on send button pressed + # we query for bundle and build a session + if contact_jid in self.query_for_bundles: + self.query_for_bundles.remove(contact_jid) + + # Enable Encryption on receiving first Device List + if not state.encryption.exist(contact_jid): + if account in self.ui_list and \ + contact_jid in self.ui_list[account]: + log.debug(account + + ' => Switch encryption ON automatically ...') + self.ui_list[account][contact_jid].activate_omemo() + else: + log.debug(account + + ' => Switch encryption ON automatically ...') + self.omemo_enable_for(contact_jid, account) + + if account in self.ui_list and \ + contact_jid not in self.ui_list[account]: + + chat_control = gajim.interface.msg_win_mgr.get_control( + contact_jid, account) + + if chat_control: + self.connect_ui(chat_control) + + return True + + @log_calls('OmemoPlugin') + def publish_own_devices_list(self, account): + """ Check if the passed event is a device list update and store the new + device ids. + + Parameters + ---------- + account : str + the account name + """ + state = self.get_omemo_state(account) + devices_list = state.own_devices + devices_list.append(state.own_device_id) + devices_list = list(set(devices_list)) + state.set_own_devices(devices_list) + + log.debug(account + ' => Publishing own Devices: ' + str( + devices_list)) + iq = DeviceListAnnouncement(devices_list) + gajim.connections[account].connection.send(iq) + id_ = str(iq.getAttr('id')) + IQ_CALLBACK[id_] = lambda event: log.debug(event) + + @log_calls('OmemoPlugin') + def connect_ui(self, chat_control): + """ Method called from Gajim when a Chat Window is opened + + Parameters + ---------- + chat_control : ChatControl + Gajim ChatControl object + """ + account = chat_control.contact.account.name + if account in self.disabled_accounts: + return + contact_jid = chat_control.contact.jid + if account not in self.ui_list: + self.ui_list[account] = {} + state = self.get_omemo_state(account) + my_jid = gajim.get_jid_from_account(account) + omemo_enabled = state.encryption.is_active(contact_jid) + if omemo_enabled: + log.debug(account + " => Adding OMEMO ui for " + contact_jid) + self.ui_list[account][contact_jid] = Ui(self, chat_control, + omemo_enabled, state) + self.ui_list[account][contact_jid].new_fingerprints_available() + return + if contact_jid in state.device_ids or contact_jid == my_jid: + log.debug(account + " => Adding OMEMO ui for " + contact_jid) + self.ui_list[account][contact_jid] = Ui(self, chat_control, + omemo_enabled, state) + self.ui_list[account][contact_jid].new_fingerprints_available() + else: + log.warning(account + " => No devices for " + contact_jid) + + if chat_control.type_id == message_control.TYPE_GC: + self.ui_list[account][contact_jid] = Ui(self, chat_control, + omemo_enabled, state) + self.ui_list[account][contact_jid].sensitive(False) + + @log_calls('OmemoPlugin') + def disconnect_ui(self, chat_control): + """ Calls the removeUi method to remove all relatad UI objects. + + Parameters + ---------- + chat_control : ChatControl + Gajim ChatControl object + """ + contact_jid = chat_control.contact.jid + account = chat_control.contact.account.name + if account in self.disabled_accounts: + return + self.ui_list[account][contact_jid].removeUi() + + def are_keys_missing(self, account, contact_jid): + """ Checks if devicekeys are missing and querys the + bundles + + Parameters + ---------- + account : str + the account name + contact_jid : str + bare jid of the contact + + Returns + ------- + bool + Returns True if there are no trusted Fingerprints + """ + state = self.get_omemo_state(account) + my_jid = gajim.get_jid_from_account(account) + + # Fetch Bundles of own other Devices + if my_jid not in self.query_for_bundles: + + devices_without_session = state \ + .devices_without_sessions(my_jid) + + self.query_for_bundles.append(my_jid) + + if devices_without_session: + for device_id in devices_without_session: + self.fetch_device_bundle_information(account, my_jid, + device_id) + + # Fetch Bundles of contacts devices + if contact_jid not in self.query_for_bundles: + + devices_without_session = state \ + .devices_without_sessions(contact_jid) + + self.query_for_bundles.append(contact_jid) + + if devices_without_session: + for device_id in devices_without_session: + self.fetch_device_bundle_information(account, contact_jid, + device_id) + + if state.getTrustedFingerprints(contact_jid): + return False + else: + return True + + @staticmethod + def handle_iq_received(event): + """ Method called when an IQ is received + + Parameters + ---------- + event : RawIqReceived + """ + id_ = str(event.stanza.getAttr("id")) + if id_ in IQ_CALLBACK: + try: + IQ_CALLBACK[id_](event.stanza) + except: + raise + finally: + del IQ_CALLBACK[id_] + + @log_calls('OmemoPlugin') + def fetch_device_bundle_information(self, account, jid, device_id): + """ Fetch bundle information for specified jid, key, and create axolotl + session on success. + + Parameters + ---------- + account : str + The account name + jid : str + The jid to query for bundle information + device_id : int + The device_id for which we are missing an axolotl session + """ + log.info(account + ' => Fetch bundle device ' + str(device_id) + + '#' + jid) + iq = BundleInformationQuery(jid, device_id) + iq_id = str(iq.getAttr('id')) + IQ_CALLBACK[iq_id] = \ + lambda stanza: self.session_from_prekey_bundle(account, + stanza, jid, + device_id) + gajim.connections[account].connection.send(iq) + + @log_calls('OmemoPlugin') + def session_from_prekey_bundle(self, account, stanza, + recipient_id, device_id): + """ Starts a session from a PreKey bundle. + + This method tries to build an axolotl session when a PreKey bundle + is fetched. + + If a session can not be build it will fail silently but log the a + warning. + + See also + -------- + + 4.4 Building a session: + http://conversations.im/xeps/multi-end.html#usecases-building + + Parameters: + ----------- + account : str + The account name + stanza + The stanza object received from callback + recipient_id : str + The recipient jid + device_id : int + The device_id for which the bundle was queried + + """ + state = self.get_omemo_state(account) + bundle_dict = unpack_device_bundle(stanza, device_id) + if not bundle_dict: + log.warning('Failed to build Session with ' + recipient_id) + return + + if state.build_session(recipient_id, device_id, bundle_dict): + log.info(account + ' => session created for: ' + recipient_id) + # Trigger dialog to trust new Fingerprints if + # the Chat Window is Open + if account in self.ui_list and \ + recipient_id in self.ui_list[account]: + self.ui_list[account][recipient_id]. \ + new_fingerprints_available() + + @log_calls('OmemoPlugin') + def query_own_devicelist(self, account): + """ Query own devicelist from the server. + + Parameters + ---------- + account : str + the account name + """ + my_jid = gajim.get_jid_from_account(account) + iq = DevicelistQuery(my_jid) + gajim.connections[account].connection.send(iq) + log.info(account + ' => Querry own devicelist ...') + id_ = str(iq.getAttr("id")) + IQ_CALLBACK[id_] = lambda stanza: \ + self.handle_devicelist_result(account, stanza) + + @log_calls('OmemoPlugin') + def publish_bundle(self, account): + """ Publish our bundle information to the PEP node. + + Parameters + ---------- + account : str + the account name + + See also + -------- + 4.3 Announcing bundle information: + http://conversations.im/xeps/multi-end.html#usecases-announcing + """ + state = self.get_omemo_state(account) + iq = BundleInformationAnnouncement(state.bundle, state.own_device_id) + gajim.connections[account].connection.send(iq) + id_ = str(iq.getAttr("id")) + log.info(account + " => Publishing bundle ...") + IQ_CALLBACK[id_] = lambda stanza: \ + self.handle_publish_result(account, stanza) + + @staticmethod + def handle_publish_result(account, stanza): + """ Log if publishing our bundle was successful + + Parameters + ---------- + account : str + the account name + stanza + The stanza object received from callback + """ + if successful(stanza): + log.info(account + ' => Publishing bundle was successful') + else: + log.error(account + ' => Publishing bundle was NOT successful') + + @log_calls('OmemoPlugin') + def handle_devicelist_result(self, account, stanza): + """ If query was successful add own device to the list. + + Parameters + ---------- + account : str + the account name + stanza + The stanza object received from callback + """ + + my_jid = gajim.get_jid_from_account(account) + state = self.get_omemo_state(account) + + if successful(stanza): + devices_list = list(set(unpack_device_list_update(stanza, account))) + if len(devices_list) == 0: + log.error(account + ' => Devicelistquery was NOT successful') + self.publish_own_devices_list(account) + return False + contact_jid = stanza.getAttr('from') + if contact_jid == my_jid: + state.set_own_devices(devices_list) + state.store.sessionStore.setActiveState(devices_list, my_jid) + log.info(account + ' => Devicelistquery was successful') + # remove contact from list, so on send button pressed + # we query for bundle and build a session + if contact_jid in self.query_for_bundles: + self.query_for_bundles.remove(contact_jid) + + if not state.own_device_id_published(): + # Our own device_id is not in the list, it could be + # overwritten by some other client + self.publish_own_devices_list(account) + else: + log.error(account + ' => Devicelistquery was NOT successful') + self.publish_own_devices_list(account) + + @log_calls('OmemoPlugin') + def clear_device_list(self, account): + """ Clears the local devicelist of our own devices and publishes + a new one including only the current ID of this device + + Parameters + ---------- + account : str + the account name + """ + connection = gajim.connections[account].connection + if not connection: + return + state = self.get_omemo_state(account) + devices_list = [state.own_device_id] + state.set_own_devices(devices_list) + + log.info(account + ' => Clearing devices_list ' + str(devices_list)) + iq = DeviceListAnnouncement(devices_list) + connection.send(iq) + id_ = str(iq.getAttr('id')) + IQ_CALLBACK[id_] = lambda event: log.info(event) + + @staticmethod + def print_msg_to_log(stanza): + """ Prints a stanza in a fancy way to the log """ + log.debug('-'*15) + stanzastr = '\n' + stanza.__str__(fancy=True) + stanzastr = stanzastr[0:-1] + log.debug(stanzastr) + log.debug('-'*15) + + @log_calls('OmemoPlugin') + def omemo_enable_for(self, jid, account): + """ Used by the UI to enable OMEMO for a specified contact. + + To activate OMEMO check first if a Ui Object exists for the + Contact. If it exists use Ui.activate_omemo(). Only if there + is no Ui Object for the contact this method is to be used. + + Parameters + ---------- + jid : str + bare jid + account : str + the account name + """ + state = self.get_omemo_state(account) + state.encryption.activate(jid) + + @log_calls('OmemoPlugin') + def omemo_disable_for(self, jid, account): + """ Used by the UI to disable OMEMO for a specified contact. + + WARNING - OMEMO should only be disabled through + User interaction with the UI. + + Parameters + ---------- + jid : str + bare jid + account : str + the account name + """ + state = self.get_omemo_state(account) + state.encryption.deactivate(jid) diff --git a/omemo/xmpp.py b/omemo/xmpp.py index c4097a5..43323a0 100644 --- a/omemo/xmpp.py +++ b/omemo/xmpp.py @@ -79,10 +79,14 @@ class OmemoMessage(Node): # , contact_jid, key, iv, payload, dev_id, my_dev_id): Node.__init__(self, 'encrypted', attrs={'xmlns': NS_OMEMO}) header = Node('header', attrs={'sid': msg_dict['sid']}) - for rid, key in msg_dict['keys'].items(): - header.addChild('key', attrs={'rid': rid}).addData(b64encode(key) - .decode('utf-8')) - + for rid, (key, prekey) in msg_dict['keys'].items(): + if prekey: + child = header.addChild('key', + attrs={'prekey': 'true', 'rid': rid}) + else: + child = header.addChild('key', + attrs={'rid': rid}) + child.addData(b64encode(key).decode('utf-8')) header.addChild('iv').addData(b64encode(msg_dict['iv']).decode('utf-8')) self.addChild(node=header) self.addChild('payload').addData(b64encode(msg_dict['payload']) From 709b44a0baf02acec8167f12ed002af25aba76bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 24 Jan 2017 13:37:05 +0100 Subject: [PATCH 3/5] [omemo] Port UI changes from master --- omemo/config_dialog.ui | 1058 ++++++++++++++++++++++++---------------- omemo/fpr_dialog.ui | 597 ++++++++++++----------- omemo/ui.py | 601 +++++++++++++++-------- 3 files changed, 1342 insertions(+), 914 deletions(-) diff --git a/omemo/config_dialog.ui b/omemo/config_dialog.ui index df0b849..604121d 100644 --- a/omemo/config_dialog.ui +++ b/omemo/config_dialog.ui @@ -1,417 +1,641 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - - - True - False - 12 - 10 - - - True - False - 5 - - - True - False - <b>Account:</b> - True - - - False - True - 0 - - - - - True - False - account_store - - - - - 0 - - - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - - - 110 - True - False - 0 - Own Fingerprint: - - - - - - False - True - 0 - - - - - True - False - <tt>-------- -------- -------- -------- -------- </tt> - True - True - - - False - True - 1 - - - - - False - True - 1 - - - - - True - False - - - 110 - True - False - 0 - Own Device ID: - - - - - - False - False - 0 - - - - - True - False - 0 - 0 - - - False - True - 1 - - - - - False - False - 2 - - - - - - - True - False - Own Fingerprints - - - False - - - - - True - False - 12 - 10 - - - 200 - True - True - automatic - automatic - - - 300 - True - True - fingerprint_store - 0 - 3 - - - - True - Name - - - - 1 - - - - - - - True - Trust - - - - 2 - - - - - - - True - Fingerprint - - - - 3 - - - - - - - - - True - True - 0 - - - - - True - False - 5 - - - Trust/Revoke Fingerprint - 200 - True - True - True - - - - False - False - 0 - - - - - False - False - 1 - - - - - 1 - - - - - True - False - Known Fingerprints - - - 1 - False - - - - - True - False - 12 - 10 - - - 25 - True - False - 0 - Published Devices - - - - - - - False - True - 7 - 0 - - - - - True - True - never - automatic - - - True - False - deviceid_store - 0 - - - Device ID - - - - 0 - - - - - - - - - True - True - 1 - - - - - True - False - 5 - - - Clear Devices - 160 - True - True - True - - - - False - False - 0 - - - - - gtk-refresh - 160 - True - True - True - True - - - - False - False - 1 - - - - - False - False - 2 - - - - - 2 - - - - - True - False - Clear Devices - - - 2 - False - - - - - True - False - - - True - False - Copy to clipboard - True - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + + + True + False + 12 + 10 + + + True + False + 5 + + + True + False + <b>Account:</b> + True + + + False + True + 0 + + + + + True + False + account_store + + + + + 0 + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + + + 110 + True + False + Own Fingerprint: + 0 + + + + + + False + True + 0 + + + + + True + False + True + True + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + + + 110 + True + False + Own Device ID: + 0 + + + + + + False + False + 0 + + + + + True + False + 0 + + + False + True + 1 + + + + + False + False + 2 + + + + + True + False + 0 + gtk-missing-image + 0 + + + True + True + 3 + + + + + False + True + For Verification QRCode please install python-qrcode + 0 + + + + + + + True + True + 4 + + + + + + + True + False + Own Fingerprints + + + False + + + + + True + False + 12 + 10 + + + 200 + True + True + + + 300 + True + True + fingerprint_store + 0 + 3 + + + + + + + True + Name + True + True + 1 + + + + 1 + + + + + + + True + Trust + + + + 2 + + + + + + + True + Fingerprint + + + + 3 + + + + + + + + + True + True + 0 + + + + + True + False + 5 + + + Trust/Revoke Fingerprint + 200 + True + True + True + + + + False + False + 0 + + + + + Delete Fingerprint + 200 + True + True + True + + + + False + False + 1 + + + + + False + False + 1 + + + + + 1 + + + + + True + False + Known Fingerprints + + + 1 + False + + + + + True + False + 12 + 10 + + + 25 + True + False + Published Devices + 0 + + + + + + + False + True + 7 + 0 + + + + + True + True + never + + + True + False + deviceid_store + 0 + + + + + + Device ID + + + + 0 + + + + + + + + + True + True + 1 + + + + + True + False + 5 + + + Clear Devices + 160 + True + True + True + + + + False + False + 0 + + + + + gtk-refresh + 160 + True + True + True + True + + + + False + False + 1 + + + + + False + False + 2 + + + + + 2 + + + + + True + False + Clear Devices + + + 2 + False + + + + + True + False + + + 30 + True + False + You have to restart Gajim for changes to take effect ! + + + + + + + False + True + 0 + + + + + True + False + 12 + 5 + + + True + False + + + True + True + + + True + True + account_store + + + + + + Active Accounts + 0.5 + + + + 0 + + + + + + + + + True + True + 0 + + + + + Disable Account + True + True + True + + + + False + False + 1 + + + + + True + True + 0 + + + + + True + False + + + True + True + + + True + True + disabled_account_store + + + + + + Disabled Accounts + 0.5 + + + + 0 + + + + + + + + + True + True + 0 + + + + + Activate Account + True + True + True + + + + False + False + 1 + + + + + True + True + 1 + + + + + True + True + 1 + + + + + 3 + + + + + True + False + Disable Accounts + + + 3 + False + + + + + True + False + + + True + False + Copy to clipboard + True + + + + + diff --git a/omemo/fpr_dialog.ui b/omemo/fpr_dialog.ui index 76fbcc5..969aebe 100644 --- a/omemo/fpr_dialog.ui +++ b/omemo/fpr_dialog.ui @@ -1,298 +1,299 @@ - - - - - - - - - - - - - - - - - - - - - - - - True - True - - - - True - False - 12 - 10 - - - 200 - True - True - automatic - automatic - - - True - True - fingerprint_store - 0 - 3 - - - - True - Name - - - - 1 - - - - - - - True - Trust - - - - 2 - - - - - - - True - Fingerprint - - - - 3 - - - - - - - - - True - True - 0 - - - - - True - False - 5 - - - Trust/Revoke Fingerprint - 200 - True - True - True - - - - False - False - 0 - - - - - False - True - 1 - - - - - - - True - False - Contact - - - False - - - - - True - False - 12 - 10 - - - True - False - 10 - - - True - False - Own Fingerprint: - - - - - - False - True - 0 - - - - - True - False - <tt>-------- -------- -------- -------- -------- </tt> - True - True - - - False - False - 1 - - - - - False - False - 0 - - - - - 100 - True - True - automatic - automatic - - - True - True - True - fingerprint_store - False - 0 - 3 - - - - True - Name - - - - 1 - - - - - - - True - Trust - - - - 2 - - - - - - - True - Fingerprint - - - - 3 - - - - - - - - - True - True - 1 - - - - - True - False - 5 - - - Trust/Revoke Fingerprint - 200 - True - True - True - - - - False - False - 0 - - - - - False - True - 2 - - - - - 1 - - - - - True - False - Own Devices - - - 1 - False - - - - - True - False - - - True - False - Copy to clipboard - True - - - - - + + + + + + + + + + + + + + + + + + + + True + True + + + + True + False + 12 + 10 + + + 200 + True + True + + + True + True + True + fingerprint_store + False + 0 + 3 + + + + + + + True + Name + True + True + 1 + + + + 1 + + + + + + + True + Trust + + + + 2 + + + + + + + True + Fingerprint + + + + 3 + + + + + + + + + True + True + 0 + + + + + True + False + 5 + + + Trust/Revoke Fingerprint + 200 + True + True + True + + + + False + False + 0 + + + + + False + True + 1 + + + + + + + True + False + Contact + + + False + + + + + True + False + 12 + 10 + + + True + False + 10 + + + True + False + Own Fingerprint: + + + + + + False + True + 0 + + + + + True + False + <tt>-------- -------- -------- -------- -------- </tt> + True + True + + + False + False + 1 + + + + + False + False + 0 + + + + + 100 + True + True + + + True + True + True + fingerprint_store + False + 0 + 3 + + + + + + + True + Name + + + + 1 + + + + + + + True + Trust + + + + 2 + + + + + + + True + Fingerprint + + + + 3 + + + + + + + + + True + True + 1 + + + + + True + False + 5 + + + Trust/Revoke Fingerprint + 200 + True + True + True + + + + False + False + 0 + + + + + False + True + 2 + + + + + 1 + + + + + True + False + Own Devices + + + 1 + False + + + + + True + False + + + True + False + + + + + diff --git a/omemo/ui.py b/omemo/ui.py index e81813a..464913c 100644 --- a/omemo/ui.py +++ b/omemo/ui.py @@ -20,18 +20,30 @@ import binascii import logging +import os +import message_control -from gi.repository import GObject -from gi.repository import Gtk +from gi.repository import GObject, Gtk, GdkPixbuf # pylint: disable=import-error import gtkgui_helpers from common import gajim +from dialogs import YesNoDialog from plugins.gui import GajimPluginConfigDialog -# pylint: enable=import-error +from axolotl.state.sessionrecord import SessionRecord +from common import configpaths log = logging.getLogger('gajim.plugin_system.omemo') +PILLOW = False +try: + import qrcode + PILLOW = True +except Exception as e: + log.exception('Error:') + log.error('python-qrcode or dependencies of it, are not available') + +# pylint: enable=import-error UNDECIDED = 2 TRUSTED = 1 UNTRUSTED = 0 @@ -106,6 +118,12 @@ class Ui(object): self.account = self.contact.account.name self.windowinstances = {} + self.groupchat = False + if chat_control.type_id == message_control.TYPE_GC: + self.groupchat = True + self.omemo_capable = False + self.room = self.chat_control.room_jid + self.display_omemo_state() self.refresh_auth_lock_icon() @@ -133,6 +151,9 @@ class Ui(object): item.set_image(Gtk.Image.new_from_file(icon_path)) item.set_submenu(submenu) + if self.groupchat: + item.set_sensitive(self.omemo_capable) + # at index 8 is the separator after the esession encryption entry menu.insert(item, 8) return menu @@ -159,7 +180,38 @@ class Ui(object): log.debug(self.account + ' => Sending Message to ' + self.contact.jid) - self.chat_control.send_message = omemo_send_message + def omemo_send_gc_message(message, xhtml=None, process_commands=True): + self.new_fingerprints_available() + if self.encryption_active(): + missing = True + own_jid = gajim.get_jid_from_account(self.account) + for nick in self.plugin.groupchat[self.room]: + real_jid = self.plugin.groupchat[self.room][nick] + if real_jid == own_jid: + continue + if not self.plugin.are_keys_missing(self.account, + real_jid): + missing = False + if missing: + log.debug(self.account + + ' => No Trusted Fingerprints for ' + + self.room) + self.no_trusted_fingerprints_warning() + else: + self.chat_control.orig_send_message(message, xhtml, + process_commands) + log.debug(self.account + ' => Sending Message to ' + + self.room) + else: + self.chat_control.orig_send_message(message, xhtml, + process_commands) + log.debug(self.account + ' => Sending Message to ' + + self.room) + + if self.groupchat: + self.chat_control.send_message = omemo_send_gc_message + else: + self.chat_control.send_message = omemo_send_message def set_omemo_state(self, enabled): """ @@ -182,6 +234,12 @@ class Ui(object): self.omemobutton.set_omemo_state(enabled) self.display_omemo_state() + def sensitive(self, value): + self.omemobutton.set_sensitive(value) + self.omemo_capable = value + if value: + self.chat_control.prepare_context_menu + def encryption_active(self): return self.state.encryption.is_active(self.contact.jid) @@ -190,16 +248,31 @@ class Ui(object): self.set_omemo_state(True) def new_fingerprints_available(self): - fingerprints = self.state.store.getNewFingerprints(self.contact.jid) - if fingerprints: - self.show_fingerprint_window(fingerprints) + jid = self.contact.jid + if self.groupchat and self.room in self.plugin.groupchat: + for nick in self.plugin.groupchat[self.room]: + real_jid = self.plugin.groupchat[self.room][nick] + fingerprints = self.state.store. \ + getNewFingerprints(real_jid) + if fingerprints: + self.show_fingerprint_window(fingerprints) + elif not self.groupchat: + fingerprints = self.state.store.getNewFingerprints(jid) + if fingerprints: + self.show_fingerprint_window(fingerprints) def show_fingerprint_window(self, fingerprints=None): if 'dialog' not in self.windowinstances: - self.windowinstances['dialog'] = \ - FingerprintWindow(self.plugin, self.contact, - self.chat_control.parent_win.window, - self.windowinstances) + if self.groupchat: + self.windowinstances['dialog'] = \ + FingerprintWindow(self.plugin, self.contact, + self.chat_control.parent_win.window, + self.windowinstances, groupchat=True) + else: + self.windowinstances['dialog'] = \ + FingerprintWindow(self.plugin, self.contact, + self.chat_control.parent_win.window, + self.windowinstances) self.windowinstances['dialog'].show_all() if fingerprints: log.debug(self.account + @@ -225,10 +298,12 @@ class Ui(object): def no_trusted_fingerprints_warning(self): msg = "To send an encrypted message, you have to " \ - "first trust the fingerprint of your contact!" + "first trust the fingerprint of your contact!" self.chat_control.print_conversation_line(msg, 'status', '', None) def refresh_auth_lock_icon(self): + if self.groupchat: + return if self.encryption_active(): if self.state.getUndecidedFingerprints(self.contact.jid): self.chat_control._show_lock_image(True, 'OMEMO', True, True, @@ -242,6 +317,8 @@ class Ui(object): def removeUi(self): self.actions_hbox.remove(self.omemobutton) + self.chat_control._show_lock_image(False, 'OMEMO', False, True, + False) self.chat_control.prepare_context_menu = \ self.chat_control.omemo_orig_prepare_context_menu self.chat_control.send_message = self.chat_control.orig_send_message @@ -256,40 +333,144 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): self.B.set_translation_domain('gajim_plugins') self.B.add_from_file(self.GTK_BUILDER_FILE_PATH) - self.fpr_model = Gtk.ListStore(GObject.TYPE_INT, - GObject.TYPE_STRING, - GObject.TYPE_STRING, - GObject.TYPE_STRING) + try: + self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS'] + except KeyError: + self.plugin.config['DISABLED_ACCOUNTS'] = [] + self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS'] - self.device_model = Gtk.ListStore(GObject.TYPE_INT) + log.debug('Disabled Accounts:') + log.debug(self.disabled_accounts) - self.account_store = self.B.get_object('account_store') - - for account in sorted(gajim.contacts.get_accounts()): - self.account_store.append(row=(account,)) + self.fpr_model = self.B.get_object('fingerprint_store') + self.device_model = self.B.get_object('deviceid_store') self.fpr_view = self.B.get_object('fingerprint_view') - self.fpr_view.set_model(self.fpr_model) - self.fpr_view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) - self.device_view = self.B.get_object('deviceid_view') - self.device_view.set_model(self.device_model) + self.disabled_acc_store = self.B.get_object('disabled_account_store') + self.account_store = self.B.get_object('account_store') - if len(self.account_store) > 0: - self.B.get_object('account_combobox').set_active(0) + self.active_acc_view = self.B.get_object('active_accounts_view') + self.disabled_acc_view = self.B.get_object('disabled_accounts_view') vbox = self.get_content_area() vbox.pack_start(self.B.get_object('notebook1'), True, True, 0) self.B.connect_signals(self) + self.plugin_active = False + def on_run(self): - self.update_context_list() - self.account_combobox_changed_cb(self.B.get_object('account_combobox')) + for plugin in gajim.plugin_manager.active_plugins: + log.debug(type(plugin)) + if type(plugin).__name__ == 'OmemoPlugin': + self.plugin_active = True + break + self.update_account_store() + self.update_account_combobox() + self.update_disabled_account_view() + + def is_in_accountstore(self, account): + for row in self.account_store: + if row[0] == account: + return True + return False + + def update_account_store(self): + for account in sorted(gajim.contacts.get_accounts()): + if account not in self.disabled_accounts and \ + not self.is_in_accountstore(account): + self.account_store.append(row=(account,)) + + def update_account_combobox(self): + if self.plugin_active is False: + return + if len(self.account_store) > 0: + self.B.get_object('account_combobox').set_active(0) + else: + self.account_combobox_changed_cb( + self.B.get_object('account_combobox')) def account_combobox_changed_cb(self, box, *args): self.update_context_list() + def get_qrcode(self, jid, sid, fingerprint): + file_name = 'omemo_{}.png'.format(jid) + path = os.path.join( + configpaths.gajimpaths['MY_DATA'], file_name) + + ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint) + log.debug('Verification String: ' + ver_string) + + if os.path.exists(path): + return path + + 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() + img.save(path) + return path + + def update_disabled_account_view(self): + self.disabled_acc_store.clear() + for account in self.disabled_accounts: + self.disabled_acc_store.append(row=(account,)) + + def activate_accounts_btn_clicked(self, button, *args): + mod, paths = self.disabled_acc_view.get_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.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() + 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.update_disabled_account_view() + self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts + self.update_account_combobox() + + def delfpr_button_clicked(self, button, *args): + active = self.B.get_object('account_combobox').get_active() + account = self.account_store[active][0] + + state = self.plugin.get_omemo_state(account) + + mod, paths = self.fpr_view.get_selection().get_selected_rows() + + def on_yes(checked): + record = state.store.loadSession(jid, deviceid) + identity_key = record.getSessionState().getRemoteIdentityKey() + + state.store.deleteSession(jid, deviceid) + state.store.deleteIdentity(jid, identity_key) + self.update_context_list() + + for path in paths: + it = mod.get_iter(path) + jid, fpr, deviceid = mod.get(it, 1, 3, 4) + fpr = fpr[31:-12] + + YesNoDialog( + 'Delete Fingerprint?', + 'Do you want to delete the ' + 'fingerprint of {} on your account {}?' + '\n\n{}'.format(jid, account, fpr), + on_response_yes=on_yes, transient_for=self) + def trust_button_clicked_cb(self, button, *args): active = self.B.get_object('account_combobox').get_active() account = self.account_store[active][0] @@ -298,43 +479,42 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): mod, paths = self.fpr_view.get_selection().get_selected_rows() + def on_yes(checked, identity_key): + state.store.setTrust(identity_key, TRUSTED) + try: + if self.plugin.ui_list[account]: + self.plugin.ui_list[account][jid]. \ + refresh_auth_lock_icon() + except: + log.debug('UI not available') + self.update_context_list() + + def on_no(identity_key): + state.store.setTrust(identity_key, UNTRUSTED) + try: + if jid in self.plugin.ui_list[account]: + self.plugin.ui_list[account][jid]. \ + refresh_auth_lock_icon() + except: + log.debug('UI not available') + self.update_context_list() + for path in paths: it = mod.get_iter(path) - _id, user, fpr = mod.get(it, 0, 1, 3) + jid, fpr, deviceid = mod.get(it, 1, 3, 4) fpr = fpr[31:-12] - dlg = Gtk.Dialog('Trust / Revoke Fingerprint', self, - Gtk.DialogFlags.MODAL | - Gtk.DialogFlags.DESTROY_WITH_PARENT, - (Gtk.STOCK_YES, Gtk.ResponseType.YES, - Gtk.STOCK_NO, Gtk.ResponseType.NO)) - l = Gtk.Label() - l.set_markup('Do you want to trust the ' - 'fingerprint of %s on your account %s?' - '\n\n%s' % (user, account, fpr)) - l.set_line_wrap(True) - l.set_padding(12, 12) - vbox = dlg.get_content_area() - vbox.add(l) - dlg.show_all() - response = dlg.run() - if response == Gtk.ResponseType.YES: - state.store.identityKeyStore.setTrust(_id, TRUSTED) - try: - if self.plugin.ui_list[account]: - self.plugin.ui_list[account][user].refresh_auth_lock_icon() - except: - dlg.destroy() - else: - if response == Gtk.ResponseType.NO: - state.store.identityKeyStore.setTrust(_id, UNTRUSTED) - try: - if user in self.plugin.ui_list[account]: - self.plugin.ui_list[account][user].refresh_auth_lock_icon() - except: - dlg.destroy() + record = state.store.loadSession(jid, deviceid) + identity_key = record.getSessionState().getRemoteIdentityKey() - self.update_context_list() + YesNoDialog( + 'Trust / Revoke Fingerprint?', + 'Do you want to trust the fingerprint of {} ' + 'on your account {}?\n\n' + '{}'.format(jid, account, fpr), + on_response_yes=(on_yes, identity_key), + on_response_no=(on_no, identity_key), + transient_for=self) def cleardevice_button_clicked_cb(self, button, *args): active = self.B.get_object('account_combobox').get_active() @@ -380,67 +560,94 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): def update_context_list(self): self.fpr_model.clear() self.device_model.clear() + self.qrcode = self.B.get_object('qrcode') + self.qrinfo = self.B.get_object('qrinfo') + if len(self.account_store) == 0: + self.B.get_object('ID').set_markup('') + self.B.get_object('fingerprint_label').set_markup('') + self.B.get_object('trust_button').set_sensitive(False) + self.B.get_object('delfprbutton').set_sensitive(False) + self.B.get_object('refresh').set_sensitive(False) + self.B.get_object('cleardevice_button').set_sensitive(False) + self.B.get_object('qrcode').clear() + return active = self.B.get_object('account_combobox').get_active() account = self.account_store[active][0] - state = self.plugin.get_omemo_state(account) + # Set buttons active + self.B.get_object('trust_button').set_sensitive(True) + self.B.get_object('delfprbutton').set_sensitive(True) + self.B.get_object('refresh').set_sensitive(True) + if account == 'Local': + self.B.get_object('cleardevice_button').set_sensitive(False) + else: + self.B.get_object('cleardevice_button').set_sensitive(True) + + # Set FPR Label and DeviceID + state = self.plugin.get_omemo_state(account) deviceid = state.own_device_id self.B.get_object('ID').set_markup('%s' % deviceid) ownfpr = binascii.hexlify(state.store.getIdentityKeyPair() .getPublicKey().serialize()).decode('utf-8') - ownfpr = self.human_hash(ownfpr[2:]) + human_ownfpr = human_hash(ownfpr[2:]) self.B.get_object('fingerprint_label').set_markup('%s' - % ownfpr) + % human_ownfpr) - fprDB = state.store.identityKeyStore.getAllFingerprints() - activeSessions = state.store.sessionStore. \ - getAllActiveSessionsKeys() - for item in fprDB: - _id, jid, fpr, tr = item - active = fpr in activeSessions - fpr = binascii.hexlify(fpr).decode('utf-8') - fpr = self.human_hash(fpr[2:]) - jid = jid.decode('utf-8') - if tr == UNTRUSTED: - if active: - self.fpr_model.append((_id, jid, 'False', - '%s' % fpr)) - else: - self.fpr_model.append((_id, jid, 'False', - '%s' % fpr)) - elif tr == TRUSTED: - if active: - self.fpr_model.append((_id, jid, 'True', - '%s' % fpr)) - else: - self.fpr_model.append((_id, jid, 'True', - '%s' % fpr)) - else: - if active: - self.fpr_model.append((_id, jid, 'Undecided', - '%s' % fpr)) - else: - self.fpr_model.append((_id, jid, 'Undecided', - '%s' % fpr)) + # Set Fingerprint List + trust_str = {0: 'False', 1: 'True', 2: 'Undecided'} + session_db = state.store.getAllSessions() + for item in session_db: + color = {0: '#FF0040', # red + 1: '#2EFE2E', # green + 2: '#FF8000'} # orange + + _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 = human_hash(fpr[2:]) + + trust = state.store.isTrustedIdentity(jid, identity_key) + + if not active: + color[trust] = '#585858' # grey + + self.fpr_model.append( + (_id, jid, trust_str[trust], + '{}'. + format(color[trust], fpr), + deviceid)) + + # Set Device ID List for item in state.own_devices: self.device_model.append([item]) - 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]) - return buf.rstrip() + # Set QR Verification Code + if PILLOW: + path = self.get_qrcode( + gajim.get_jid_from_account(account), deviceid, ownfpr[2:]) + self.qrcode.set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file(path)) + self.qrinfo.hide() + else: + self.qrinfo.show() class FingerprintWindow(Gtk.Dialog): - def __init__(self, plugin, contact, parent, windowinstances): + def __init__(self, plugin, contact, parent, windowinstances, + groupchat=False): + self.groupchat = groupchat self.contact = contact self.windowinstances = windowinstances + self.account = self.contact.account.name + self.plugin = plugin + self.omemostate = self.plugin.get_omemo_state(self.account) + self.own_jid = gajim.get_jid_from_account(self.account) Gtk.Dialog.__init__(self, title=('Fingerprints for %s') % contact.jid, parent=parent, @@ -448,45 +655,34 @@ class FingerprintWindow(Gtk.Dialog): close_button = self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) close_button.connect('clicked', self.on_close_button_clicked) self.connect('delete-event', self.on_window_delete) - self.plugin = plugin + self.GTK_BUILDER_FILE_PATH = \ self.plugin.local_file_path('fpr_dialog.ui') - self.B = Gtk.Builder() - self.B.set_translation_domain('gajim_plugins') - self.B.add_from_file(self.GTK_BUILDER_FILE_PATH) + self.xml = Gtk.Builder() + self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH) + self.xml.set_translation_domain('gajim_plugins') - self.fpr_model = Gtk.ListStore(GObject.TYPE_INT, - GObject.TYPE_STRING, - GObject.TYPE_STRING, - GObject.TYPE_STRING) + self.fpr_model = self.xml.get_object('fingerprint_store') - self.fpr_view = self.B.get_object('fingerprint_view') - self.fpr_view.set_model(self.fpr_model) - self.fpr_view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) - - self.fpr_view_own = self.B.get_object('fingerprint_view_own') - self.fpr_view_own.set_model(self.fpr_model) - self.fpr_view_own.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) - - self.notebook = self.B.get_object('notebook1') + self.fpr_view = self.xml.get_object('fingerprint_view') + self.fpr_view_own = self.xml.get_object('fingerprint_view_own') + self.notebook = self.xml.get_object('notebook1') vbox = self.get_content_area() vbox.pack_start(self.notebook, True, True, 0) - self.B.connect_signals(self) - - self.account = self.contact.account.name - self.omemostate = self.plugin.get_omemo_state(self.account) + self.xml.connect_signals(self) + # Set own Fingerprint Label ownfpr = binascii.hexlify(self.omemostate.store.getIdentityKeyPair() .getPublicKey().serialize()).decode('utf-8') - ownfpr = self.human_hash(ownfpr[2:]) - - self.B.get_object('fingerprint_label_own').set_markup('%s' - % ownfpr) - + ownfpr = human_hash(ownfpr[2:]) + self.xml.get_object('fingerprint_label_own').set_markup('%s' + % ownfpr) self.update_context_list() + self.show_all() + def on_close_button_clicked(self, widget): del self.windowinstances['dialog'] self.hide() @@ -496,43 +692,43 @@ class FingerprintWindow(Gtk.Dialog): self.hide() def trust_button_clicked_cb(self, button, *args): + state = self.omemostate + if self.notebook.get_current_page() == 1: mod, paths = self.fpr_view_own.get_selection().get_selected_rows() else: mod, paths = self.fpr_view.get_selection().get_selected_rows() - for path in paths: - it = mod.get_iter(path) - _id, user, fpr = mod.get(it, 0, 1, 3) - fpr = fpr[31:-12] - dlg = Gtk.Dialog('Trust / Revoke Fingerprint', self, - Gtk.DialogFlags.MODAL | - Gtk.DialogFlags.DESTROY_WITH_PARENT, - (Gtk.STOCK_YES, Gtk.ResponseType.YES, - Gtk.STOCK_NO, Gtk.ResponseType.NO)) - l = Gtk.Label() - l.set_markup('Do you want to trust the ' - 'fingerprint of %s on your account %s?' - '\n\n%s' % (user, self.account, fpr)) - l.set_line_wrap(True) - l.set_padding(12, 12) - vbox = dlg.get_content_area() - vbox.add(l) - dlg.show_all() - response = dlg.run() - if response == Gtk.ResponseType.YES: - self.omemostate.store.identityKeyStore.setTrust(_id, TRUSTED) + def on_yes(checked, identity_key): + state.store.setTrust(identity_key, TRUSTED) + if not self.groupchat: self.plugin.ui_list[self.account][self.contact.jid]. \ refresh_auth_lock_icon() - dlg.destroy() - else: - if response == Gtk.ResponseType.NO: - self.omemostate.store.identityKeyStore.setTrust(_id, UNTRUSTED) - self.plugin.ui_list[self.account][self.contact.jid]. \ - refresh_auth_lock_icon() - dlg.destroy() + self.update_context_list() - self.update_context_list() + def on_no(identity_key): + state.store.setTrust(identity_key, UNTRUSTED) + if not self.groupchat: + self.plugin.ui_list[self.account][self.contact.jid]. \ + refresh_auth_lock_icon() + self.update_context_list() + + for path in paths: + it = mod.get_iter(path) + jid, fpr, deviceid = mod.get(it, 1, 3, 4) + fpr = fpr[31:-12] + + record = state.store.loadSession(jid, deviceid) + identity_key = record.getSessionState().getRemoteIdentityKey() + + YesNoDialog( + 'Trust / Revoke Fingerprint?', + 'Do you want to trust the fingerprint of {} ' + 'on your account {}?\n\n' + '{}'.format(jid, self.account, fpr), + on_response_yes=(on_yes, identity_key), + on_response_no=(on_no, identity_key), + transient_for=self) def fpr_button_pressed_cb(self, tw, event): if event.button == 3: @@ -547,7 +743,7 @@ class FingerprintWindow(Gtk.Dialog): # selection, otherwise we only select the new item keep_selection = tw.get_selection().path_is_selected(pthinfo[0]) - pop = self.B.get_object('fprclipboard_menu') + pop = self.xml.get_object('fprclipboard_menu') pop.popup(None, None, None, event.button, event.time) # keep_selection=True -> no further processing of click event @@ -571,49 +767,56 @@ class FingerprintWindow(Gtk.Dialog): def update_context_list(self, *args): self.fpr_model.clear() + state = self.omemostate if self.notebook.get_current_page() == 1: - jid = gajim.get_jid_from_account(self.account) + contact_jid = self.own_jid else: - jid = self.contact.jid + contact_jid = self.contact.jid - fprDB = self.omemostate.store.identityKeyStore.getFingerprints(jid) - activeSessions = self.omemostate.store.sessionStore. \ - getActiveSessionsKeys(jid) + trust_str = {0: 'False', 1: 'True', 2: 'Undecided'} + if self.groupchat and self.notebook.get_current_page() == 0: + contact_jids = [] + for nick in self.plugin.groupchat[contact_jid]: + real_jid = self.plugin.groupchat[contact_jid][nick] + if real_jid == self.own_jid: + continue + contact_jids.append(real_jid) + session_db = state.store.getSessionsFromJids(contact_jids) + else: + session_db = state.store.getSessionsFromJid(contact_jid) - for item in fprDB: - _id, jid, fpr, tr = item - active = fpr in activeSessions - fpr = binascii.hexlify(fpr).decode('utf-8') - fpr = self.human_hash(fpr[2:]) - jid = jid.decode('utf-8') - if tr == UNTRUSTED: - if active: - self.fpr_model.append((_id, jid, 'False', - '%s' % fpr)) - else: - self.fpr_model.append((_id, jid, 'False', - '%s' % fpr)) - elif tr == TRUSTED: - if active: - self.fpr_model.append((_id, jid, 'True', - '%s' % fpr)) - else: - self.fpr_model.append((_id, jid, 'True', - '%s' % fpr)) - else: - if active: - self.fpr_model.append((_id, jid, 'Undecided', - '%s' % fpr)) - else: - self.fpr_model.append((_id, jid, 'Undecided', - '%s' % fpr)) + for item in session_db: + color = {0: '#FF0040', # red + 1: '#2EFE2E', # green + 2: '#FF8000'} # orange - 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]) - return buf.rstrip() + _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 = human_hash(fpr[2:]) + + trust = state.store.isTrustedIdentity(jid, identity_key) + + if not active: + color[trust] = '#585858' # grey + + self.fpr_model.append( + (_id, jid, trust_str[trust], + '{}'. + format(color[trust], fpr), + deviceid)) + + +def human_hash(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]) + return buf.rstrip() From 832c9879c8b7410c929c01257002b191205ea709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 24 Jan 2017 14:29:57 +0100 Subject: [PATCH 4/5] [omemo] Update/Delete various files --- omemo/.pylintrc | 379 -------------------------------------------- omemo/.style.yapf | 4 - omemo/CHANGELOG | 33 +++- omemo/README.md | 90 ----------- omemo/pkgs/PKGBUILD | 24 --- omemo/setup.cfg | 2 - 6 files changed, 29 insertions(+), 503 deletions(-) delete mode 100644 omemo/.pylintrc delete mode 100644 omemo/.style.yapf delete mode 100644 omemo/README.md delete mode 100644 omemo/pkgs/PKGBUILD delete mode 100644 omemo/setup.cfg diff --git a/omemo/.pylintrc b/omemo/.pylintrc deleted file mode 100644 index 7222fa7..0000000 --- a/omemo/.pylintrc +++ /dev/null @@ -1,379 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,input - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,e,Run,_,log,ui,iq,db - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make,_show_lock_image - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/omemo/.style.yapf b/omemo/.style.yapf deleted file mode 100644 index 9277d6a..0000000 --- a/omemo/.style.yapf +++ /dev/null @@ -1,4 +0,0 @@ -[style] -based_on_style = pep8 -align_closing_bracket_with_visual_indent = true -join_multiple_lines = true diff --git a/omemo/CHANGELOG b/omemo/CHANGELOG index 20bd7d1..82c46bd 100644 --- a/omemo/CHANGELOG +++ b/omemo/CHANGELOG @@ -1,8 +1,33 @@ +1.0.1 / 2017-01-14 +- Better XEP Compliance +- Bugfixes + +1.0.0 / 2016-12-04 +- Bugfixes + +0.9.9 / 2016-12-01 +- Bugfixes + +0.9.8 / 2016-11-28 +- Fix a Problem where OMEMO wouldnt activate after the plugin is updated +- Add QR Verification Code to Plugin Config + +0.9.7 / 2016-11-12 +- Bugfixes + +0.9.6 / 2016-11-01 +- Bugfixes + +0.9.5 / 2016-10-10 +- Add GroupChat BETA +- Add Option to delete Fingerprints +- Add Option to deactivate Accounts for OMEMO + 0.9.0 / 2016-08-28 - - Send INFO message to resources who dont support OMEMO - - Check dependencys and give correct error message - - Dont process PreKeyWhisperMessages without PreKey - - Dont process PGP messages +- Send INFO message to resources who dont support OMEMO +- Check dependencys and give correct error message +- Dont process PreKeyWhisperMessages without PreKey +- Dont process PGP messages 0.8.1 / 2016-08-05 - Query own Device Bundles on send button press diff --git a/omemo/README.md b/omemo/README.md deleted file mode 100644 index e72cceb..0000000 --- a/omemo/README.md +++ /dev/null @@ -1,90 +0,0 @@ -# OMEMO Plugin for Gajim - -This Plugin adds support for the [OMEMO Encryption](http://conversations.im/omemo) to [Gajim](https://gajim.org/). This -plugin is [free software](http://www.gnu.org/philosophy/free-sw.en.html) -distributed under the GNU General Public License version 3 or any later version. - -## Installation - -Before you open any issues please read our [Wiki](https://github.com/omemo/gajim-omemo/wiki) which addresses some problems that can occur during an install - -### Linux - -See [Linux Wiki](https://github.com/omemo/gajim-omemo/wiki/Installing-on-Linux) - -### Windows - -See [Windows Wiki](https://github.com/omemo/gajim-omemo/wiki/Installing-on-Windows) - -### Via Package Manager -#### Arch -See [Arch Wiki](https://wiki.archlinux.org/index.php/Gajim#OMEMO_Support) - -#### Gentoo -`layman -a flow && emerge gajim-omemo` - -### Via PluginInstallerPlugin - -Install the current stable version via the Gajim PluginManager. You *need* Gajim -version *0.16.5*. If your package manager does not provide an up to date version -you can install it from the official Mercurial repository. *DO NOT USE* gajim -0.16.4 it contains a vulnerability, which is fixed in 0.16.5. -```shell -hg clone https://hg.gajim.org/gajim -cd gajim -hg update gajim-0.16.5 --clean -``` - -**NOTE:** You *have* to install `python-axolotl` via `pip`. Depending on your setup you might -want to use `pip2` as Gajim is using python2.7. If you are using the official repository, -do not forget to install the `nbxmpp` dependency via pip or you package manager. - -if you still have problems, we have written down the most common problems [here](https://github.com/omemo/gajim-omemo/wiki/It-doesnt-work,-what-should-i-do%3F-(Linux)) - -## Running -Enable *OMEMO Multi-End Message and Object Encryption* in the Plugin-Manager. -If your contact supports OMEMO you should see a new orange fish icon in the chat window. - -Encryption will be enabled by default for contacts that support OMEMO. -If you open the chat window, the Plugin will tell you with a green status message if its *enabled* or *disabled*. -If you see no status message, your contact doesnt support OMEMO. -(**Beware**, every status message is green. A green message does not mean encryption is active. Read the message !) -You can also check if encryption is enabled/disabled, when you click on the OMEMO icon. - -When you send your first message the Plugin will query your contacts encryption keys and you will -see them in a readable fingerprint format in the fingerprint window which pops up. -you have to trust at least **one** fingerprint to send messages. -you can receive messages from fingerprints where you didnt made a trust decision, but you cant -receive Messages from *not trusted* fingerprints - - -## Debugging -To see OMEMO related debug output start Gajim with the parameter `-l -gajim.plugin_system.omemo=DEBUG`. - -## Hacking -This repository contains the current development version. If you want to -contribute clone the git repository into your Gajim's plugin directory. -```shell -mkdir ~/.local/share/gajim/plugins -p -cd ~/.local/share/gajim/plugins -git clone https://github.com/omemo/gajim-omemo -``` - -## Support this project -I develop this project in my free time. Your donation allows me to spend more -time working on it and on free software generally. - -My Bitcoin Address is: `1CnNM3Mree9hU8eRjCXrfCWVmX6oBnEfV1` - -[![Support Me via Flattr](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/thing/5038679) - -## I found a bug -Please report it to the [issue -tracker](https://github.com/omemo/gajim-omemo/issues). If you are experiencing -misbehaviour please provide detailed steps to reproduce and debugging output. -Always mention the exact Gajim version. - -## Contact -You can contact me via email at `bahtiar@gadimov.de` or follow me on -[Twitter](https://twitter.com/_kalkin) diff --git a/omemo/pkgs/PKGBUILD b/omemo/pkgs/PKGBUILD deleted file mode 100644 index 21ce483..0000000 --- a/omemo/pkgs/PKGBUILD +++ /dev/null @@ -1,24 +0,0 @@ -# Maintainer: Tommaso Sardelli - -pkgname=gajim-plugin-omemo -_pkgname=gajim-omemo -pkgver=0.8.1 -pkgrel=2 -pkgdesc="Gajim plugin for OMEMO Multi-End Message and Object Encryption." -arch=(any) -url="https://github.com/omemo/${_pkgname}" -license=('GPL') -depends=("gajim" "python2-setuptools" "python2-cryptography" "python2-axolotl-git") -provides=('gajim-plugin-omemo') -conflicts=('gajim-plugin-omemo-git') -source=("https://github.com/omemo/${_pkgname}/archive/${pkgver}.tar.gz") -sha512sums=('e9280033fbe111f5010f2e9e8fa32c5b8c0abe308000f9a043a1c5e8215c96f8be434876b1d72cc8d68aed4ddaebe9655c70f9648a2db718cba71d90434fee2e') - -package() { - cd $srcdir/gajim-omemo-${pkgver} - rm -r CHANGELOG COPYING doc pkgs README.md - install -d ${pkgdir}/usr/share/gajim/plugins/omemo - cp -r * ${pkgdir}/usr/share/gajim/plugins/omemo/ -} - -# vim:set ts=2 sw=2 et: diff --git a/omemo/setup.cfg b/omemo/setup.cfg deleted file mode 100644 index 2cd96cc..0000000 --- a/omemo/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length=80 From 6ecf3c91ed1d6de055be3e029ba2ba9e2661fd78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 24 Jan 2017 14:41:00 +0100 Subject: [PATCH 5/5] [omemo] Update manifest.ini --- omemo/manifest.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/omemo/manifest.ini b/omemo/manifest.ini index ee82b11..d8822c5 100644 --- a/omemo/manifest.ini +++ b/omemo/manifest.ini @@ -1,11 +1,11 @@ [info] name: OMEMO short_name: omemo -version: 0.9.0 +version: 1.0.1 description: OMEMO is an XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption based on Axolotl and PEP. You need to install some dependencys, you can find install instructions for your system in the Github Wiki. authors: Bahtiar `kalkin-` Gadimov Daniel Gultsch Philipp Hörist -homepage: http://github.com/omemo/gajim-omemo.git +homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/OmemoGajimPlugin min_gajim_version: 0.16.9 max_gajim_version: 0.16.11