diff --git a/omemo/__init__.py b/omemo/__init__.py index 47757fc..53d9664 100644 --- a/omemo/__init__.py +++ b/omemo/__init__.py @@ -1 +1 @@ -from .omemoplugin import OmemoPlugin +from omemo.plugin import OmemoPlugin diff --git a/omemo/omemo/__init__.py b/omemo/backend/__init__.py similarity index 95% rename from omemo/omemo/__init__.py rename to omemo/backend/__init__.py index 3f5c4a7..3dc1f76 100644 --- a/omemo/omemo/__init__.py +++ b/omemo/backend/__init__.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.0" diff --git a/omemo/backend/aes.py b/omemo/backend/aes.py new file mode 100644 index 0000000..581697a --- /dev/null +++ b/omemo/backend/aes.py @@ -0,0 +1,86 @@ +# Copyright (C) 2019 Philipp Hörist +# +# This file is part of OMEMO Gajim Plugin. +# +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OMEMO Gajim Plugin. If not, see . + + +import os +import logging +from collections import namedtuple + +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives.ciphers.modes import GCM +from cryptography.hazmat.backends import default_backend + +log = logging.getLogger('gajim.plugin_system.omemo') + +EncryptionResult = namedtuple('EncryptionResult', 'payload key iv') + + +def _decrypt(key, iv, tag, data): + decryptor = Cipher( + algorithms.AES(key), + GCM(iv, tag=tag), + backend=default_backend()).decryptor() + return decryptor.update(data) + decryptor.finalize() + + +def aes_decrypt(_key, iv, payload): + 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:] + + return _decrypt(key, iv, tag, data).decode() + + +def aes_decrypt_file(key, iv, payload): + data = payload[:-16] + tag = payload[-16:] + return _decrypt(key, iv, tag, data) + + +def _encrypt(data, key_size, iv_size): + if isinstance(data, str): + data = data.encode() + key = os.urandom(key_size) + iv = os.urandom(iv_size) + encryptor = Cipher( + algorithms.AES(key), + GCM(iv), + backend=default_backend()).encryptor() + + payload = encryptor.update(data) + encryptor.finalize() + return key, iv, encryptor.tag, payload + + +def aes_encrypt(plaintext): + key, iv, tag, payload = _encrypt(plaintext, 16, 16) + key += tag + return EncryptionResult(payload=payload, key=key, iv=iv) + + +def aes_encrypt_file(data): + key, iv, tag, payload, = _encrypt(data, 32, 16) + payload += tag + return EncryptionResult(payload=payload, key=key, iv=iv) diff --git a/omemo/omemo/db_helpers.py b/omemo/backend/db_helpers.py similarity index 96% rename from omemo/omemo/db_helpers.py rename to omemo/backend/db_helpers.py index dc95d6c..2045955 100644 --- a/omemo/omemo/db_helpers.py +++ b/omemo/backend/db_helpers.py @@ -1,15 +1,15 @@ -''' Database helper functions ''' - - -def table_exists(db, name): - """ Check if the specified table exists in the db. """ - - query = """ SELECT name FROM sqlite_master - WHERE type='table' AND name=?; - """ - return db.execute(query, (name, )).fetchone() is not None - - -def user_version(db): - """ Return the value of PRAGMA user_version. """ - return db.execute('PRAGMA user_version').fetchone()[0] +''' Database helper functions ''' + + +def table_exists(db, name): + """ Check if the specified table exists in the db. """ + + query = """ SELECT name FROM sqlite_master + WHERE type='table' AND name=?; + """ + return db.execute(query, (name, )).fetchone() is not None + + +def user_version(db): + """ Return the value of PRAGMA user_version. """ + return db.execute('PRAGMA user_version').fetchone()[0] diff --git a/omemo/omemo/encryption.py b/omemo/backend/encryption.py similarity index 97% rename from omemo/omemo/encryption.py rename to omemo/backend/encryption.py index e4d4fd8..6cb6983 100644 --- a/omemo/omemo/encryption.py +++ b/omemo/backend/encryption.py @@ -1,64 +1,64 @@ -# -*- 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 . -# - - -class EncryptionState(): - """ Used to store if OMEMO is enabled or not between gajim restarts """ - - def __init__(self, dbConn): - """ - :type dbConn: Connection - """ - self.dbConn = dbConn - - def activate(self, jid): - q = """INSERT OR REPLACE INTO encryption_state (jid, encryption) - VALUES (?, 1) """ - - c = self.dbConn.cursor() - c.execute(q, (jid, )) - self.dbConn.commit() - - def deactivate(self, jid): - q = """INSERT OR REPLACE INTO encryption_state (jid, encryption) - VALUES (?, 0)""" - - c = self.dbConn.cursor() - c.execute(q, (jid, )) - self.dbConn.commit() - - def is_active(self, jid): - q = 'SELECT encryption FROM encryption_state where jid = ?;' - c = self.dbConn.cursor() - c.execute(q, (jid, )) - result = c.fetchone() - if result is None: - return False - return result[0] - - def exist(self, jid): - q = 'SELECT encryption FROM encryption_state where jid = ?;' - c = self.dbConn.cursor() - c.execute(q, (jid, )) - result = c.fetchone() - if result is None: - return False - else: - return True +# -*- 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 . +# + + +class EncryptionState(): + """ Used to store if OMEMO is enabled or not between gajim restarts """ + + def __init__(self, dbConn): + """ + :type dbConn: Connection + """ + self.dbConn = dbConn + + def activate(self, jid): + q = """INSERT OR REPLACE INTO encryption_state (jid, encryption) + VALUES (?, 1) """ + + c = self.dbConn.cursor() + c.execute(q, (jid, )) + self.dbConn.commit() + + def deactivate(self, jid): + q = """INSERT OR REPLACE INTO encryption_state (jid, encryption) + VALUES (?, 0)""" + + c = self.dbConn.cursor() + c.execute(q, (jid, )) + self.dbConn.commit() + + def is_active(self, jid): + q = 'SELECT encryption FROM encryption_state where jid = ?;' + c = self.dbConn.cursor() + c.execute(q, (jid, )) + result = c.fetchone() + if result is None: + return False + return result[0] + + def exist(self, jid): + q = 'SELECT encryption FROM encryption_state where jid = ?;' + c = self.dbConn.cursor() + c.execute(q, (jid, )) + result = c.fetchone() + if result is None: + return False + else: + return True diff --git a/omemo/omemo/liteaxolotlstore.py b/omemo/backend/liteaxolotlstore.py similarity index 97% rename from omemo/omemo/liteaxolotlstore.py rename to omemo/backend/liteaxolotlstore.py index 968faee..4ee00d1 100644 --- a/omemo/omemo/liteaxolotlstore.py +++ b/omemo/backend/liteaxolotlstore.py @@ -1,186 +1,186 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Tarek Galal -# -# 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 - -from axolotl.state.axolotlstore import AxolotlStore -from axolotl.util.keyhelper import KeyHelper - -from .liteidentitykeystore import LiteIdentityKeyStore -from .liteprekeystore import LitePreKeyStore -from .litesessionstore import LiteSessionStore -from .litesignedprekeystore import LiteSignedPreKeyStore -from .encryption import EncryptionState -from .sql import SQLDatabase - -log = logging.getLogger('gajim.plugin_system.omemo') - -DEFAULT_PREKEY_AMOUNT = 100 -MIN_PREKEY_AMOUNT = 80 -SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days -SPK_CYCLE_TIME = 86400 # 24 Hours - - -class LiteAxolotlStore(AxolotlStore): - def __init__(self, connection): - try: - connection.text_factory = bytes - except(AttributeError): - raise AssertionError('Expected a sqlite3.Connection got ' + - str(connection)) - - self.sql = SQLDatabase(connection) - self.identityKeyStore = LiteIdentityKeyStore(connection) - self.preKeyStore = LitePreKeyStore(connection) - self.signedPreKeyStore = LiteSignedPreKeyStore(connection) - self.sessionStore = LiteSessionStore(connection) - self.encryptionStore = EncryptionState(connection) - - if not self.getLocalRegistrationId(): - log.info("Generating Axolotl keys") - self._generate_axolotl_keys() - - def _generate_axolotl_keys(self): - identityKeyPair = KeyHelper.generateIdentityKeyPair() - registrationId = KeyHelper.generateRegistrationId() - preKeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(), - DEFAULT_PREKEY_AMOUNT) - self.storeLocalData(registrationId, identityKeyPair) - - signedPreKey = KeyHelper.generateSignedPreKey( - identityKeyPair, KeyHelper.getRandomSequence(65536)) - - self.storeSignedPreKey(signedPreKey.getId(), signedPreKey) - - for preKey in preKeys: - self.storePreKey(preKey.getId(), preKey) - - def getIdentityKeyPair(self): - return self.identityKeyStore.getIdentityKeyPair() - - def storeLocalData(self, registrationId, identityKeyPair): - self.identityKeyStore.storeLocalData(registrationId, identityKeyPair) - - def getLocalRegistrationId(self): - return self.identityKeyStore.getLocalRegistrationId() - - 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) - - def getUndecidedFingerprints(self, jid): - return self.identityKeyStore.getUndecidedFingerprints(jid) - - def setShownFingerprints(self, jid): - return self.identityKeyStore.setShownFingerprints(jid) - - def getNewFingerprints(self, jid): - return self.identityKeyStore.getNewFingerprints(jid) - - def loadPreKey(self, preKeyId): - return self.preKeyStore.loadPreKey(preKeyId) - - def loadPreKeys(self): - return self.preKeyStore.loadPendingPreKeys() - - def storePreKey(self, preKeyId, preKeyRecord): - self.preKeyStore.storePreKey(preKeyId, preKeyRecord) - - def containsPreKey(self, preKeyId): - return self.preKeyStore.containsPreKey(preKeyId) - - def removePreKey(self, preKeyId): - self.preKeyStore.removePreKey(preKeyId) - - def loadSession(self, recepientId, deviceId): - return self.sessionStore.loadSession(recepientId, deviceId) - - def getActiveDeviceTuples(self): - return self.sessionStore.getActiveDeviceTuples() - - def getInactiveSessionsKeys(self, recipientId): - return self.sessionStore.getInactiveSessionsKeys(recipientId) - - def getSubDeviceSessions(self, recepientId): - # 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) - - def containsSession(self, recepientId, deviceId): - return self.sessionStore.containsSession(recepientId, deviceId) - - def deleteSession(self, recepientId, deviceId): - self.sessionStore.deleteSession(recepientId, deviceId) - - 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) - - def loadSignedPreKeys(self): - return self.signedPreKeyStore.loadSignedPreKeys() - - def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord): - self.signedPreKeyStore.storeSignedPreKey(signedPreKeyId, - signedPreKeyRecord) - - def containsSignedPreKey(self, signedPreKeyId): - return self.signedPreKeyStore.containsSignedPreKey(signedPreKeyId) - - def removeSignedPreKey(self, signedPreKeyId): - self.signedPreKeyStore.removeSignedPreKey(signedPreKeyId) - - def getNextSignedPreKeyId(self): - return self.signedPreKeyStore.getNextSignedPreKeyId() - - def getCurrentSignedPreKeyId(self): - return self.signedPreKeyStore.getCurrentSignedPreKeyId() - - def getSignedPreKeyTimestamp(self, signedPreKeyId): - return self.signedPreKeyStore.getSignedPreKeyTimestamp(signedPreKeyId) - - def removeOldSignedPreKeys(self, timestamp): - self.signedPreKeyStore.removeOldSignedPreKeys(timestamp) +# -*- coding: utf-8 -*- +# +# Copyright 2015 Tarek Galal +# +# 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 + +from axolotl.state.axolotlstore import AxolotlStore +from axolotl.util.keyhelper import KeyHelper + +from .liteidentitykeystore import LiteIdentityKeyStore +from .liteprekeystore import LitePreKeyStore +from .litesessionstore import LiteSessionStore +from .litesignedprekeystore import LiteSignedPreKeyStore +from .encryption import EncryptionState +from .sql import SQLDatabase + +log = logging.getLogger('gajim.plugin_system.omemo') + +DEFAULT_PREKEY_AMOUNT = 100 +MIN_PREKEY_AMOUNT = 80 +SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days +SPK_CYCLE_TIME = 86400 # 24 Hours + + +class LiteAxolotlStore(AxolotlStore): + def __init__(self, connection): + try: + connection.text_factory = bytes + except(AttributeError): + raise AssertionError('Expected a sqlite3.Connection got ' + + str(connection)) + + self.sql = SQLDatabase(connection) + self.identityKeyStore = LiteIdentityKeyStore(connection) + self.preKeyStore = LitePreKeyStore(connection) + self.signedPreKeyStore = LiteSignedPreKeyStore(connection) + self.sessionStore = LiteSessionStore(connection) + self.encryptionStore = EncryptionState(connection) + + if not self.getLocalRegistrationId(): + log.info("Generating Axolotl keys") + self._generate_axolotl_keys() + + def _generate_axolotl_keys(self): + identityKeyPair = KeyHelper.generateIdentityKeyPair() + registrationId = KeyHelper.generateRegistrationId() + preKeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(), + DEFAULT_PREKEY_AMOUNT) + self.storeLocalData(registrationId, identityKeyPair) + + signedPreKey = KeyHelper.generateSignedPreKey( + identityKeyPair, KeyHelper.getRandomSequence(65536)) + + self.storeSignedPreKey(signedPreKey.getId(), signedPreKey) + + for preKey in preKeys: + self.storePreKey(preKey.getId(), preKey) + + def getIdentityKeyPair(self): + return self.identityKeyStore.getIdentityKeyPair() + + def storeLocalData(self, registrationId, identityKeyPair): + self.identityKeyStore.storeLocalData(registrationId, identityKeyPair) + + def getLocalRegistrationId(self): + return self.identityKeyStore.getLocalRegistrationId() + + 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) + + def getUndecidedFingerprints(self, jid): + return self.identityKeyStore.getUndecidedFingerprints(jid) + + def setShownFingerprints(self, jid): + return self.identityKeyStore.setShownFingerprints(jid) + + def getNewFingerprints(self, jid): + return self.identityKeyStore.getNewFingerprints(jid) + + def loadPreKey(self, preKeyId): + return self.preKeyStore.loadPreKey(preKeyId) + + def loadPreKeys(self): + return self.preKeyStore.loadPendingPreKeys() + + def storePreKey(self, preKeyId, preKeyRecord): + self.preKeyStore.storePreKey(preKeyId, preKeyRecord) + + def containsPreKey(self, preKeyId): + return self.preKeyStore.containsPreKey(preKeyId) + + def removePreKey(self, preKeyId): + self.preKeyStore.removePreKey(preKeyId) + + def loadSession(self, recepientId, deviceId): + return self.sessionStore.loadSession(recepientId, deviceId) + + def getActiveDeviceTuples(self): + return self.sessionStore.getActiveDeviceTuples() + + def getInactiveSessionsKeys(self, recipientId): + return self.sessionStore.getInactiveSessionsKeys(recipientId) + + def getSubDeviceSessions(self, recepientId): + # 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) + + def containsSession(self, recepientId, deviceId): + return self.sessionStore.containsSession(recepientId, deviceId) + + def deleteSession(self, recepientId, deviceId): + self.sessionStore.deleteSession(recepientId, deviceId) + + 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) + + def loadSignedPreKeys(self): + return self.signedPreKeyStore.loadSignedPreKeys() + + def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord): + self.signedPreKeyStore.storeSignedPreKey(signedPreKeyId, + signedPreKeyRecord) + + def containsSignedPreKey(self, signedPreKeyId): + return self.signedPreKeyStore.containsSignedPreKey(signedPreKeyId) + + def removeSignedPreKey(self, signedPreKeyId): + self.signedPreKeyStore.removeSignedPreKey(signedPreKeyId) + + def getNextSignedPreKeyId(self): + return self.signedPreKeyStore.getNextSignedPreKeyId() + + def getCurrentSignedPreKeyId(self): + return self.signedPreKeyStore.getCurrentSignedPreKeyId() + + def getSignedPreKeyTimestamp(self, signedPreKeyId): + return self.signedPreKeyStore.getSignedPreKeyTimestamp(signedPreKeyId) + + def removeOldSignedPreKeys(self, timestamp): + self.signedPreKeyStore.removeOldSignedPreKeys(timestamp) diff --git a/omemo/omemo/liteidentitykeystore.py b/omemo/backend/liteidentitykeystore.py similarity index 97% rename from omemo/omemo/liteidentitykeystore.py rename to omemo/backend/liteidentitykeystore.py index 6d51e17..1e02e63 100644 --- a/omemo/omemo/liteidentitykeystore.py +++ b/omemo/backend/liteidentitykeystore.py @@ -1,174 +1,174 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Tarek Galal -# -# 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 . -# - -from axolotl.ecc.djbec import DjbECPrivateKey, DjbECPublicKey -from axolotl.identitykey import IdentityKey -from axolotl.identitykeypair import IdentityKeyPair -from axolotl.state.identitykeystore import IdentityKeyStore - -UNDECIDED = 2 -TRUSTED = 1 -UNTRUSTED = 0 - - -class LiteIdentityKeyStore(IdentityKeyStore): - def __init__(self, dbConn): - """ - :type dbConn: Connection - """ - self.dbConn = dbConn - - def getIdentityKeyPair(self): - q = "SELECT public_key, private_key FROM identities " + \ - "WHERE recipient_id = -1" - c = self.dbConn.cursor() - c.execute(q) - result = c.fetchone() - - publicKey, privateKey = result - return IdentityKeyPair( - IdentityKey(DjbECPublicKey(publicKey[1:])), - DjbECPrivateKey(privateKey)) - - def getLocalRegistrationId(self): - q = "SELECT registration_id FROM identities WHERE recipient_id = -1" - c = self.dbConn.cursor() - c.execute(q) - result = c.fetchone() - return result[0] if result else None - - def storeLocalData(self, registrationId, identityKeyPair): - q = "INSERT INTO identities( " + \ - "recipient_id, registration_id, public_key, private_key) " + \ - "VALUES(-1, ?, ?, ?)" - c = self.dbConn.cursor() - c.execute(q, - (registrationId, - identityKeyPair.getPublicKey().getPublicKey().serialize(), - identityKeyPair.getPrivateKey().serialize())) - - self.dbConn.commit() - - def saveIdentity(self, recipientId, identityKey): - q = "INSERT INTO identities (recipient_id, public_key, trust) " \ - "VALUES(?, ?, ?)" - c = self.dbConn.cursor() - - if not self.getIdentity(recipientId, identityKey): - c.execute(q, (recipientId, - identityKey.getPublicKey().serialize(), - UNDECIDED)) - self.dbConn.commit() - - def getIdentity(self, recipientId, identityKey): - q = "SELECT * FROM identities WHERE recipient_id = ? " \ - "AND public_key = ?" - c = self.dbConn.cursor() - - c.execute(q, (recipientId, identityKey.getPublicKey().serialize())) - result = c.fetchone() - - 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 = ?" - c = self.dbConn.cursor() - - c.execute(q, (recipientId, identityKey.getPublicKey().serialize())) - result = c.fetchone() - - states = [UNTRUSTED, TRUSTED, UNDECIDED] - - if result and result[0] in states: - return result[0] - else: - return True - - def getAllFingerprints(self): - q = "SELECT _id, recipient_id, public_key, trust FROM identities " \ - "WHERE recipient_id != -1 ORDER BY recipient_id ASC" - c = self.dbConn.cursor() - - result = [] - for row in c.execute(q): - result.append((row[0], row[1], row[2], row[3])) - return result - - def getFingerprints(self, jid): - q = "SELECT _id, recipient_id, public_key, trust FROM identities " \ - "WHERE recipient_id =? ORDER BY trust ASC" - c = self.dbConn.cursor() - - result = [] - c.execute(q, (jid,)) - rows = c.fetchall() - for row in rows: - result.append((row[0], row[1], row[2], row[3])) - return result - - def getTrustedFingerprints(self, jid): - q = "SELECT public_key FROM identities WHERE recipient_id = ? AND trust = ?" - c = self.dbConn.cursor() - - result = [] - c.execute(q, (jid, TRUSTED)) - rows = c.fetchall() - for row in rows: - result.append(row[0]) - return result - - def getUndecidedFingerprints(self, jid): - q = "SELECT trust FROM identities WHERE recipient_id = ? AND trust = ?" - c = self.dbConn.cursor() - - result = [] - c.execute(q, (jid, UNDECIDED)) - result = c.fetchall() - - return result - - def getNewFingerprints(self, jid): - q = "SELECT _id FROM identities WHERE shown = 0 AND " \ - "recipient_id = ?" - c = self.dbConn.cursor() - result = [] - for row in c.execute(q, (jid,)): - result.append(row[0]) - return result - - def setShownFingerprints(self, fingerprints): - q = "UPDATE identities SET shown = 1 WHERE _id IN ({})" \ - .format(', '.join(['?'] * len(fingerprints))) - c = self.dbConn.cursor() - c.execute(q, fingerprints) - self.dbConn.commit() - - def setTrust(self, identityKey, trust): - q = "UPDATE identities SET trust = ? WHERE public_key = ?" - c = self.dbConn.cursor() - c.execute(q, (trust, identityKey.getPublicKey().serialize())) - self.dbConn.commit() +# -*- coding: utf-8 -*- +# +# Copyright 2015 Tarek Galal +# +# 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 . +# + +from axolotl.ecc.djbec import DjbECPrivateKey, DjbECPublicKey +from axolotl.identitykey import IdentityKey +from axolotl.identitykeypair import IdentityKeyPair +from axolotl.state.identitykeystore import IdentityKeyStore + +UNDECIDED = 2 +TRUSTED = 1 +UNTRUSTED = 0 + + +class LiteIdentityKeyStore(IdentityKeyStore): + def __init__(self, dbConn): + """ + :type dbConn: Connection + """ + self.dbConn = dbConn + + def getIdentityKeyPair(self): + q = "SELECT public_key, private_key FROM identities " + \ + "WHERE recipient_id = -1" + c = self.dbConn.cursor() + c.execute(q) + result = c.fetchone() + + publicKey, privateKey = result + return IdentityKeyPair( + IdentityKey(DjbECPublicKey(publicKey[1:])), + DjbECPrivateKey(privateKey)) + + def getLocalRegistrationId(self): + q = "SELECT registration_id FROM identities WHERE recipient_id = -1" + c = self.dbConn.cursor() + c.execute(q) + result = c.fetchone() + return result[0] if result else None + + def storeLocalData(self, registrationId, identityKeyPair): + q = "INSERT INTO identities( " + \ + "recipient_id, registration_id, public_key, private_key) " + \ + "VALUES(-1, ?, ?, ?)" + c = self.dbConn.cursor() + c.execute(q, + (registrationId, + identityKeyPair.getPublicKey().getPublicKey().serialize(), + identityKeyPair.getPrivateKey().serialize())) + + self.dbConn.commit() + + def saveIdentity(self, recipientId, identityKey): + q = "INSERT INTO identities (recipient_id, public_key, trust) " \ + "VALUES(?, ?, ?)" + c = self.dbConn.cursor() + + if not self.getIdentity(recipientId, identityKey): + c.execute(q, (recipientId, + identityKey.getPublicKey().serialize(), + UNDECIDED)) + self.dbConn.commit() + + def getIdentity(self, recipientId, identityKey): + q = "SELECT * FROM identities WHERE recipient_id = ? " \ + "AND public_key = ?" + c = self.dbConn.cursor() + + c.execute(q, (recipientId, identityKey.getPublicKey().serialize())) + result = c.fetchone() + + 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 = ?" + c = self.dbConn.cursor() + + c.execute(q, (recipientId, identityKey.getPublicKey().serialize())) + result = c.fetchone() + + states = [UNTRUSTED, TRUSTED, UNDECIDED] + + if result and result[0] in states: + return result[0] + else: + return True + + def getAllFingerprints(self): + q = "SELECT _id, recipient_id, public_key, trust FROM identities " \ + "WHERE recipient_id != -1 ORDER BY recipient_id ASC" + c = self.dbConn.cursor() + + result = [] + for row in c.execute(q): + result.append((row[0], row[1], row[2], row[3])) + return result + + def getFingerprints(self, jid): + q = "SELECT _id, recipient_id, public_key, trust FROM identities " \ + "WHERE recipient_id =? ORDER BY trust ASC" + c = self.dbConn.cursor() + + result = [] + c.execute(q, (jid,)) + rows = c.fetchall() + for row in rows: + result.append((row[0], row[1], row[2], row[3])) + return result + + def getTrustedFingerprints(self, jid): + q = "SELECT public_key FROM identities WHERE recipient_id = ? AND trust = ?" + c = self.dbConn.cursor() + + result = [] + c.execute(q, (jid, TRUSTED)) + rows = c.fetchall() + for row in rows: + result.append(row[0]) + return result + + def getUndecidedFingerprints(self, jid): + q = "SELECT trust FROM identities WHERE recipient_id = ? AND trust = ?" + c = self.dbConn.cursor() + + result = [] + c.execute(q, (jid, UNDECIDED)) + result = c.fetchall() + + return result + + def getNewFingerprints(self, jid): + q = "SELECT _id FROM identities WHERE shown = 0 AND " \ + "recipient_id = ?" + c = self.dbConn.cursor() + result = [] + for row in c.execute(q, (jid,)): + result.append(row[0]) + return result + + def setShownFingerprints(self, fingerprints): + q = "UPDATE identities SET shown = 1 WHERE _id IN ({})" \ + .format(', '.join(['?'] * len(fingerprints))) + c = self.dbConn.cursor() + c.execute(q, fingerprints) + self.dbConn.commit() + + def setTrust(self, identityKey, trust): + q = "UPDATE identities SET trust = ? WHERE public_key = ?" + c = self.dbConn.cursor() + c.execute(q, (trust, identityKey.getPublicKey().serialize())) + self.dbConn.commit() diff --git a/omemo/omemo/liteprekeystore.py b/omemo/backend/liteprekeystore.py similarity index 97% rename from omemo/omemo/liteprekeystore.py rename to omemo/backend/liteprekeystore.py index 78ffc7a..fb671aa 100644 --- a/omemo/omemo/liteprekeystore.py +++ b/omemo/backend/liteprekeystore.py @@ -1,87 +1,87 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Tarek Galal -# -# 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 . -# - -from axolotl.state.prekeyrecord import PreKeyRecord -from axolotl.state.prekeystore import PreKeyStore -from axolotl.util.keyhelper import KeyHelper - - -class LitePreKeyStore(PreKeyStore): - def __init__(self, dbConn): - """ - :type dbConn: Connection - """ - self.dbConn = dbConn - - def loadPreKey(self, preKeyId): - q = "SELECT record FROM prekeys WHERE prekey_id = ?" - - cursor = self.dbConn.cursor() - cursor.execute(q, (preKeyId, )) - - result = cursor.fetchone() - if not result: - raise Exception("No such prekeyRecord!") - - return PreKeyRecord(serialized=result[0]) - - def loadPendingPreKeys(self): - q = "SELECT record FROM prekeys" - cursor = self.dbConn.cursor() - cursor.execute(q) - result = cursor.fetchall() - - return [PreKeyRecord(serialized=r[0]) for r in result] - - def storePreKey(self, preKeyId, preKeyRecord): - q = "INSERT INTO prekeys (prekey_id, record) VALUES(?,?)" - cursor = self.dbConn.cursor() - cursor.execute(q, (preKeyId, preKeyRecord.serialize())) - self.dbConn.commit() - - def containsPreKey(self, preKeyId): - q = "SELECT record FROM prekeys WHERE prekey_id = ?" - cursor = self.dbConn.cursor() - cursor.execute(q, (preKeyId, )) - return cursor.fetchone() is not None - - def removePreKey(self, preKeyId): - q = "DELETE FROM prekeys WHERE prekey_id = ?" - cursor = self.dbConn.cursor() - cursor.execute(q, (preKeyId, )) - self.dbConn.commit() - - def getCurrentPreKeyId(self): - q = "SELECT MAX(prekey_id) FROM prekeys" - cursor = self.dbConn.cursor() - cursor.execute(q) - return cursor.fetchone()[0] - - def getPreKeyCount(self): - q = "SELECT COUNT(prekey_id) FROM prekeys" - cursor = self.dbConn.cursor() - cursor.execute(q) - return cursor.fetchone()[0] - - def generateNewPreKeys(self, count): - startId = self.getCurrentPreKeyId() + 1 - preKeys = KeyHelper.generatePreKeys(startId, count) - - for preKey in preKeys: - self.storePreKey(preKey.getId(), preKey) +# -*- coding: utf-8 -*- +# +# Copyright 2015 Tarek Galal +# +# 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 . +# + +from axolotl.state.prekeyrecord import PreKeyRecord +from axolotl.state.prekeystore import PreKeyStore +from axolotl.util.keyhelper import KeyHelper + + +class LitePreKeyStore(PreKeyStore): + def __init__(self, dbConn): + """ + :type dbConn: Connection + """ + self.dbConn = dbConn + + def loadPreKey(self, preKeyId): + q = "SELECT record FROM prekeys WHERE prekey_id = ?" + + cursor = self.dbConn.cursor() + cursor.execute(q, (preKeyId, )) + + result = cursor.fetchone() + if not result: + raise Exception("No such prekeyRecord!") + + return PreKeyRecord(serialized=result[0]) + + def loadPendingPreKeys(self): + q = "SELECT record FROM prekeys" + cursor = self.dbConn.cursor() + cursor.execute(q) + result = cursor.fetchall() + + return [PreKeyRecord(serialized=r[0]) for r in result] + + def storePreKey(self, preKeyId, preKeyRecord): + q = "INSERT INTO prekeys (prekey_id, record) VALUES(?,?)" + cursor = self.dbConn.cursor() + cursor.execute(q, (preKeyId, preKeyRecord.serialize())) + self.dbConn.commit() + + def containsPreKey(self, preKeyId): + q = "SELECT record FROM prekeys WHERE prekey_id = ?" + cursor = self.dbConn.cursor() + cursor.execute(q, (preKeyId, )) + return cursor.fetchone() is not None + + def removePreKey(self, preKeyId): + q = "DELETE FROM prekeys WHERE prekey_id = ?" + cursor = self.dbConn.cursor() + cursor.execute(q, (preKeyId, )) + self.dbConn.commit() + + def getCurrentPreKeyId(self): + q = "SELECT MAX(prekey_id) FROM prekeys" + cursor = self.dbConn.cursor() + cursor.execute(q) + return cursor.fetchone()[0] + + def getPreKeyCount(self): + q = "SELECT COUNT(prekey_id) FROM prekeys" + cursor = self.dbConn.cursor() + cursor.execute(q) + return cursor.fetchone()[0] + + def generateNewPreKeys(self, count): + startId = self.getCurrentPreKeyId() + 1 + preKeys = KeyHelper.generatePreKeys(startId, count) + + for preKey in preKeys: + self.storePreKey(preKey.getId(), preKey) diff --git a/omemo/omemo/litesessionstore.py b/omemo/backend/litesessionstore.py similarity index 97% rename from omemo/omemo/litesessionstore.py rename to omemo/backend/litesessionstore.py index 24c23e9..6bfae99 100644 --- a/omemo/omemo/litesessionstore.py +++ b/omemo/backend/litesessionstore.py @@ -1,143 +1,143 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Tarek Galal -# -# 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 . -# - -from axolotl.state.sessionrecord import SessionRecord -from axolotl.state.sessionstore import SessionStore - - -class LiteSessionStore(SessionStore): - def __init__(self, dbConn): - """ - :type dbConn: Connection - """ - self.dbConn = dbConn - - def loadSession(self, recipientId, deviceId): - q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?" - c = self.dbConn.cursor() - c.execute(q, (recipientId, deviceId)) - result = c.fetchone() - - if result: - return SessionRecord(serialized=result[0]) - else: - return SessionRecord() - - def getSubDeviceSessions(self, recipientId): - q = "SELECT device_id from sessions WHERE recipient_id = ?" - c = self.dbConn.cursor() - c.execute(q, (recipientId, )) - result = c.fetchall() - - 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') if result else None - - def getActiveDeviceTuples(self): - q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1" - c = self.dbConn.cursor() - result = [] - for row in c.execute(q): - result.append((row[0].decode('utf-8'), row[1])) - return result - - def storeSession(self, recipientId, deviceId, sessionRecord): - self.deleteSession(recipientId, deviceId) - - q = "INSERT INTO sessions(recipient_id, device_id, record) VALUES(?,?,?)" - c = self.dbConn.cursor() - c.execute(q, (recipientId, deviceId, sessionRecord.serialize())) - self.dbConn.commit() - - def containsSession(self, recipientId, deviceId): - q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?" - c = self.dbConn.cursor() - c.execute(q, (recipientId, deviceId)) - result = c.fetchone() - - return result is not None - - def deleteSession(self, recipientId, deviceId): - q = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?" - self.dbConn.cursor().execute(q, (recipientId, deviceId)) - self.dbConn.commit() - - def deleteAllSessions(self, recipientId): - q = "DELETE FROM sessions WHERE recipient_id = ?" - 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() - - q = "UPDATE sessions SET active = {} " \ - "WHERE recipient_id = '{}' AND device_id IN ({})" \ - .format(1, jid, ', '.join(['?'] * len(deviceList))) - c.execute(q, deviceList) - - q = "UPDATE sessions SET active = {} " \ - "WHERE recipient_id = '{}' AND device_id NOT IN ({})" \ - .format(0, jid, ', '.join(['?'] * len(deviceList))) - c.execute(q, deviceList) - self.dbConn.commit() - - def getInactiveSessionsKeys(self, recipientId): - q = "SELECT record FROM sessions WHERE active = 0 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 +# -*- coding: utf-8 -*- +# +# Copyright 2015 Tarek Galal +# +# 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 . +# + +from axolotl.state.sessionrecord import SessionRecord +from axolotl.state.sessionstore import SessionStore + + +class LiteSessionStore(SessionStore): + def __init__(self, dbConn): + """ + :type dbConn: Connection + """ + self.dbConn = dbConn + + def loadSession(self, recipientId, deviceId): + q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?" + c = self.dbConn.cursor() + c.execute(q, (recipientId, deviceId)) + result = c.fetchone() + + if result: + return SessionRecord(serialized=result[0]) + else: + return SessionRecord() + + def getSubDeviceSessions(self, recipientId): + q = "SELECT device_id from sessions WHERE recipient_id = ?" + c = self.dbConn.cursor() + c.execute(q, (recipientId, )) + result = c.fetchall() + + 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') if result else None + + def getActiveDeviceTuples(self): + q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1" + c = self.dbConn.cursor() + result = [] + for row in c.execute(q): + result.append((row[0].decode('utf-8'), row[1])) + return result + + def storeSession(self, recipientId, deviceId, sessionRecord): + self.deleteSession(recipientId, deviceId) + + q = "INSERT INTO sessions(recipient_id, device_id, record) VALUES(?,?,?)" + c = self.dbConn.cursor() + c.execute(q, (recipientId, deviceId, sessionRecord.serialize())) + self.dbConn.commit() + + def containsSession(self, recipientId, deviceId): + q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?" + c = self.dbConn.cursor() + c.execute(q, (recipientId, deviceId)) + result = c.fetchone() + + return result is not None + + def deleteSession(self, recipientId, deviceId): + q = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?" + self.dbConn.cursor().execute(q, (recipientId, deviceId)) + self.dbConn.commit() + + def deleteAllSessions(self, recipientId): + q = "DELETE FROM sessions WHERE recipient_id = ?" + 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() + + q = "UPDATE sessions SET active = {} " \ + "WHERE recipient_id = '{}' AND device_id IN ({})" \ + .format(1, jid, ', '.join(['?'] * len(deviceList))) + c.execute(q, deviceList) + + q = "UPDATE sessions SET active = {} " \ + "WHERE recipient_id = '{}' AND device_id NOT IN ({})" \ + .format(0, jid, ', '.join(['?'] * len(deviceList))) + c.execute(q, deviceList) + self.dbConn.commit() + + def getInactiveSessionsKeys(self, recipientId): + q = "SELECT record FROM sessions WHERE active = 0 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 diff --git a/omemo/omemo/litesignedprekeystore.py b/omemo/backend/litesignedprekeystore.py similarity index 97% rename from omemo/omemo/litesignedprekeystore.py rename to omemo/backend/litesignedprekeystore.py index d6e4a90..fc9cccf 100644 --- a/omemo/omemo/litesignedprekeystore.py +++ b/omemo/backend/litesignedprekeystore.py @@ -1,113 +1,113 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Tarek Galal -# -# 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 . -# - -from axolotl.invalidkeyidexception import InvalidKeyIdException -from axolotl.state.signedprekeyrecord import SignedPreKeyRecord -from axolotl.state.signedprekeystore import SignedPreKeyStore -from axolotl.util.medium import Medium - - -class LiteSignedPreKeyStore(SignedPreKeyStore): - def __init__(self, dbConn): - """ - :type dbConn: Connection - """ - self.dbConn = dbConn - - def loadSignedPreKey(self, signedPreKeyId): - q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?" - - cursor = self.dbConn.cursor() - cursor.execute(q, (signedPreKeyId, )) - - result = cursor.fetchone() - if not result: - raise InvalidKeyIdException("No such signedprekeyrecord! %s " % - signedPreKeyId) - - return SignedPreKeyRecord(serialized=result[0]) - - def loadSignedPreKeys(self): - q = "SELECT record FROM signed_prekeys" - - cursor = self.dbConn.cursor() - cursor.execute(q, ) - result = cursor.fetchall() - results = [] - for row in result: - results.append(SignedPreKeyRecord(serialized=row[0])) - - return results - - def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord): - q = "INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)" - cursor = self.dbConn.cursor() - cursor.execute(q, (signedPreKeyId, signedPreKeyRecord.serialize())) - self.dbConn.commit() - - def containsSignedPreKey(self, signedPreKeyId): - q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?" - cursor = self.dbConn.cursor() - cursor.execute(q, (signedPreKeyId, )) - return cursor.fetchone() is not None - - def removeSignedPreKey(self, signedPreKeyId): - q = "DELETE FROM signed_prekeys WHERE prekey_id = ?" - cursor = self.dbConn.cursor() - cursor.execute(q, (signedPreKeyId, )) - self.dbConn.commit() - - def getNextSignedPreKeyId(self): - result = self.getCurrentSignedPreKeyId() - if not result: - return 1 # StartId if no SignedPreKeys exist - else: - return (result % (Medium.MAX_VALUE - 1)) + 1 - - def getCurrentSignedPreKeyId(self): - q = "SELECT MAX(prekey_id) FROM signed_prekeys" - - cursor = self.dbConn.cursor() - cursor.execute(q) - result = cursor.fetchone() - if not result: - return None - else: - return result[0] - - def getSignedPreKeyTimestamp(self, signedPreKeyId): - q = "SELECT strftime('%s', timestamp) FROM " \ - "signed_prekeys WHERE prekey_id = ?" - - cursor = self.dbConn.cursor() - cursor.execute(q, (signedPreKeyId, )) - - result = cursor.fetchone() - if not result: - raise InvalidKeyIdException("No such signedprekeyrecord! %s " % - signedPreKeyId) - - return result[0] - - def removeOldSignedPreKeys(self, timestamp): - q = "DELETE FROM signed_prekeys " \ - "WHERE timestamp < datetime(?, 'unixepoch')" - cursor = self.dbConn.cursor() - cursor.execute(q, (timestamp, )) - self.dbConn.commit() +# -*- coding: utf-8 -*- +# +# Copyright 2015 Tarek Galal +# +# 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 . +# + +from axolotl.invalidkeyidexception import InvalidKeyIdException +from axolotl.state.signedprekeyrecord import SignedPreKeyRecord +from axolotl.state.signedprekeystore import SignedPreKeyStore +from axolotl.util.medium import Medium + + +class LiteSignedPreKeyStore(SignedPreKeyStore): + def __init__(self, dbConn): + """ + :type dbConn: Connection + """ + self.dbConn = dbConn + + def loadSignedPreKey(self, signedPreKeyId): + q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?" + + cursor = self.dbConn.cursor() + cursor.execute(q, (signedPreKeyId, )) + + result = cursor.fetchone() + if not result: + raise InvalidKeyIdException("No such signedprekeyrecord! %s " % + signedPreKeyId) + + return SignedPreKeyRecord(serialized=result[0]) + + def loadSignedPreKeys(self): + q = "SELECT record FROM signed_prekeys" + + cursor = self.dbConn.cursor() + cursor.execute(q, ) + result = cursor.fetchall() + results = [] + for row in result: + results.append(SignedPreKeyRecord(serialized=row[0])) + + return results + + def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord): + q = "INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)" + cursor = self.dbConn.cursor() + cursor.execute(q, (signedPreKeyId, signedPreKeyRecord.serialize())) + self.dbConn.commit() + + def containsSignedPreKey(self, signedPreKeyId): + q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?" + cursor = self.dbConn.cursor() + cursor.execute(q, (signedPreKeyId, )) + return cursor.fetchone() is not None + + def removeSignedPreKey(self, signedPreKeyId): + q = "DELETE FROM signed_prekeys WHERE prekey_id = ?" + cursor = self.dbConn.cursor() + cursor.execute(q, (signedPreKeyId, )) + self.dbConn.commit() + + def getNextSignedPreKeyId(self): + result = self.getCurrentSignedPreKeyId() + if not result: + return 1 # StartId if no SignedPreKeys exist + else: + return (result % (Medium.MAX_VALUE - 1)) + 1 + + def getCurrentSignedPreKeyId(self): + q = "SELECT MAX(prekey_id) FROM signed_prekeys" + + cursor = self.dbConn.cursor() + cursor.execute(q) + result = cursor.fetchone() + if not result: + return None + else: + return result[0] + + def getSignedPreKeyTimestamp(self, signedPreKeyId): + q = "SELECT strftime('%s', timestamp) FROM " \ + "signed_prekeys WHERE prekey_id = ?" + + cursor = self.dbConn.cursor() + cursor.execute(q, (signedPreKeyId, )) + + result = cursor.fetchone() + if not result: + raise InvalidKeyIdException("No such signedprekeyrecord! %s " % + signedPreKeyId) + + return result[0] + + def removeOldSignedPreKeys(self, timestamp): + q = "DELETE FROM signed_prekeys " \ + "WHERE timestamp < datetime(?, 'unixepoch')" + cursor = self.dbConn.cursor() + cursor.execute(q, (timestamp, )) + self.dbConn.commit() diff --git a/omemo/omemo/sql.py b/omemo/backend/sql.py similarity index 97% rename from omemo/omemo/sql.py rename to omemo/backend/sql.py index 3252b27..f23b1cc 100644 --- a/omemo/omemo/sql.py +++ b/omemo/backend/sql.py @@ -1,154 +1,154 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Tarek Galal -# -# 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 . -# -from .db_helpers import user_version - - -class SQLDatabase(): - """ SQL Database """ - - def __init__(self, dbConn): - """ - :type dbConn: Connection - """ - 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, don't 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: - - # Creates - # IdentityKeyStore - # PreKeyStore - # SignedPreKeyStore - # SessionStore - # EncryptionStore - - create_tables = ''' - CREATE TABLE IF NOT EXISTS identities ( - _id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id TEXT, - registration_id INTEGER, public_key BLOB, private_key BLOB, - next_prekey_id INTEGER, timestamp INTEGER, trust INTEGER, - shown INTEGER DEFAULT 0); - - CREATE UNIQUE INDEX IF NOT EXISTS - public_key_index ON identities (public_key, recipient_id); - - CREATE TABLE IF NOT EXISTS prekeys( - _id INTEGER PRIMARY KEY AUTOINCREMENT, - prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN, - record BLOB); - - CREATE TABLE IF NOT EXISTS signed_prekeys ( - _id INTEGER PRIMARY KEY AUTOINCREMENT, - prekey_id INTEGER UNIQUE, - timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB); - - CREATE TABLE IF NOT EXISTS sessions ( - _id INTEGER PRIMARY KEY AUTOINCREMENT, - recipient_id TEXT, device_id INTEGER, - record BLOB, timestamp INTEGER, active INTEGER DEFAULT 1, - UNIQUE(recipient_id, device_id)); - - CREATE TABLE IF NOT EXISTS encryption_state ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - jid TEXT UNIQUE, - encryption INTEGER - ); - ''' - - create_db_sql = """ - BEGIN TRANSACTION; - %s - PRAGMA user_version=5; - END TRANSACTION; - """ % (create_tables) - self.dbConn.executescript(create_db_sql) - - def migrateDb(self): - """ Migrates the DB - """ - - # Find all double entries and delete them - if user_version(self.dbConn) < 2: - delete_dupes = """ DELETE FROM identities WHERE _id not in ( - SELECT MIN(_id) - FROM identities - GROUP BY - recipient_id, public_key - ); - """ - - self.dbConn.executescript(""" BEGIN TRANSACTION; - %s - PRAGMA user_version=2; - END TRANSACTION; - """ % (delete_dupes)) - - if user_version(self.dbConn) < 3: - # Create a UNIQUE INDEX so every public key/recipient_id tuple - # can only be once in the db - add_index = """ CREATE UNIQUE INDEX IF NOT EXISTS - public_key_index - ON identities (public_key, recipient_id); - """ - - self.dbConn.executescript(""" BEGIN TRANSACTION; - %s - PRAGMA user_version=3; - END TRANSACTION; - """ % (add_index)) - - if user_version(self.dbConn) < 4: - # Adds column "active" to the sessions table - add_active = """ ALTER TABLE sessions - ADD COLUMN active INTEGER DEFAULT 1; - """ - - self.dbConn.executescript(""" BEGIN TRANSACTION; - %s - PRAGMA user_version=4; - END TRANSACTION; - """ % (add_active)) - - if user_version(self.dbConn) < 5: - # Adds DEFAULT Timestamp - add_timestamp = """ - DROP TABLE signed_prekeys; - CREATE TABLE IF NOT EXISTS signed_prekeys ( - _id INTEGER PRIMARY KEY AUTOINCREMENT, - prekey_id INTEGER UNIQUE, - timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB); - ALTER TABLE identities ADD COLUMN shown INTEGER DEFAULT 0; - UPDATE identities SET shown = 1; - """ - - self.dbConn.executescript(""" BEGIN TRANSACTION; - %s - PRAGMA user_version=5; - END TRANSACTION; - """ % (add_timestamp)) +# -*- coding: utf-8 -*- +# +# Copyright 2015 Tarek Galal +# +# 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 . +# +from .db_helpers import user_version + + +class SQLDatabase(): + """ SQL Database """ + + def __init__(self, dbConn): + """ + :type dbConn: Connection + """ + 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, don't 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: + + # Creates + # IdentityKeyStore + # PreKeyStore + # SignedPreKeyStore + # SessionStore + # EncryptionStore + + create_tables = ''' + CREATE TABLE IF NOT EXISTS identities ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id TEXT, + registration_id INTEGER, public_key BLOB, private_key BLOB, + next_prekey_id INTEGER, timestamp INTEGER, trust INTEGER, + shown INTEGER DEFAULT 0); + + CREATE UNIQUE INDEX IF NOT EXISTS + public_key_index ON identities (public_key, recipient_id); + + CREATE TABLE IF NOT EXISTS prekeys( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN, + record BLOB); + + CREATE TABLE IF NOT EXISTS signed_prekeys ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + prekey_id INTEGER UNIQUE, + timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB); + + CREATE TABLE IF NOT EXISTS sessions ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + recipient_id TEXT, device_id INTEGER, + record BLOB, timestamp INTEGER, active INTEGER DEFAULT 1, + UNIQUE(recipient_id, device_id)); + + CREATE TABLE IF NOT EXISTS encryption_state ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + jid TEXT UNIQUE, + encryption INTEGER + ); + ''' + + create_db_sql = """ + BEGIN TRANSACTION; + %s + PRAGMA user_version=5; + END TRANSACTION; + """ % (create_tables) + self.dbConn.executescript(create_db_sql) + + def migrateDb(self): + """ Migrates the DB + """ + + # Find all double entries and delete them + if user_version(self.dbConn) < 2: + delete_dupes = """ DELETE FROM identities WHERE _id not in ( + SELECT MIN(_id) + FROM identities + GROUP BY + recipient_id, public_key + ); + """ + + self.dbConn.executescript(""" BEGIN TRANSACTION; + %s + PRAGMA user_version=2; + END TRANSACTION; + """ % (delete_dupes)) + + if user_version(self.dbConn) < 3: + # Create a UNIQUE INDEX so every public key/recipient_id tuple + # can only be once in the db + add_index = """ CREATE UNIQUE INDEX IF NOT EXISTS + public_key_index + ON identities (public_key, recipient_id); + """ + + self.dbConn.executescript(""" BEGIN TRANSACTION; + %s + PRAGMA user_version=3; + END TRANSACTION; + """ % (add_index)) + + if user_version(self.dbConn) < 4: + # Adds column "active" to the sessions table + add_active = """ ALTER TABLE sessions + ADD COLUMN active INTEGER DEFAULT 1; + """ + + self.dbConn.executescript(""" BEGIN TRANSACTION; + %s + PRAGMA user_version=4; + END TRANSACTION; + """ % (add_active)) + + if user_version(self.dbConn) < 5: + # Adds DEFAULT Timestamp + add_timestamp = """ + DROP TABLE signed_prekeys; + CREATE TABLE IF NOT EXISTS signed_prekeys ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + prekey_id INTEGER UNIQUE, + timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB); + ALTER TABLE identities ADD COLUMN shown INTEGER DEFAULT 0; + UPDATE identities SET shown = 1; + """ + + self.dbConn.executescript(""" BEGIN TRANSACTION; + %s + PRAGMA user_version=5; + END TRANSACTION; + """ % (add_timestamp)) diff --git a/omemo/omemo/state.py b/omemo/backend/state.py similarity index 77% rename from omemo/omemo/state.py rename to omemo/backend/state.py index 050db17..165cdbf 100644 --- a/omemo/omemo/state.py +++ b/omemo/backend/state.py @@ -19,16 +19,14 @@ import logging import time -import os -from base64 import b64encode + +from nbxmpp.structs import OMEMOBundle +from nbxmpp.structs import OMEMOMessage from axolotl.ecc.djbec import DjbECPublicKey from axolotl.identitykey import IdentityKey -from axolotl.duplicatemessagexception import DuplicateMessageException -from axolotl.invalidmessageexception import InvalidMessageException -from axolotl.invalidversionexception import InvalidVersionException from axolotl.untrustedidentityexception import UntrustedIdentityException -from axolotl.nosessionexception import NoSessionException + from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage from axolotl.protocol.whispermessage import WhisperMessage from axolotl.sessionbuilder import SessionBuilder @@ -36,13 +34,12 @@ from axolotl.sessioncipher import SessionCipher from axolotl.state.prekeybundle import PreKeyBundle from axolotl.util.keyhelper import KeyHelper -from .aes_gcm import NoValidSessions, decrypt, encrypt +from omemo.backend.aes import aes_decrypt, aes_encrypt from .liteaxolotlstore import (LiteAxolotlStore, DEFAULT_PREKEY_AMOUNT, MIN_PREKEY_AMOUNT, SPK_CYCLE_TIME, SPK_ARCHIVE_TIME) log = logging.getLogger('gajim.plugin_system.omemo') -logAxolotl = logging.getLogger('axolotl') UNTRUSTED = 0 @@ -78,23 +75,24 @@ class OmemoState: str(self.store.preKeyStore.getPreKeyCount()) + ' PreKeys available') - def build_session(self, recipient_id, device_id, bundle_dict): + def build_session(self, recipient_id, device_id, bundle): sessionBuilder = SessionBuilder(self.store, self.store, self.store, self.store, recipient_id, device_id) registration_id = self.store.getLocalRegistrationId() - preKeyPublic = DjbECPublicKey(bundle_dict['preKeyPublic'][1:]) + prekey = bundle.pick_prekey() + preKeyPublic = DjbECPublicKey(prekey['key'][1:]) - signedPreKeyPublic = DjbECPublicKey(bundle_dict['signedPreKeyPublic'][ - 1:]) - identityKey = IdentityKey(DjbECPublicKey(bundle_dict['identityKey'][ - 1:])) + signedPreKeyPublic = DjbECPublicKey(bundle.spk['key'][1:]) + identityKey = IdentityKey(DjbECPublicKey(bundle.ik[1:])) prekey_bundle = PreKeyBundle( - registration_id, device_id, bundle_dict['preKeyId'], preKeyPublic, - bundle_dict['signedPreKeyId'], signedPreKeyPublic, - bundle_dict['signedPreKeySignature'], identityKey) + registration_id, device_id, + prekey['id'], preKeyPublic, + bundle.spk['id'], signedPreKeyPublic, + bundle.spk_signature, + identityKey) sessionBuilder.processPreKeyBundle(prekey_bundle) return self.get_session_cipher(recipient_id, device_id) @@ -153,101 +151,85 @@ class OmemoState: @property def bundle(self): self.checkPreKeyAmount() - prekeys = [ - (k.getId(), b64encode(k.getKeyPair().getPublicKey().serialize())) - for k in self.store.loadPreKeys() - ] + + bundle = {'otpks': []} + for k in self.store.loadPreKeys(): + key = k.getKeyPair().getPublicKey().serialize() + bundle['otpks'].append({'key': key, 'id': k.getId()}) identityKeyPair = self.store.getIdentityKeyPair() + bundle['ik'] = identityKeyPair.getPublicKey().serialize() self.cycleSignedPreKey(identityKeyPair) signedPreKey = self.store.loadSignedPreKey( self.store.getCurrentSignedPreKeyId()) + bundle['spk_signature'] = signedPreKey.getSignature() + bundle['spk'] = {'key': signedPreKey.getKeyPair().getPublicKey().serialize(), + 'id': signedPreKey.getId()} - result = { - 'signedPreKeyId': signedPreKey.getId(), - 'signedPreKeyPublic': - b64encode(signedPreKey.getKeyPair().getPublicKey().serialize()), - 'signedPreKeySignature': b64encode(signedPreKey.getSignature()), - 'identityKey': - b64encode(identityKeyPair.getPublicKey().serialize()), - 'prekeys': prekeys - } - return result + return OMEMOBundle(**bundle) - def decrypt_msg(self, msg_dict): + def decrypt_msg(self, omemo_message, jid): own_id = self.own_device_id - if msg_dict['sid'] == own_id: + if omemo_message.sid == own_id: log.info('Received previously sent message by us') return - if own_id not in msg_dict['keys']: + if own_id not in omemo_message.keys: log.warning('OMEMO message does not contain our device key') return - iv = msg_dict['iv'] - sid = msg_dict['sid'] - sender_jid = msg_dict['sender_jid'] - payload = msg_dict['payload'] + encrypted_key, prekey = omemo_message.keys[own_id] - encrypted_key = msg_dict['keys'][own_id] - - try: - key = self.handlePreKeyWhisperMessage(sender_jid, sid, - encrypted_key) - except (InvalidVersionException, InvalidMessageException): + if prekey: try: - key = self.handleWhisperMessage(sender_jid, sid, encrypted_key) - except (NoSessionException, InvalidMessageException) as e: - log.warning(e) - log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' + - str(sid)) - return - except (DuplicateMessageException) as e: - log.warning('Duplicate message found ' + str(e.args)) + key = self.handlePreKeyWhisperMessage( + jid, omemo_message.sid, encrypted_key) + except Exception as error: + log.warning(error) return - except (DuplicateMessageException) as e: - log.warning('Duplicate message found ' + str(e.args)) - return + else: + try: + key = self.handleWhisperMessage( + jid, omemo_message.sid, encrypted_key) + except Exception as error: + log.warning(error) + return - if payload is None: + if omemo_message.payload is None: result = None log.debug("Decrypted Key Exchange Message") else: - result = decrypt(key, iv, payload) - log.debug("Decrypted Message => " + result) + result = aes_decrypt(key, omemo_message.iv, omemo_message.payload) + log.debug("Decrypted Message => %s", result) return result def create_msg(self, from_jid, jid, plaintext): - key = os.urandom(16) - iv = os.urandom(16) encrypted_keys = {} devices_list = self.device_list_for(jid) - if len(devices_list) == 0: + if not devices_list: log.error('No known devices') return - payload, tag = encrypt(key, iv, plaintext) - - key += tag + result = aes_encrypt(plaintext) # Encrypt the message key with for each of receivers devices for device in devices_list: try: if self.isTrusted(jid, device) == TRUSTED: cipher = self.get_session_cipher(jid, device) - cipher_key = cipher.encrypt(key) + cipher_key = cipher.encrypt(result.key) prekey = isinstance(cipher_key, PreKeyWhisperMessage) encrypted_keys[device] = (cipher_key.serialize(), prekey) else: - log.debug('Skipped Device because Trust is: ' + - str(self.isTrusted(jid, device))) - except: - log.warning('Failed to find key for device ' + str(device)) + log.debug('Skipped Device because Trust is: %s', + self.isTrusted(jid, device)) + except Exception: + log.warning('Failed to find key for device: %s', device) - if len(encrypted_keys) == 0: + if not encrypted_keys: log.error('Encrypted keys empty') raise NoValidSessions('Encrypted keys empty') @@ -257,36 +239,29 @@ class OmemoState: try: if self.isTrusted(from_jid, device) == TRUSTED: cipher = self.get_session_cipher(from_jid, device) - cipher_key = cipher.encrypt(key) + cipher_key = cipher.encrypt(result.key) prekey = isinstance(cipher_key, PreKeyWhisperMessage) encrypted_keys[device] = (cipher_key.serialize(), prekey) else: - log.debug('Skipped own Device because Trust is: ' + - str(self.isTrusted(from_jid, device))) - except: - log.warning('Failed to find key for device ' + str(device)) - - result = {'sid': self.own_device_id, - 'keys': encrypted_keys, - 'jid': jid, - 'iv': iv, - 'payload': payload} + log.debug('Skipped own Device because Trust is: %s', + self.isTrusted(from_jid, device)) + except Exception: + log.warning('Failed to find key for device: %s', device) log.debug('Finished encrypting message') - return result + return OMEMOMessage(sid=self.own_device_id, + keys=encrypted_keys, + iv=result.iv, + payload=result.payload) def create_gc_msg(self, from_jid, jid, plaintext): - key = os.urandom(16) - iv = os.urandom(16) encrypted_keys = {} room = jid encrypted_jids = [] devices_list = self.device_list_for(jid, True) - payload, tag = encrypt(key, iv, plaintext) - - key += tag + result = aes_encrypt(plaintext, append_tag=True) for tup in devices_list: self.get_session_cipher(tup[0], tup[1]) @@ -303,16 +278,15 @@ class OmemoState: for rid, cipher in self.session_ciphers[jid_to].items(): try: if self.isTrusted(jid_to, rid) == TRUSTED: - cipher_key = cipher.encrypt(key) + cipher_key = cipher.encrypt(result.key) prekey = isinstance(cipher_key, PreKeyWhisperMessage) encrypted_keys[rid] = (cipher_key.serialize(), prekey) else: - log.debug('Skipped Device because Trust is: ' + - str(self.isTrusted(jid_to, rid))) - except: + log.debug('Skipped Device because Trust is: %s', + self.isTrusted(jid_to, rid)) + except Exception: log.exception('ERROR:') - log.warning('Failed to find key for device ' + - str(rid)) + log.warning('Failed to find key for device %s', rid) encrypted_jids.append(jid_to) my_other_devices = set(self.own_devices) - set({self.own_device_id}) @@ -321,28 +295,25 @@ class OmemoState: try: cipher = self.get_session_cipher(from_jid, dev) if self.isTrusted(from_jid, dev) == TRUSTED: - cipher_key = cipher.encrypt(key) + cipher_key = cipher.encrypt(result.key) prekey = isinstance(cipher_key, PreKeyWhisperMessage) encrypted_keys[dev] = (cipher_key.serialize(), prekey) else: - log.debug('Skipped own Device because Trust is: ' + - str(self.isTrusted(from_jid, dev))) - except: + log.debug('Skipped own Device because Trust is: %s', + self.isTrusted(from_jid, dev)) + except Exception: log.exception('ERROR:') - log.warning('Failed to find key for device ' + str(dev)) + log.warning('Failed to find key for device: %s', dev) if not encrypted_keys: log.error('Encrypted keys empty') raise NoValidSessions('Encrypted keys empty') - result = {'sid': self.own_device_id, - 'keys': encrypted_keys, - 'jid': jid, - 'iv': iv, - 'payload': payload} - log.debug('Finished encrypting message') - return result + return OMEMOMessage(sid=self.own_device_id, + keys=encrypted_keys, + iv=result.iv, + payload=result.payload) def device_list_for(self, jid, gc=False): """ Return a list of known device ids for the specified jid. @@ -439,7 +410,7 @@ class OmemoState: key = sessionCipher.decryptPkmsg(preKeyWhisperMessage) # Publish new bundle after PreKey has been used # for building a new Session - self.xmpp_con.publish_bundle() + self.xmpp_con.set_bundle() self.add_device(recipient_id, device_id) return key except UntrustedIdentityException as e: @@ -495,3 +466,7 @@ class OmemoState: # Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME timestamp = now - SPK_ARCHIVE_TIME self.store.removeOldSignedPreKeys(timestamp) + + +class NoValidSessions(Exception): + pass diff --git a/omemo/file_crypto.py b/omemo/file_crypto.py index 05e5d09..546af35 100644 --- a/omemo/file_crypto.py +++ b/omemo/file_crypto.py @@ -1,21 +1,18 @@ -# -*- coding: utf-8 -*- +# Copyright (C) 2019 Philipp Hörist # -# Copyright 2017 Philipp Hörist +# This file is part of OMEMO Gajim Plugin. # -# This file is part of Gajim-OMEMO plugin. +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. # -# 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 . +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. # +# You should have received a copy of the GNU General Public License +# along with OMEMO Gajim Plugin. If not, see . import os import hashlib @@ -32,29 +29,23 @@ from urllib.parse import urlparse, urldefrag from io import BufferedWriter, FileIO, BytesIO from gi.repository import GLib + from gajim.common import app from gajim.common import configpaths from gajim.plugins.plugins_i18n import _ from gajim.gtk.dialogs import ErrorDialog, YesNoDialog + from omemo.gtk.progress import ProgressWindow +from omemo.backend.aes import aes_decrypt_file + if os.name == 'nt': import certifi log = logging.getLogger('gajim.plugin_system.omemo.filedecryption') -ERROR = False -try: - from cryptography.hazmat.primitives.ciphers import Cipher - from cryptography.hazmat.primitives.ciphers import algorithms - from cryptography.hazmat.primitives.ciphers.modes import GCM - from cryptography.hazmat.backends import default_backend - from omemo.omemo.aes_gcm_native import aes_encrypt -except ImportError: - log.exception('ImportError') - ERROR = True - DIRECTORY = os.path.join(configpaths.get('MY_DATA'), 'downloads') +ERROR = False try: if not os.path.exists(DIRECTORY): os.makedirs(DIRECTORY) @@ -63,15 +54,6 @@ except Exception: log.exception('Error') -def encrypt_file(data): - key = os.urandom(32) - iv = os.urandom(16) - - payload, tag = aes_encrypt(key, iv, data) - encrypted_data = payload + tag - return (encrypted_data, key, iv) - - class File: def __init__(self, url, account): self.account = account @@ -105,7 +87,7 @@ class FileDecryption: if not self.is_encrypted(file): log.info('Url not encrypted: %s', url) return - + print('ADASD') self.create_paths(file) if os.path.exists(file.filepath): @@ -181,7 +163,10 @@ class Download: return GLib.idle_add(self.progressbar.set_text, _('Decrypting...')) - decrypted_data = self.aes_decrypt(data) + + decrypted_data = aes_decrypt_file(self.file.key, + self.file.iv, + data.getvalue()) GLib.idle_add( self.progressbar.set_text, _('Writing file to harddisk...')) @@ -203,11 +188,11 @@ class Download: log.warning('CERT Verification disabled') get_request = urlopen(self.file.url, timeout=30, context=context) else: + cafile = None if os.name == 'nt': - get_request = urlopen( - self.file.url, cafile=certifi.where(), timeout=30) - else: - get_request = urlopen(self.file.url, timeout=30) + cafile = certifi.where() + context = ssl.create_default_context(cafile=cafile) + get_request = urlopen(self.file.url, timeout=30, context=context) size = get_request.info()['Content-Length'] if not size: @@ -241,17 +226,6 @@ class Download: stream.close() return str(errormsg) - def aes_decrypt(self, payload): - # Use AES128 GCM with the given key and iv to decrypt the payload. - payload = payload.getvalue() - data = payload[:-16] - tag = payload[-16:] - decryptor = Cipher( - algorithms.AES(self.file.key), - GCM(self.file.iv, tag=tag), - backend=default_backend()).decryptor() - return decryptor.update(data) + decryptor.finalize() - def write_file(self, data): log.info('Writing data to %s', self.file.filepath) try: diff --git a/omemo/gtk/config.py b/omemo/gtk/config.py index f116eb6..0122744 100644 --- a/omemo/gtk/config.py +++ b/omemo/gtk/config.py @@ -1,22 +1,20 @@ -''' -Copyright 2015 Bahtiar `kalkin-` Gadimov -Copyright 2015 Daniel Gultsch -Copyright 2016 Philipp Hörist - -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 . -''' +# Copyright (C) 2019 Philipp Hörist +# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov +# Copyright (C) 2015 Daniel Gultsch +# +# This file is part of OMEMO Gajim Plugin. +# +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OMEMO Gajim Plugin. If not, see . import binascii import logging @@ -171,7 +169,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog): def cleardevice_button_clicked_cb(self, button, *args): active = self._ui.get_object('account_combobox').get_active() account = self.account_store[active][0] - app.connections[account].get_module('OMEMO').publish_own_devices_list(new=True) + app.connections[account].get_module('OMEMO').set_devicelist(new=True) self.update_context_list() def refresh_button_clicked_cb(self, button, *args): diff --git a/omemo/gtk/key.py b/omemo/gtk/key.py index 66621cc..b6f02d1 100644 --- a/omemo/gtk/key.py +++ b/omemo/gtk/key.py @@ -1,18 +1,18 @@ -# Copyright (C) 2018 Philipp Hörist +# Copyright (C) 2019 Philipp Hörist # -# This file is part of Gajim. +# This file is part of OMEMO Gajim Plugin. # -# Gajim is free software; you can redistribute it and/or modify +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # -# Gajim is distributed in the hope that it will be useful, +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Gajim. If not, see . +# along with OMEMO Gajim Plugin. If not, see . import logging import binascii diff --git a/omemo/gtk/progress.py b/omemo/gtk/progress.py index 1d565fc..3cf5086 100644 --- a/omemo/gtk/progress.py +++ b/omemo/gtk/progress.py @@ -1,18 +1,19 @@ -# Copyright (C) 2018 Philipp Hörist +# Copyright (C) 2019 Philipp Hörist # -# This file is part of Gajim. +# This file is part of OMEMO Gajim Plugin. # -# Gajim is free software; you can redistribute it and/or modify +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # -# Gajim is distributed in the hope that it will be useful, +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Gajim. If not, see . +# along with OMEMO Gajim Plugin. If not, see . + from gajim.plugins.helpers import get_builder diff --git a/omemo/gtk/util.py b/omemo/gtk/util.py index f6a42e5..e32168b 100644 --- a/omemo/gtk/util.py +++ b/omemo/gtk/util.py @@ -1,16 +1,18 @@ -# This file is part of Gajim-OMEMO. +# Copyright (C) 2019 Philipp Hörist # -# Gajim-OMEMO is free software; you can redistribute it and/or modify +# This file is part of OMEMO Gajim Plugin. +# +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # -# Gajim-OMEMO is distributed in the hope that it will be useful, +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Gajim-OMEMO. If not, see . +# along with OMEMO Gajim Plugin. If not, see . from collections import namedtuple diff --git a/omemo/modules/omemo.py b/omemo/modules/omemo.py index 471b6a7..9cf6e37 100644 --- a/omemo/modules/omemo.py +++ b/omemo/modules/omemo.py @@ -1,18 +1,18 @@ -# Copyright (C) 2018 Philipp Hörist +# Copyright (C) 2019 Philipp Hörist # -# This file is part of OMEMO. +# This file is part of OMEMO Gajim Plugin. # -# OMEMO is free software; you can redistribute it and/or modify +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # -# OMEMO is distributed in the hope that it will be useful, +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with OMEMO. If not, see . +# along with OMEMO Gajim Plugin. If not, see . # XEP-0384: OMEMO Encryption @@ -22,41 +22,40 @@ import logging import sqlite3 import nbxmpp -from nbxmpp.simplexml import Node -from nbxmpp.protocol import JID +from nbxmpp.protocol import NodeProcessed +from nbxmpp.util import is_error_result from nbxmpp.const import StatusCode from nbxmpp.const import PresenceType from nbxmpp.structs import StanzaHandler +from nbxmpp.modules.omemo import create_omemo_message from gajim.common import app from gajim.common import ged from gajim.common import helpers from gajim.common import configpaths from gajim.common.nec import NetworkEvent +from gajim.common.const import EncryptionData +from gajim.common.modules.base import BaseModule +from gajim.common.modules.util import event_node -from gajim.plugins.plugins_i18n import _ +from omemo.backend.state import OmemoState +from omemo.modules.util import prepare_stanza -from omemo.xmpp import ( - NS_NOTIFY, NS_OMEMO, NS_EME, NS_HINTS, NS_BUNDLES, - BundleInformationQuery, DevicelistQuery, make_bundle, - OmemoMessage, successful, unpack_device_bundle, - unpack_device_list_update, unpack_encrypted, NS_DEVICE_LIST) -from omemo.omemo.state import OmemoState - -ALLOWED_TAGS = [('request', nbxmpp.NS_RECEIPTS), - ('active', nbxmpp.NS_CHATSTATES), - ('gone', nbxmpp.NS_CHATSTATES), - ('inactive', nbxmpp.NS_CHATSTATES), - ('paused', nbxmpp.NS_CHATSTATES), - ('composing', nbxmpp.NS_CHATSTATES), - ('no-store', nbxmpp.NS_MSG_HINTS), - ('store', nbxmpp.NS_MSG_HINTS), - ('no-copy', nbxmpp.NS_MSG_HINTS), - ('no-permanent-store', nbxmpp.NS_MSG_HINTS), - ('replace', nbxmpp.NS_CORRECT), - ('thread', None), - ('origin-id', nbxmpp.NS_SID), - ] +ALLOWED_TAGS = [ + ('request', nbxmpp.NS_RECEIPTS), + ('active', nbxmpp.NS_CHATSTATES), + ('gone', nbxmpp.NS_CHATSTATES), + ('inactive', nbxmpp.NS_CHATSTATES), + ('paused', nbxmpp.NS_CHATSTATES), + ('composing', nbxmpp.NS_CHATSTATES), + ('no-store', nbxmpp.NS_MSG_HINTS), + ('store', nbxmpp.NS_MSG_HINTS), + ('no-copy', nbxmpp.NS_MSG_HINTS), + ('no-permanent-store', nbxmpp.NS_MSG_HINTS), + ('replace', nbxmpp.NS_CORRECT), + ('thread', None), + ('origin-id', nbxmpp.NS_SID), +] log = logging.getLogger('gajim.plugin_system.omemo') @@ -67,17 +66,32 @@ name = 'OMEMO' zeroconf = False -class OMEMO: +class OMEMO(BaseModule): + + _nbxmpp_extends = 'OMEMO' + _nbxmpp_methods = [ + 'set_devicelist', + 'request_devicelist', + 'set_bundle', + 'request_bundle', + ] + def __init__(self, con): - self._con = con - self._account = con.name + BaseModule.__init__(self, con) self.handlers = [ + StanzaHandler(name='message', + callback=self._message_received, + ns=nbxmpp.NS_OMEMO_TEMP, + priority=9), StanzaHandler(name='presence', callback=self._on_muc_user_presence, ns=nbxmpp.NS_MUC_USER, priority=48), ] + + self._register_pubsub_handler(self._devicelist_notification_received) + self.available = True # self.plugin = plugin self.own_jid = self._con.get_own_jid().getStripped() @@ -94,31 +108,12 @@ class OMEMO: app.ged.register_event_handler('muc-config-changed', ged.GUI2, self._on_config_changed) - def send_with_callback(self, stanza, callback, data=None): - if data is None: - self._con.connection.SendAndCallForResponse(stanza, callback) - else: - self._con.connection.SendAndCallForResponse( - stanza, callback, data) - def get_own_jid(self, stripped=False): if stripped: return self._con.get_own_jid().getStripped() return self._con.get_own_jid() def __get_omemo(self): - """ 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 - """ data_dir = configpaths.get('MY_DATA') db_path = os.path.join(data_dir, 'omemo_' + self.own_jid + '.db') conn = sqlite3.connect(db_path, check_same_thread=False) @@ -126,19 +121,13 @@ class OMEMO: return OmemoState(self.own_jid, conn, self._account, self) def signed_in(self, event): - """ Method called on SignIn - - Parameters - ---------- - event : SignedInEvent - """ if event.conn.name != self._account: return log.info('%s => Announce Support after Sign In', self._account) self.query_for_bundles = [] - self.publish_bundle() - self.query_devicelist() + self.set_bundle() + self.request_devicelist() def activate(self): """ Method called when the Plugin is activated in the PluginManager @@ -151,8 +140,8 @@ class OMEMO: log.info('%s => Announce Support after Plugin Activation', self._account) self.query_for_bundles = [] - self.publish_bundle() - self.query_devicelist() + self.set_bundle() + self.request_devicelist() def deactivate(self): """ Method called when the Plugin is deactivated in the PluginManager @@ -161,243 +150,73 @@ class OMEMO: @staticmethod def update_caps(account): - if NS_NOTIFY not in app.gajim_optional_features[account]: - app.gajim_optional_features[account].append(NS_NOTIFY) + node = '%s+notify' % nbxmpp.NS_OMEMO_TEMP_DL + if node not in app.gajim_optional_features[account]: + app.gajim_optional_features[account].append(node) - def message_received(self, conn, obj, callback): - if obj.encrypted: - return - if obj.name == 'message-received': - self._message_received(obj) - elif obj.name == 'mam-message-received': - self._mam_message_received(obj) - elif obj.name == 'mam-gc-message-received': - self._mam_gc_message_received(obj) - if obj.encrypted == 'OMEMO': - callback(obj) - - def _mam_gc_message_received(self, event): - """ Handles an incoming GC MAM message - - Payload is decrypted and the plaintext is written into the - event object. Afterwards the event is passed on further to Gajim. - - Parameters - ---------- - event : MamGcMessageReceivedEvent - - Returns - ------- - Return means that the Event is passed on to Gajim - """ - if event.conn.name != self._account: + def _message_received(self, _con, stanza, properties): + if not properties.is_omemo: return - # Compatibility for Gajim 1.0.3 - if hasattr(event, 'message'): - message = event.message + if properties.is_mam_message: + if properties.omemo.sid == self.omemo.own_device_id: + log.info('%s => Skip message because it was sent by us', + self._account) + raise NodeProcessed + log.info('%s => Message received, archive: %s', + self._account, properties.mam.archive) else: - message = event.msg_ + log.info('%s => Message received', self._account) - omemo = message.getTag('encrypted', namespace=NS_OMEMO) - if omemo is None: - return + from_jid = properties.jid.getBare() - if event.real_jid is None: - log.error('%s => Received Groupchat Message without real jid', - self._account) - return - - log.info('%s => Groupchat Message received', self._account) - - msg_dict = unpack_encrypted(omemo) - msg_dict['sender_jid'] = JID(event.real_jid).getStripped() - - plaintext = self.omemo.decrypt_msg(msg_dict) - - if not plaintext: - event.encrypted = 'drop' - return - - self.print_msg_to_log(message) - - event.msgtxt = plaintext - event.encrypted = ENCRYPTION_NAME - self.add_additional_data(event.additional_data) - - def _mam_message_received(self, event): - """ 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 - ---------- - event : MamMessageReceivedEvent - - Returns - ------- - Return means that the Event is passed on to Gajim - """ - if event.conn.name != self._account: - return - - # Compatibility for Gajim 1.0.3 - if hasattr(event, 'message'): - message = event.message - else: - message = event.msg_ - - omemo_encrypted_tag = message.getTag('encrypted', namespace=NS_OMEMO) - if omemo_encrypted_tag: - log.debug('%s => OMEMO MAM msg received', self._account) - - msg_dict = unpack_encrypted(omemo_encrypted_tag) - if msg_dict is None: - log.error('Invalid omemo message received:\n%s', message) - event.encrypted = 'drop' - return - - msg_dict['sender_jid'] = message.getFrom().getStripped() - - plaintext = self.omemo.decrypt_msg(msg_dict) - - if not plaintext: - event.encrypted = 'drop' - return - - self.print_msg_to_log(message) - - event.msgtxt = plaintext - event.encrypted = ENCRYPTION_NAME - self.add_additional_data(event.additional_data) - return - - 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.conn.name != self._account: - return - if msg.stanza.getTag('encrypted', namespace=NS_OMEMO): - log.debug('%s => OMEMO msg received', self._account) - - if msg.forwarded and msg.sent: - from_jid = self.own_jid - log.debug('Sent Carbon') + if properties.from_muc: + if properties.is_mam_message: + log.info('%s => MUC MAM Message received', self._account) + if properties.muc_user.jid is None: + log.info('No real jid found, ignore message') + return + from_jid = properties.muc_user.jid.getBare() 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=nbxmpp.NS_ADDRESS) - if address_tag: # History Message from MUC - from_jid = address_tag.getTag( - 'address', attrs={'type': 'ofrom'}).getAttr('jid') + room_jid = properties.jid.getBare() + resource = properties.jid.getResource() + if properties.muc_ofrom is not None: + # History Message from MUC + from_jid = properties.muc_ofrom.getBare() else: try: - from_jid = self.groupchat[msg.jid][msg.resource] + from_jid = self.groupchat[room_jid][resource] except KeyError: log.debug('Groupchat: Last resort trying to ' 'find SID in DB') - from_jid = self.omemo.store. \ - getJidFromDevice(msg_dict['sid']) + from_jid = self.omemo.store.getJidFromDevice( + properties.omemo.sid) if not from_jid: log.error("%s => Can't decrypt GroupChat Message " - "from %s", self._account, msg.resource) - msg.encrypted = 'drop' + "from %s", self._account, resource) return - self.groupchat[msg.jid][msg.resource] = from_jid + self.groupchat[room_jid][resource] = from_jid log.debug('GroupChat Message from: %s', from_jid) - plaintext = '' - if msg_dict['sid'] == self.omemo.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("%s => Can't decrypt own GroupChat Message", - self._account) - msg.encrypted = 'drop' - return + plaintext = '' + if properties.omemo.sid == self.omemo.own_device_id: + if properties.omemo.payload in self.gc_message: + plaintext = self.gc_message[properties.omemo.payload] + del self.gc_message[properties.omemo.payload] else: - msg_dict['sender_jid'] = app. \ - get_jid_without_resource(from_jid) - plaintext = self.omemo.decrypt_msg(msg_dict) - - if not plaintext: - msg.encrypted = 'drop' + log.error("%s => Can't decrypt own GroupChat Message", + self._account) return + else: + plaintext = self.omemo.decrypt_msg(properties.omemo, from_jid) - msg.msgtxt = plaintext - # Gajim bug: there must be a body or the message - # gets dropped from history - msg.stanza.setBody(plaintext) - msg.encrypted = ENCRYPTION_NAME - self.add_additional_data(msg.additional_data) - - def room_memberlist_received(self, stanza): - if not nbxmpp.isResultNode(stanza): - log.error('Room %s Memberlist received: %s', - stanza.getFrom(), stanza.getError()) + if not plaintext: return - room_jid = stanza.getFrom().getStripped() - log.info('Room %s Memberlist received', room_jid) - if room_jid not in self.groupchat: - self.groupchat[room_jid] = {} - - def jid_known(jid): - for nick in self.groupchat[room_jid]: - if self.groupchat[room_jid][nick] == jid: - return True - return False - - items = stanza.getTag( - 'query', namespace=nbxmpp.NS_MUC_ADMIN).getTags('item') - - for item in items: - if not item.has_attr('jid'): - continue - try: - jid = helpers.parse_jid(item.getAttr('jid')) - except helpers.InvalidFormat: - log.warning( - 'Invalid JID: %s, ignoring it', item.getAttr('jid')) - continue - - if not jid_known(jid): - # Add JID with JID because we have no Nick yet - self.groupchat[room_jid][jid] = jid - log.info('JID Added: %s', jid) - - if not self.is_contact_in_roster(jid): - # Query Devicelists from JIDs not in our Roster - log.info('%s not in Roster, query devicelist...', jid) - self.query_devicelist(jid) - - def is_contact_in_roster(self, jid): - if jid == self.own_jid: - return True - contact = app.contacts.get_first_contact_from_jid(self._account, jid) - if contact is None: - return False - return contact.sub == 'both' + prepare_stanza(stanza, plaintext) + self.print_msg_to_log(stanza) + properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME}) def _on_muc_user_presence(self, _con, _stanza, properties): if properties.type == PresenceType.ERROR: @@ -454,7 +273,7 @@ class OMEMO: if not self.is_contact_in_roster(jid): # Query Devicelists from JIDs not in our Roster log.info('%s not in Roster, query devicelist...', jid) - self.query_devicelist(jid) + self.request_devicelist(jid) if properties.is_muc_self_presence: if StatusCode.NON_ANONYMOUS in status_codes: @@ -463,17 +282,55 @@ class OMEMO: self.groupchat[room] = self.temp_groupchat[room] log.info('OMEMO capable Room found: %s', room) + self.get_affiliation_list(room) - self.get_affiliation_list(room, 'owner') - self.get_affiliation_list(room, 'admin') - self.get_affiliation_list(room, 'member') + def get_affiliation_list(self, room_jid): + for affiliation in ('owner', 'admin', 'member'): + self._nbxmpp('MUC').get_affiliation( + room_jid, + affiliation, + callback=self._on_affiliations_received, + user_data=room_jid) - def get_affiliation_list(self, room_jid, affiliation): - iq = nbxmpp.Iq(typ='get', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN) - item = iq.setQuery().setTag('item') - item.setAttr('affiliation', affiliation) - self._con.connection.SendAndCallForResponse( - iq, self.room_memberlist_received) + def _on_affiliations_received(self, result, room_jid): + if is_error_result(result): + log.info('Affiliation request failed: %s', result) + return + + log.info('Room %s Memberlist received', room_jid) + if room_jid not in self.groupchat: + self.groupchat[room_jid] = {} + + def jid_known(jid): + for nick in self.groupchat[room_jid]: + if self.groupchat[room_jid][nick] == jid: + return True + return False + + for user_jid in result.users: + try: + jid = helpers.parse_jid(user_jid) + except helpers.InvalidFormat: + log.warning('Invalid JID: %s, ignoring it', user_jid) + continue + + if not jid_known(jid): + # Add JID with JID because we have no Nick yet + self.groupchat[room_jid][jid] = jid + log.info('JID Added: %s', jid) + + if not self.is_contact_in_roster(jid): + # Query Devicelists from JIDs not in our Roster + log.info('%s not in Roster, query devicelist...', jid) + self.request_devicelist(jid) + + def is_contact_in_roster(self, jid): + if jid == self.own_jid: + return True + contact = app.contacts.get_first_contact_from_jid(self._account, jid) + if contact is None: + return False + return contact.sub == 'both' def _on_config_changed(self, event): if event.account != self._account: @@ -487,35 +344,19 @@ class OMEMO: log.info('Room config change: non-anonymous') def gc_encrypt_message(self, conn, event, callback): - """ Manipulates the outgoing groupchat stanza - - The body is getting encrypted - - Parameters - ---------- - conn : nbxmpp.NonBlockingClient - - event : GcStanzaMessageOutgoingEvent - - callback: func - The callback. Its only called if the stanza was encrypted. - This prevents any accidental sending of unencrypted messages. - - """ if event.conn.name != self._account: return + + if not event.message: + callback(event) + return + + to_jid = app.get_jid_without_resource(event.jid) + try: - self.cleanup_stanza(event) - - if not event.message: - callback(event) - return - - to_jid = app.get_jid_without_resource(event.jid) - - msg_dict = self.omemo.create_gc_msg( - self.own_jid, to_jid, event.message.encode('utf8')) - if not msg_dict: + omemo_message = self.omemo.create_gc_msg( + self.own_jid, to_jid, event.message) + if omemo_message is None: raise OMEMOError('Error while encrypting') except OMEMOError as error: @@ -526,58 +367,27 @@ class OMEMO: error=error, time_=time.time(), session=None)) return - self.gc_message[msg_dict['payload']] = event.message - encrypted_node = OmemoMessage(msg_dict) - - event.msg_iq.addChild(node=encrypted_node) - - # XEP-0380: Explicit Message Encryption - eme_node = Node('encryption', attrs={'xmlns': NS_EME, - 'name': 'OMEMO', - 'namespace': NS_OMEMO}) - event.msg_iq.addChild(node=eme_node) - - # Add Message for devices that don't support OMEMO - support_msg = _("You received a message encrypted with " \ - "OMEMO but your client doesn't 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) + self.gc_message[omemo_message.payload] = event.message + create_omemo_message(event.msg_iq, omemo_message, + node_whitelist=ALLOWED_TAGS) self.print_msg_to_log(event.msg_iq) callback(event) def encrypt_message(self, conn, event, callback): - """ Manipulates the outgoing stanza - - Encrypt the body - - Parameters - ---------- - conn : nbxmpp.NonBlockingClient - - event : StanzaMessageOutgoingEvent - - callback: func - The callback. Its only called if the stanza was encrypted. - This prevents any accidental sending of unencrypted messages. - """ if event.conn.name != self._account: return + + if not event.message: + callback(event) + return + + to_jid = app.get_jid_without_resource(event.jid) + try: - self.cleanup_stanza(event) - - if not event.message: - callback(event) - return - - to_jid = app.get_jid_without_resource(event.jid) - - plaintext = event.message.encode('utf8') - msg_dict = self.omemo.create_msg(self.own_jid, to_jid, plaintext) - if not msg_dict: + omemo_message = self.omemo.create_msg( + self.own_jid, to_jid, event.message) + if omemo_message is None: raise OMEMOError('Error while encrypting') except OMEMOError as error: @@ -588,167 +398,15 @@ class OMEMO: error=error, time_=time.time(), session=event.session)) return - encrypted_node = OmemoMessage(msg_dict) - event.msg_iq.addChild(node=encrypted_node) + create_omemo_message(event.msg_iq, omemo_message, + node_whitelist=ALLOWED_TAGS) - # XEP-0380: Explicit Message Encryption - eme_node = Node('encryption', attrs={'xmlns': NS_EME, - 'name': 'OMEMO', - 'namespace': NS_OMEMO}) - event.msg_iq.addChild(node=eme_node) - - # Add Message for devices that don't support OMEMO - support_msg = _("You received a message encrypted with " \ - "OMEMO but your client doesn't 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) self.print_msg_to_log(event.msg_iq) event.xhtml = None event.encrypted = ENCRYPTION_NAME - self.add_additional_data(event.additional_data) + event.additional_data['encrypted'] = {'name': ENCRYPTION_NAME} callback(event) - @staticmethod - def cleanup_stanza(obj): - ''' We make sure only allowed tags are in the stanza ''' - - stanza = nbxmpp.Message( - to=obj.msg_iq.getTo(), - typ=obj.msg_iq.getType()) - stanza.setID(obj.stanza_id) - for tag, ns in ALLOWED_TAGS: - node = obj.msg_iq.getTag(tag, namespace=ns) - if node: - stanza.addChild(node=node) - obj.msg_iq = stanza - - def device_list_received(self, device_list, jid): - if not device_list: - log.error('%s => Received empty or invalid Devicelist from: %s', - self._account, jid) - return - - if self.get_own_jid().bareMatch(jid): - log.info('%s => Received own device list: %s', - self._account, device_list) - self.omemo.set_own_devices(device_list) - self.omemo.store.sessionStore.setActiveState( - device_list, self.own_jid) - - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if jid in self.query_for_bundles: - self.query_for_bundles.remove(jid) - - if not self.omemo.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() - else: - log.info('%s => Received device list for %s: %s', - self._account, jid, device_list) - self.omemo.set_devices(jid, device_list) - self.omemo.store.sessionStore.setActiveState( - device_list, jid) - - # remove contact from list, so on send button pressed - # we query for bundle and build a session - if jid in self.query_for_bundles: - self.query_for_bundles.remove(jid) - - - def _handle_device_list_update(self, conn, stanza, fetch_bundle=False): - """ Check if the passed event is a device list update and store the new - device ids. - - Parameters - ---------- - conn : nbxmpp.NonBlockingClient - - stanza: nbxmpp.Iq - - fetch_bundle: If True, bundles are fetched for the device ids - - """ - - devices_list = list(set(unpack_device_list_update(stanza, - self._account))) - contact_jid = stanza.getFrom().getStripped() - if not devices_list: - log.error('%s => Received empty or invalid Devicelist from: %s', - self._account, contact_jid) - return False - - if self.get_own_jid().bareMatch(contact_jid): - log.info('%s => Received own device list: %s', - self._account, devices_list) - self.omemo.set_own_devices(devices_list) - self.omemo.store.sessionStore.setActiveState( - devices_list, self.own_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 self.omemo.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() - else: - log.info('%s => Received device list for %s: %s', - self._account, contact_jid, devices_list) - self.omemo.set_devices(contact_jid, devices_list) - self.omemo.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) - - if fetch_bundle: - self.are_keys_missing(contact_jid) - # Enable Encryption on receiving first Device List - # TODO - - def publish_own_devices_list(self, new=False): - """ Get all currently known own active device ids and publish them - - Parameters - ---------- - new : bool - if True, a devicelist with only one - (the current id of this instance) device id is pushed - """ - if new: - devices_list = [self.omemo.own_device_id] - else: - devices_list = self.omemo.own_devices - devices_list.append(self.omemo.own_device_id) - devices_list = list(set(devices_list)) - self.omemo.set_own_devices(devices_list) - - list_node = Node('list', attrs={'xmlns': NS_OMEMO}) - for device in devices_list: - list_node.addChild('device').setAttr('id', device) - - con = app.connections[self._account] - con.get_module('PubSub').send_pb_publish( - '', NS_DEVICE_LIST, list_node, 'current', - cb=self.device_list_publish_result) - - log.info('%s => Publishing own Devices: %s', - self._account, devices_list) - - def device_list_publish_result(self, _con, stanza): - if not nbxmpp.isResultNode(stanza): - log.error('%s => Publishing devicelist failed: %s', - self._account, stanza.getError()) - def are_keys_missing(self, contact_jid): """ Checks if devicekeys are missing and queries the bundles @@ -774,8 +432,7 @@ class OMEMO: if devices_without_session: for device_id in devices_without_session: - self.fetch_device_bundle_information(self.own_jid, - device_id) + self.request_bundle(self.own_jid, device_id) # Fetch Bundles of contacts devices if contact_jid not in self.query_for_bundles: @@ -787,56 +444,34 @@ class OMEMO: if devices_without_session: for device_id in devices_without_session: - self.fetch_device_bundle_information(contact_jid, - device_id) + self.request_bundle(contact_jid, device_id) if self.omemo.getTrustedFingerprints(contact_jid): return False return True - def fetch_device_bundle_information(self, jid, device_id): - """ Fetch bundle information for specified jid, key, and create axolotl - session on success. + def set_bundle(self): + self._nbxmpp('OMEMO').set_bundle(self.omemo.bundle, + self.omemo.own_device_id) - Parameters - ---------- - jid : str - The jid to query for bundle information - device_id : int - The device id for which we want the bundle - """ - log.info('%s => Fetch bundle device %s#%s', + def request_bundle(self, jid, device_id): + log.info('%s => Fetch device bundle %s %s', self._account, device_id, jid) - bundle_query = BundleInformationQuery(jid, device_id) - self.send_with_callback(bundle_query, - self.session_from_prekey_bundle, - {'jid': jid, 'device_id': device_id}) - def session_from_prekey_bundle(self, conn, stanza, jid, 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. + self._nbxmpp('OMEMO').request_bundle( + jid, + device_id, + callback=self._bundle_received, + user_data=(jid, device_id)) - Parameters - ---------- - conn : nbxmpp.NonBlockingClient - - stanza : nbxmpp.Iq - The stanza - jid : str - Jid of the contact - device_id : int - The device id - """ - - bundle_dict = unpack_device_bundle(stanza, device_id) - if not bundle_dict: - log.warning('Failed to build Session with %s', jid) + def _bundle_received(self, bundle, user_data): + jid, device_id = user_data + if is_error_result(bundle): + log.info('%s => Bundle request failed: %s %s: %s', + self._account, jid, device_id, bundle) return - if self.omemo.build_session(jid, device_id, bundle_dict): + if self.omemo.build_session(jid, device_id, bundle): log.info('%s => session created for: %s', self._account, jid) # Trigger dialog to trust new Fingerprints if @@ -847,90 +482,93 @@ class OMEMO: app.nec.push_incoming_event( NetworkEvent('omemo-new-fingerprint', chat_control=ctrl)) - def query_devicelist(self, jid=None, fetch_bundle=False): - """ Query own devicelist from the server """ + def set_devicelist(self, new=False): + """ Get all currently known own active device ids and publish them + + Parameters + ---------- + new : bool + if True, a devicelist with only the id of this device + is published + """ + if new: + devicelist = [self.omemo.own_device_id] + else: + devicelist = self.omemo.own_devices + devicelist.append(self.omemo.own_device_id) + devicelist = list(set(devicelist)) + self.omemo.set_own_devices(devicelist) + log.info('%s => Publishing own devicelist: %s', + self._account, devicelist) + self._nbxmpp('OMEMO').set_devicelist(devicelist) + + def request_devicelist(self, jid=None, fetch_bundle=False): if jid in self.query_for_devicelists: return - if jid is None: - device_query = DevicelistQuery(self.own_jid) - log.info('%s => Querry own devicelist ...', self._account) - self.send_with_callback(device_query, - self.handle_devicelist_result) - else: - device_query = DevicelistQuery(jid) - log.info('%s => Querry devicelist from %s', self._account, jid) - self.send_with_callback(device_query, - self._handle_device_list_update, - data={'fetch_bundle': fetch_bundle}) + + self._nbxmpp('OMEMO').request_devicelist( + jid, + callback=self._devicelist_received, + user_data=(jid, fetch_bundle)) self.query_for_devicelists.append(jid) - def publish_bundle(self): - """ Publish our bundle information to the PEP node """ + def _devicelist_received(self, devicelist, user_data): + jid, fetch_bundle = user_data + if is_error_result(devicelist): + log.info('%s => Devicelist request failed: %s %s', + self._account, jid, devicelist) + devicelist = [] - bundle = make_bundle(self.omemo.bundle) - node = '%s%s' % (NS_BUNDLES, self.omemo.own_device_id) + self._process_devicelist_update(jid, devicelist, fetch_bundle) - log.info('%s => Publishing bundle ...', self._account) + @event_node(nbxmpp.NS_OMEMO_TEMP_DL) + def _devicelist_notification_received(self, _con, _stanza, properties): + devicelist = [] + if not properties.pubsub_event.empty: + devicelist = properties.pubsub_event.data - con = app.connections[self._account] - con.get_module('PubSub').send_pb_publish( - '', node, bundle, 'current', cb=self.handle_publish_result) + self._process_devicelist_update(str(properties.jid), devicelist, False) - def handle_publish_result(self, _con, stanza): - """ Log if publishing our bundle was successful - - Parameters - ---------- - stanza : nbxmpp.Iq - The stanza - """ - if successful(stanza): - log.info('%s => Publishing bundle was successful', self._account) - else: - log.error('%s => Publishing bundle was NOT successful', - self._account) - - def handle_devicelist_result(self, stanza): - """ If query was successful add own device to the list. - - Parameters - ---------- - stanza : nbxmpp.Iq - The stanza - """ - - if successful(stanza): - devices_list = list(set(unpack_device_list_update(stanza, self._account))) - if not devices_list: - self.publish_own_devices_list(new=True) - return - - self.omemo.set_own_devices(devices_list) + def _process_devicelist_update(self, jid, devicelist, fetch_bundle): + if jid is None or self._con.get_own_jid().bareMatch(jid): + log.info('%s => Received own device list: %s', + self._account, devicelist) + self.omemo.set_own_devices(devicelist) self.omemo.store.sessionStore.setActiveState( - devices_list, self.own_jid) - log.info('%s => Devicelistquery was successful', self._account) + devicelist, self.own_jid) + + # remove contact from list, so on send button pressed + # we query for bundle and build a session + if jid in self.query_for_bundles: + self.query_for_bundles.remove(jid) + if not self.omemo.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() + self.set_devicelist() + else: - log.error('%s => Devicelistquery was NOT successful: %s', - self._account, stanza.getError()) - self.publish_own_devices_list(new=True) + log.info('%s => Received device list for %s: %s', + self._account, jid, devicelist) + self.omemo.set_devices(jid, devicelist) + self.omemo.store.sessionStore.setActiveState(devicelist, jid) + + # remove contact from list, so on send button pressed + # we query for bundle and build a session + if jid in self.query_for_bundles: + self.query_for_bundles.remove(jid) + + if fetch_bundle: + self.are_keys_missing(jid) @staticmethod 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) - def add_additional_data(self, data): - data['encrypted'] = {'name': ENCRYPTION_NAME} - class OMEMOError(Exception): pass diff --git a/omemo/modules/omemo_devicelist.py b/omemo/modules/omemo_devicelist.py deleted file mode 100644 index c57eb37..0000000 --- a/omemo/modules/omemo_devicelist.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2018 Philipp Hörist -# -# This file is part of OMEMO. -# -# OMEMO is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published -# by the Free Software Foundation; version 3 only. -# -# OMEMO is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with OMEMO. If not, see . - -# XEP-0384: OMEMO Encryption - -import logging - -import nbxmpp - -from gajim.common import app -from gajim.common.modules.pep import AbstractPEPModule, AbstractPEPData - -from omemo.modules.util import NS_OMEMO -from omemo.modules.util import NS_DEVICE_LIST -from omemo.modules.util import unpack_devicelist - -log = logging.getLogger('gajim.plugin_system.omemo.pep') - -# Module name -name = 'OMEMODevicelist' -zeroconf = False - - -class OMEMODevicelistData(AbstractPEPData): - - type_ = 'omemo-devicelist' - - -class OMEMODevicelist(AbstractPEPModule): - ''' - - - - - - - ''' - - name = 'omemo-devicelist' - namespace = NS_DEVICE_LIST - pep_class = OMEMODevicelistData - store_publish = True - _log = log - - @staticmethod - def _extract_info(item): - return unpack_devicelist(item) - - def _notification_received(self, jid, devicelist): - con = app.connections[self._account] - con.get_module('OMEMO').device_list_received(devicelist.data, - jid.getStripped()) - @staticmethod - def _build_node(devicelist): - list_node = nbxmpp.Node('list', {'xmlns': NS_OMEMO}) - if devicelist is None: - return list_node - for device in devicelist: - list_node.addChild('device', attrs={'id': device}) - return list_node - - -def get_instance(*args, **kwargs): - return OMEMODevicelist(*args, **kwargs), 'OMEMODevicelist' diff --git a/omemo/modules/util.py b/omemo/modules/util.py index 084aeb0..5706d06 100644 --- a/omemo/modules/util.py +++ b/omemo/modules/util.py @@ -1,46 +1,30 @@ -# Copyright (C) 2018 Philipp Hörist +# Copyright (C) 2019 Philipp Hörist # -# This file is part of OMEMO. +# This file is part of OMEMO Gajim Plugin. # -# OMEMO is free software; you can redistribute it and/or modify +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # -# OMEMO is distributed in the hope that it will be useful, +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with OMEMO. If not, see . +# along with OMEMO Gajim Plugin. If not, see . -# XEP-0384: OMEMO Encryption - -import logging import nbxmpp -from gajim.common.exceptions import StanzaMalformed + +def prepare_stanza(stanza, plaintext): + delete_nodes(stanza, 'encrypted', nbxmpp.NS_OMEMO_TEMP) + delete_nodes(stanza, 'body') + stanza.setBody(plaintext) -log = logging.getLogger('gajim.plugin_system.omemo') - -NS_OMEMO = 'eu.siacs.conversations.axolotl' -NS_DEVICE_LIST = NS_OMEMO + '.devicelist' - - -def unpack_devicelist(item): - list_ = item.getTag('list', namespace=NS_OMEMO) - if list_ is None: - raise StanzaMalformed('No list node') - - device_list = list_.getTags('device') - - devices = [] - for device in device_list: - id_ = device.getAttr('id') - if id_ is None: - raise StanzaMalformed('No id for device found') - - devices.append(int(id_)) - return devices +def delete_nodes(stanza, name, namespace=None): + nodes = stanza.getTags(name, namespace=namespace) + for node in nodes: + stanza.delChild(node) diff --git a/omemo/omemo/aes_gcm.py b/omemo/omemo/aes_gcm.py deleted file mode 100644 index ebc5235..0000000 --- a/omemo/omemo/aes_gcm.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Bahtiar `kalkin-` Gadimov -# -# This file is part of python-omemo library. -# -# The python-omemo library 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. -# -# python-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 python-omemo library. If not, see . -# - - -import sys -import logging - -from .aes_gcm_native import aes_decrypt -from .aes_gcm_native import aes_encrypt - -log = logging.getLogger('gajim.plugin_system.omemo') - - -def encrypt(key, iv, plaintext): - return aes_encrypt(key, iv, plaintext) - - -def decrypt(key, iv, ciphertext): - return aes_decrypt(key, iv, ciphertext).decode('utf-8') - - -class NoValidSessions(Exception): - pass diff --git a/omemo/omemo/aes_gcm_native.py b/omemo/omemo/aes_gcm_native.py deleted file mode 100644 index de3b7b5..0000000 --- a/omemo/omemo/aes_gcm_native.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Bahtiar `kalkin-` Gadimov -# -# This file is part of python-omemo library. -# -# The python-omemo library 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. -# -# python-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 python-omemo library. If not, see . -# - - -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 - -# On Windows we have to import a specific backend because the -# default_backend() mechanism doesn't work in Gajim for Windows. -# Its because of how Gajim is build with cx_freeze - -if os.name == 'nt': - from cryptography.hazmat.backends.openssl import backend -else: - from cryptography.hazmat.backends import default_backend - -log = logging.getLogger('gajim.plugin_system.omemo') - -def aes_decrypt(_key, iv, payload): - """ Use AES128 GCM with the given key and iv to decrypt the payload. """ - 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: - _backend = default_backend() - decryptor = Cipher( - algorithms.AES(key), - GCM(iv, tag=tag), - backend=_backend).decryptor() - return decryptor.update(data) + decryptor.finalize() - - -def aes_encrypt(key, iv, plaintext): - """ Use AES128 GCM with the given key and iv to encrypt the plaintext. """ - if os.name == 'nt': - _backend = backend - else: - _backend = default_backend() - encryptor = Cipher( - algorithms.AES(key), - GCM(iv), - backend=_backend).encryptor() - return encryptor.update(plaintext) + encryptor.finalize(), encryptor.tag diff --git a/omemo/omemoplugin.py b/omemo/plugin.py similarity index 85% rename from omemo/omemoplugin.py rename to omemo/plugin.py index b84f446..44194c1 100644 --- a/omemo/omemoplugin.py +++ b/omemo/plugin.py @@ -1,24 +1,20 @@ -# -*- coding: utf-8 -*- - -''' -Copyright 2015 Bahtiar `kalkin-` Gadimov -Copyright 2015 Daniel Gultsch -Copyright 2016 Philipp Hörist - -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 . -''' +# Copyright (C) 2019 Philipp Hörist +# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov +# Copyright (C) 2015 Daniel Gultsch +# +# This file is part of OMEMO Gajim Plugin. +# +# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# OMEMO Gajim Plugin is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OMEMO Gajim Plugin. If not, see . import logging import binascii @@ -36,11 +32,12 @@ from gajim.plugins import GajimPlugin from gajim.plugins.plugins_i18n import _ from gajim.groupchat_control import GroupchatControl +from omemo import file_crypto from omemo.gtk.key import KeyDialog from omemo.gtk.config import OMEMOConfigDialog +from omemo.backend.aes import aes_encrypt_file -CRYPTOGRAPHY_MISSING = 'You are missing Python3-Cryptography' AXOLOTL_MISSING = 'You are missing Python3-Axolotl or use an outdated version' PROTOBUF_MISSING = "OMEMO can't import Google Protobuf, you can find help in " \ "the GitHub Wiki" @@ -48,12 +45,11 @@ ERROR_MSG = '' log = logging.getLogger('gajim.plugin_system.omemo') - -try: - from omemo import file_crypto -except Exception as error: - log.exception(error) - ERROR_MSG = CRYPTOGRAPHY_MISSING +if log.getEffectiveLevel() == logging.DEBUG: + log_axolotl = logging.getLogger('axolotl') + log_axolotl.setLevel(logging.DEBUG) + log_axolotl.addHandler(logging.StreamHandler()) + log_axolotl.propagate = False try: import google.protobuf @@ -70,14 +66,10 @@ except Exception as error: if not ERROR_MSG: try: from omemo.modules import omemo - from omemo.modules import omemo_devicelist except Exception as error: log.error(error) ERROR_MSG = 'Error: %s' % error -# pylint: disable=no-init -# pylint: disable=attribute-defined-outside-init - @unique class UserMessages(IntEnum): @@ -87,7 +79,6 @@ class UserMessages(IntEnum): class OmemoPlugin(GajimPlugin): def init(self): - """ Init """ if ERROR_MSG: self.activatable = False self.available_text = ERROR_MSG @@ -99,17 +90,13 @@ class OmemoPlugin(GajimPlugin): 'signed-in': (ged.PRECORE, self.signed_in), 'omemo-new-fingerprint': (ged.PRECORE, self._on_new_fingerprints), } - self.modules = [ - omemo, - omemo_devicelist, - ] + self.modules = [omemo] self.config_dialog = OMEMOConfigDialog(self) self.gui_extension_points = { 'hyperlink_handler': (self._file_decryption, None), 'encrypt' + self.encryption_name: (self._encrypt_message, None), 'gc_encrypt' + self.encryption_name: ( self._gc_encrypt_message, None), - 'decrypt': (self._message_received, None), 'send_message' + self.encryption_name: ( self.before_sendmessage, None), 'encryption_dialog' + self.encryption_name: ( @@ -198,11 +185,6 @@ class OmemoPlugin(GajimPlugin): return False return True - def _message_received(self, conn, obj, callback): - if conn.name == 'Local': - return - app.connections[conn.name].get_module('OMEMO').message_received(conn, obj, callback) - def _gc_encrypt_message(self, conn, obj, callback): if conn.name == 'Local': return @@ -225,12 +207,11 @@ class OmemoPlugin(GajimPlugin): @staticmethod def _encrypt_file_thread(file, callback, *args, **kwargs): - encrypted_data, key, iv = file_crypto.encrypt_file( - file.get_data(full=True)) + result = aes_encrypt_file(file.get_data(full=True)) file.encrypted = True - file.size = len(encrypted_data) - file.user_data = binascii.hexlify(iv + key).decode('utf-8') - file.data = encrypted_data + file.size = len(result.payload) + file.user_data = binascii.hexlify(result.iv + result.key).decode() + file.data = result.payload if file.event.isSet(): return GLib.idle_add(callback, file) @@ -267,7 +248,7 @@ class OmemoPlugin(GajimPlugin): else: # check if we have devices for the contact if not self.get_omemo(account).device_list_for(contact.jid): - con.query_devicelist(contact.jid, True) + con.request_devicelist(contact.jid, True) self.print_message(chat_control, UserMessages.QUERY_DEVICES) chat_control.sendmessage = False return diff --git a/omemo/xmpp.py b/omemo/xmpp.py deleted file mode 100644 index 28ae76d..0000000 --- a/omemo/xmpp.py +++ /dev/null @@ -1,308 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Bahtiar `kalkin-` Gadimov -# -# 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 . -# - -""" This module handles all the XMPP logic like creating different kind of -stanza nodes and getting data from stanzas. -""" - -import logging -import random -from base64 import b64decode, b64encode - -from nbxmpp.protocol import NS_PUBSUB, Iq -from nbxmpp.simplexml import Node - -from gajim.common import app # pylint: disable=import-error -from gajim.plugins.helpers import log_calls # pylint: disable=import-error - -NS_PUBSUB_EVENT = NS_PUBSUB + '#event' - -NS_EME = 'urn:xmpp:eme:0' -NS_OMEMO = 'eu.siacs.conversations.axolotl' -NS_DEVICE_LIST = NS_OMEMO + '.devicelist' -NS_NOTIFY = NS_DEVICE_LIST + '+notify' -NS_BUNDLES = NS_OMEMO + '.bundles:' -NS_HINTS = 'urn:xmpp:hints' -log = logging.getLogger('gajim.plugin_system.omemo') - - -class PubsubNode(Node): - def __init__(self, data): - assert isinstance(data, Node) - Node.__init__(self, tag='pubsub', attrs={'xmlns': NS_PUBSUB}) - self.addChild(node=data) - - -class OmemoMessage(Node): - def __init__(self, msg_dict): - # , 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, 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']) - .decode('utf-8')) - - -class BundleInformationQuery(Iq): - def __init__(self, contact_jid, device_id): - assert isinstance(device_id, int) - id_ = app.get_an_id() - attrs = {'id': id_} - Iq.__init__(self, typ='get', attrs=attrs, to=contact_jid) - items = Node('items', attrs={'node': NS_BUNDLES + str(device_id), - 'max_items': 1}) - pubsub = PubsubNode(items) - self.addChild(node=pubsub) - - -class DevicelistQuery(Iq): - def __init__(self, contact_jid): - id_ = app.get_an_id() - attrs = {'id': id_} - Iq.__init__(self, typ='get', attrs=attrs, to=contact_jid) - items = Node('items', attrs={'node': NS_DEVICE_LIST, 'max_items': 1}) - pubsub = PubsubNode(items) - self.addChild(node=pubsub) - - -def make_bundle(state_bundle): - result = Node('bundle', attrs={'xmlns': NS_OMEMO}) - prekey_pub_node = result.addChild( - 'signedPreKeyPublic', - attrs={'signedPreKeyId': state_bundle['signedPreKeyId']}) - prekey_pub_node.addData(state_bundle['signedPreKeyPublic'] - .decode('utf-8')) - - prekey_sig_node = result.addChild('signedPreKeySignature') - prekey_sig_node.addData(state_bundle['signedPreKeySignature'] - .decode('utf-8')) - - identity_key_node = result.addChild('identityKey') - identity_key_node.addData(state_bundle['identityKey'].decode('utf-8')) - prekeys = result.addChild('prekeys') - - for key in state_bundle['prekeys']: - prekeys.addChild('preKeyPublic', - attrs={'preKeyId': key[0]}) \ - .addData(key[1].decode('utf-8')) - return result - - -@log_calls('OmemoPlugin') -def unpack_device_bundle(bundle, device_id): - pubsub = bundle.getTag('pubsub', namespace=NS_PUBSUB) - if not pubsub: - log.warning('OMEMO device bundle has no pubsub node') - return - items = pubsub.getTag('items', attrs={'node': NS_BUNDLES + str(device_id)}) - if not items: - log.warning('OMEMO device bundle has no items node') - return - - item = items.getTag('item', namespace=NS_PUBSUB) - if not item: - log.warning('OMEMO device bundle has no item node') - return - - bundle = item.getTag('bundle', namespace=NS_OMEMO) - if not bundle: - log.warning('OMEMO device bundle has no bundle node') - return - - signed_prekey_node = bundle.getTag('signedPreKeyPublic', - namespace=NS_OMEMO) - if not signed_prekey_node: - log.warning('OMEMO device bundle has no signedPreKeyPublic node') - return - - result = {} - result['signedPreKeyPublic'] = decode_data(signed_prekey_node) - if not result['signedPreKeyPublic']: - log.warning('OMEMO device bundle has no signedPreKeyPublic data') - return - - if not signed_prekey_node.getAttr('signedPreKeyId'): - log.warning('OMEMO device bundle has no signedPreKeyId') - return - result['signedPreKeyId'] = int(signed_prekey_node.getAttr( - 'signedPreKeyId')) - - signed_signature_node = bundle.getTag('signedPreKeySignature', - namespace=NS_OMEMO) - if not signed_signature_node: - log.warning('OMEMO device bundle has no signedPreKeySignature node') - return - - result['signedPreKeySignature'] = decode_data(signed_signature_node) - if not result['signedPreKeySignature']: - log.warning('OMEMO device bundle has no signedPreKeySignature data') - return - - identity_key_node = bundle.getTag('identityKey', namespace=NS_OMEMO) - if not identity_key_node: - log.warning('OMEMO device bundle has no identityKey node') - return - - result['identityKey'] = decode_data(identity_key_node) - if not result['identityKey']: - log.warning('OMEMO device bundle has no identityKey data') - return - - prekeys = bundle.getTag('prekeys', namespace=NS_OMEMO) - if not prekeys or len(prekeys.getChildren()) == 0: - log.warning('OMEMO device bundle has no prekys') - return - - picked_key_node = random.SystemRandom().choice(prekeys.getChildren()) - - if not picked_key_node.getAttr('preKeyId'): - log.warning('OMEMO PreKey has no id set') - return - result['preKeyId'] = int(picked_key_node.getAttr('preKeyId')) - - result['preKeyPublic'] = decode_data(picked_key_node) - if not result['preKeyPublic']: - return - return result - - -@log_calls('OmemoPlugin') -def unpack_encrypted(encrypted_node): - """ Unpacks the encrypted node, decodes the data and returns a msg_dict. - """ - if not encrypted_node.getNamespace() == NS_OMEMO: - log.warning("Encrypted node with wrong NS") - return - - header_node = encrypted_node.getTag('header', namespace=NS_OMEMO) - if not header_node: - log.warning("OMEMO message without header") - return - - if not header_node.getAttr('sid'): - log.warning("OMEMO message without sid in header") - return - - sid = int(header_node.getAttr('sid')) - - iv_node = header_node.getTag('iv', namespace=NS_OMEMO) - if not iv_node: - log.warning("OMEMO message without iv") - return - - iv = decode_data(iv_node) - if not iv: - log.warning("OMEMO message without iv data") - - payload_node = encrypted_node.getTag('payload', namespace=NS_OMEMO) - payload = None - if payload_node: - payload = decode_data(payload_node) - - key_nodes = header_node.getTags('key') - if len(key_nodes) < 1: - log.warning("OMEMO message without keys") - return - - keys = {} - for kn in key_nodes: - rid = kn.getAttr('rid') - if not rid: - log.warning('Omemo key without rid') - continue - - if not kn.getData(): - log.warning('Omemo key without data') - continue - - keys[int(rid)] = decode_data(kn) - - result = {'sid': sid, 'iv': iv, 'keys': keys, 'payload': payload} - return result - - -def unpack_device_list_update(stanza, account): - """ Unpacks the device list from a stanza - - Parameters - ---------- - stanza - - Returns - ------- - [int] - List of device ids or empty list if nothing found - """ - event_node = stanza.getTag('event', namespace=NS_PUBSUB_EVENT) - if not event_node: - event_node = stanza.getTag('pubsub', namespace=NS_PUBSUB) - result = [] - - if not event_node: - log.warning(account + ' => Device list update event node empty!') - return result - - items = event_node.getTag('items', {'node': NS_DEVICE_LIST}) - if not items or len(items.getChildren()) != 1: - log.warning( - account + - ' => Device list update items node empty or not omemo device update') - return result - - list_node = items.getChildren()[0].getTag('list') - if not list_node or len(list_node.getChildren()) == 0: - log.warning(account + ' => Device list update list node empty!') - return result - - devices_nodes = list_node.getChildren() - - for dn in devices_nodes: - _id = dn.getAttr('id') - if _id: - result.append(int(_id)) - - return result - - -def decode_data(node): - """ Fetch the data from specified node and b64decode it. """ - data = node.getData() - - if not data: - log.warning("No node data") - return - try: - return b64decode(data) - except: - log.warning('b64decode broken') - return - - -def successful(stanza): - """ Check if stanza type is result. """ - return stanza.getAttr('type') == 'result'