[omemo] Refactor Plugin

- Adapt to nbxmpp supporting OMEMO
- Move python-axolotl code into backend folder
This commit is contained in:
Philipp Hörist
2019-02-11 22:32:27 +01:00
parent a6ed314941
commit 3c78b09fb2
24 changed files with 1460 additions and 2318 deletions

View File

@@ -1 +1 @@
from .omemoplugin import OmemoPlugin from omemo.plugin import OmemoPlugin

View File

@@ -1 +1 @@
__version__ = "0.1.0" __version__ = "0.1.0"

86
omemo/backend/aes.py Normal file
View File

@@ -0,0 +1,86 @@
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of OMEMO Gajim Plugin.
#
# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# OMEMO Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import 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)

View File

@@ -1,15 +1,15 @@
''' Database helper functions ''' ''' Database helper functions '''
def table_exists(db, name): def table_exists(db, name):
""" Check if the specified table exists in the db. """ """ Check if the specified table exists in the db. """
query = """ SELECT name FROM sqlite_master query = """ SELECT name FROM sqlite_master
WHERE type='table' AND name=?; WHERE type='table' AND name=?;
""" """
return db.execute(query, (name, )).fetchone() is not None return db.execute(query, (name, )).fetchone() is not None
def user_version(db): def user_version(db):
""" Return the value of PRAGMA user_version. """ """ Return the value of PRAGMA user_version. """
return db.execute('PRAGMA user_version').fetchone()[0] return db.execute('PRAGMA user_version').fetchone()[0]

View File

@@ -1,64 +1,64 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de> # Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright 2015 Daniel Gultsch <daniel@cgultsch.de> # Copyright 2015 Daniel Gultsch <daniel@cgultsch.de>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
class EncryptionState(): class EncryptionState():
""" Used to store if OMEMO is enabled or not between gajim restarts """ """ Used to store if OMEMO is enabled or not between gajim restarts """
def __init__(self, dbConn): def __init__(self, dbConn):
""" """
:type dbConn: Connection :type dbConn: Connection
""" """
self.dbConn = dbConn self.dbConn = dbConn
def activate(self, jid): def activate(self, jid):
q = """INSERT OR REPLACE INTO encryption_state (jid, encryption) q = """INSERT OR REPLACE INTO encryption_state (jid, encryption)
VALUES (?, 1) """ VALUES (?, 1) """
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (jid, )) c.execute(q, (jid, ))
self.dbConn.commit() self.dbConn.commit()
def deactivate(self, jid): def deactivate(self, jid):
q = """INSERT OR REPLACE INTO encryption_state (jid, encryption) q = """INSERT OR REPLACE INTO encryption_state (jid, encryption)
VALUES (?, 0)""" VALUES (?, 0)"""
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (jid, )) c.execute(q, (jid, ))
self.dbConn.commit() self.dbConn.commit()
def is_active(self, jid): def is_active(self, jid):
q = 'SELECT encryption FROM encryption_state where jid = ?;' q = 'SELECT encryption FROM encryption_state where jid = ?;'
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (jid, )) c.execute(q, (jid, ))
result = c.fetchone() result = c.fetchone()
if result is None: if result is None:
return False return False
return result[0] return result[0]
def exist(self, jid): def exist(self, jid):
q = 'SELECT encryption FROM encryption_state where jid = ?;' q = 'SELECT encryption FROM encryption_state where jid = ?;'
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (jid, )) c.execute(q, (jid, ))
result = c.fetchone() result = c.fetchone()
if result is None: if result is None:
return False return False
else: else:
return True return True

View File

@@ -1,186 +1,186 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Tarek Galal <tare2.galal@gmail.com> # Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
import logging import logging
from axolotl.state.axolotlstore import AxolotlStore from axolotl.state.axolotlstore import AxolotlStore
from axolotl.util.keyhelper import KeyHelper from axolotl.util.keyhelper import KeyHelper
from .liteidentitykeystore import LiteIdentityKeyStore from .liteidentitykeystore import LiteIdentityKeyStore
from .liteprekeystore import LitePreKeyStore from .liteprekeystore import LitePreKeyStore
from .litesessionstore import LiteSessionStore from .litesessionstore import LiteSessionStore
from .litesignedprekeystore import LiteSignedPreKeyStore from .litesignedprekeystore import LiteSignedPreKeyStore
from .encryption import EncryptionState from .encryption import EncryptionState
from .sql import SQLDatabase from .sql import SQLDatabase
log = logging.getLogger('gajim.plugin_system.omemo') log = logging.getLogger('gajim.plugin_system.omemo')
DEFAULT_PREKEY_AMOUNT = 100 DEFAULT_PREKEY_AMOUNT = 100
MIN_PREKEY_AMOUNT = 80 MIN_PREKEY_AMOUNT = 80
SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days
SPK_CYCLE_TIME = 86400 # 24 Hours SPK_CYCLE_TIME = 86400 # 24 Hours
class LiteAxolotlStore(AxolotlStore): class LiteAxolotlStore(AxolotlStore):
def __init__(self, connection): def __init__(self, connection):
try: try:
connection.text_factory = bytes connection.text_factory = bytes
except(AttributeError): except(AttributeError):
raise AssertionError('Expected a sqlite3.Connection got ' + raise AssertionError('Expected a sqlite3.Connection got ' +
str(connection)) str(connection))
self.sql = SQLDatabase(connection) self.sql = SQLDatabase(connection)
self.identityKeyStore = LiteIdentityKeyStore(connection) self.identityKeyStore = LiteIdentityKeyStore(connection)
self.preKeyStore = LitePreKeyStore(connection) self.preKeyStore = LitePreKeyStore(connection)
self.signedPreKeyStore = LiteSignedPreKeyStore(connection) self.signedPreKeyStore = LiteSignedPreKeyStore(connection)
self.sessionStore = LiteSessionStore(connection) self.sessionStore = LiteSessionStore(connection)
self.encryptionStore = EncryptionState(connection) self.encryptionStore = EncryptionState(connection)
if not self.getLocalRegistrationId(): if not self.getLocalRegistrationId():
log.info("Generating Axolotl keys") log.info("Generating Axolotl keys")
self._generate_axolotl_keys() self._generate_axolotl_keys()
def _generate_axolotl_keys(self): def _generate_axolotl_keys(self):
identityKeyPair = KeyHelper.generateIdentityKeyPair() identityKeyPair = KeyHelper.generateIdentityKeyPair()
registrationId = KeyHelper.generateRegistrationId() registrationId = KeyHelper.generateRegistrationId()
preKeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(), preKeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(),
DEFAULT_PREKEY_AMOUNT) DEFAULT_PREKEY_AMOUNT)
self.storeLocalData(registrationId, identityKeyPair) self.storeLocalData(registrationId, identityKeyPair)
signedPreKey = KeyHelper.generateSignedPreKey( signedPreKey = KeyHelper.generateSignedPreKey(
identityKeyPair, KeyHelper.getRandomSequence(65536)) identityKeyPair, KeyHelper.getRandomSequence(65536))
self.storeSignedPreKey(signedPreKey.getId(), signedPreKey) self.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
for preKey in preKeys: for preKey in preKeys:
self.storePreKey(preKey.getId(), preKey) self.storePreKey(preKey.getId(), preKey)
def getIdentityKeyPair(self): def getIdentityKeyPair(self):
return self.identityKeyStore.getIdentityKeyPair() return self.identityKeyStore.getIdentityKeyPair()
def storeLocalData(self, registrationId, identityKeyPair): def storeLocalData(self, registrationId, identityKeyPair):
self.identityKeyStore.storeLocalData(registrationId, identityKeyPair) self.identityKeyStore.storeLocalData(registrationId, identityKeyPair)
def getLocalRegistrationId(self): def getLocalRegistrationId(self):
return self.identityKeyStore.getLocalRegistrationId() return self.identityKeyStore.getLocalRegistrationId()
def saveIdentity(self, recepientId, identityKey): def saveIdentity(self, recepientId, identityKey):
self.identityKeyStore.saveIdentity(recepientId, identityKey) self.identityKeyStore.saveIdentity(recepientId, identityKey)
def deleteIdentity(self, recipientId, identityKey): def deleteIdentity(self, recipientId, identityKey):
self.identityKeyStore.deleteIdentity(recipientId, identityKey) self.identityKeyStore.deleteIdentity(recipientId, identityKey)
def isTrustedIdentity(self, recepientId, identityKey): def isTrustedIdentity(self, recepientId, identityKey):
return self.identityKeyStore.isTrustedIdentity(recepientId, return self.identityKeyStore.isTrustedIdentity(recepientId,
identityKey) identityKey)
def setTrust(self, identityKey, trust): def setTrust(self, identityKey, trust):
return self.identityKeyStore.setTrust(identityKey, trust) return self.identityKeyStore.setTrust(identityKey, trust)
def getTrustedFingerprints(self, jid): def getTrustedFingerprints(self, jid):
return self.identityKeyStore.getTrustedFingerprints(jid) return self.identityKeyStore.getTrustedFingerprints(jid)
def getUndecidedFingerprints(self, jid): def getUndecidedFingerprints(self, jid):
return self.identityKeyStore.getUndecidedFingerprints(jid) return self.identityKeyStore.getUndecidedFingerprints(jid)
def setShownFingerprints(self, jid): def setShownFingerprints(self, jid):
return self.identityKeyStore.setShownFingerprints(jid) return self.identityKeyStore.setShownFingerprints(jid)
def getNewFingerprints(self, jid): def getNewFingerprints(self, jid):
return self.identityKeyStore.getNewFingerprints(jid) return self.identityKeyStore.getNewFingerprints(jid)
def loadPreKey(self, preKeyId): def loadPreKey(self, preKeyId):
return self.preKeyStore.loadPreKey(preKeyId) return self.preKeyStore.loadPreKey(preKeyId)
def loadPreKeys(self): def loadPreKeys(self):
return self.preKeyStore.loadPendingPreKeys() return self.preKeyStore.loadPendingPreKeys()
def storePreKey(self, preKeyId, preKeyRecord): def storePreKey(self, preKeyId, preKeyRecord):
self.preKeyStore.storePreKey(preKeyId, preKeyRecord) self.preKeyStore.storePreKey(preKeyId, preKeyRecord)
def containsPreKey(self, preKeyId): def containsPreKey(self, preKeyId):
return self.preKeyStore.containsPreKey(preKeyId) return self.preKeyStore.containsPreKey(preKeyId)
def removePreKey(self, preKeyId): def removePreKey(self, preKeyId):
self.preKeyStore.removePreKey(preKeyId) self.preKeyStore.removePreKey(preKeyId)
def loadSession(self, recepientId, deviceId): def loadSession(self, recepientId, deviceId):
return self.sessionStore.loadSession(recepientId, deviceId) return self.sessionStore.loadSession(recepientId, deviceId)
def getActiveDeviceTuples(self): def getActiveDeviceTuples(self):
return self.sessionStore.getActiveDeviceTuples() return self.sessionStore.getActiveDeviceTuples()
def getInactiveSessionsKeys(self, recipientId): def getInactiveSessionsKeys(self, recipientId):
return self.sessionStore.getInactiveSessionsKeys(recipientId) return self.sessionStore.getInactiveSessionsKeys(recipientId)
def getSubDeviceSessions(self, recepientId): def getSubDeviceSessions(self, recepientId):
# TODO Reuse this # TODO Reuse this
return self.sessionStore.getSubDeviceSessions(recepientId) return self.sessionStore.getSubDeviceSessions(recepientId)
def getJidFromDevice(self, device_id): def getJidFromDevice(self, device_id):
return self.sessionStore.getJidFromDevice(device_id) return self.sessionStore.getJidFromDevice(device_id)
def storeSession(self, recepientId, deviceId, sessionRecord): def storeSession(self, recepientId, deviceId, sessionRecord):
self.sessionStore.storeSession(recepientId, deviceId, sessionRecord) self.sessionStore.storeSession(recepientId, deviceId, sessionRecord)
def containsSession(self, recepientId, deviceId): def containsSession(self, recepientId, deviceId):
return self.sessionStore.containsSession(recepientId, deviceId) return self.sessionStore.containsSession(recepientId, deviceId)
def deleteSession(self, recepientId, deviceId): def deleteSession(self, recepientId, deviceId):
self.sessionStore.deleteSession(recepientId, deviceId) self.sessionStore.deleteSession(recepientId, deviceId)
def deleteAllSessions(self, recepientId): def deleteAllSessions(self, recepientId):
self.sessionStore.deleteAllSessions(recepientId) self.sessionStore.deleteAllSessions(recepientId)
def getSessionsFromJid(self, recipientId): def getSessionsFromJid(self, recipientId):
return self.sessionStore.getSessionsFromJid(recipientId) return self.sessionStore.getSessionsFromJid(recipientId)
def getSessionsFromJids(self, recipientId): def getSessionsFromJids(self, recipientId):
return self.sessionStore.getSessionsFromJids(recipientId) return self.sessionStore.getSessionsFromJids(recipientId)
def getAllSessions(self): def getAllSessions(self):
return self.sessionStore.getAllSessions() return self.sessionStore.getAllSessions()
def loadSignedPreKey(self, signedPreKeyId): def loadSignedPreKey(self, signedPreKeyId):
return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId) return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId)
def loadSignedPreKeys(self): def loadSignedPreKeys(self):
return self.signedPreKeyStore.loadSignedPreKeys() return self.signedPreKeyStore.loadSignedPreKeys()
def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord): def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
self.signedPreKeyStore.storeSignedPreKey(signedPreKeyId, self.signedPreKeyStore.storeSignedPreKey(signedPreKeyId,
signedPreKeyRecord) signedPreKeyRecord)
def containsSignedPreKey(self, signedPreKeyId): def containsSignedPreKey(self, signedPreKeyId):
return self.signedPreKeyStore.containsSignedPreKey(signedPreKeyId) return self.signedPreKeyStore.containsSignedPreKey(signedPreKeyId)
def removeSignedPreKey(self, signedPreKeyId): def removeSignedPreKey(self, signedPreKeyId):
self.signedPreKeyStore.removeSignedPreKey(signedPreKeyId) self.signedPreKeyStore.removeSignedPreKey(signedPreKeyId)
def getNextSignedPreKeyId(self): def getNextSignedPreKeyId(self):
return self.signedPreKeyStore.getNextSignedPreKeyId() return self.signedPreKeyStore.getNextSignedPreKeyId()
def getCurrentSignedPreKeyId(self): def getCurrentSignedPreKeyId(self):
return self.signedPreKeyStore.getCurrentSignedPreKeyId() return self.signedPreKeyStore.getCurrentSignedPreKeyId()
def getSignedPreKeyTimestamp(self, signedPreKeyId): def getSignedPreKeyTimestamp(self, signedPreKeyId):
return self.signedPreKeyStore.getSignedPreKeyTimestamp(signedPreKeyId) return self.signedPreKeyStore.getSignedPreKeyTimestamp(signedPreKeyId)
def removeOldSignedPreKeys(self, timestamp): def removeOldSignedPreKeys(self, timestamp):
self.signedPreKeyStore.removeOldSignedPreKeys(timestamp) self.signedPreKeyStore.removeOldSignedPreKeys(timestamp)

View File

@@ -1,174 +1,174 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Tarek Galal <tare2.galal@gmail.com> # Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
from axolotl.ecc.djbec import DjbECPrivateKey, DjbECPublicKey from axolotl.ecc.djbec import DjbECPrivateKey, DjbECPublicKey
from axolotl.identitykey import IdentityKey from axolotl.identitykey import IdentityKey
from axolotl.identitykeypair import IdentityKeyPair from axolotl.identitykeypair import IdentityKeyPair
from axolotl.state.identitykeystore import IdentityKeyStore from axolotl.state.identitykeystore import IdentityKeyStore
UNDECIDED = 2 UNDECIDED = 2
TRUSTED = 1 TRUSTED = 1
UNTRUSTED = 0 UNTRUSTED = 0
class LiteIdentityKeyStore(IdentityKeyStore): class LiteIdentityKeyStore(IdentityKeyStore):
def __init__(self, dbConn): def __init__(self, dbConn):
""" """
:type dbConn: Connection :type dbConn: Connection
""" """
self.dbConn = dbConn self.dbConn = dbConn
def getIdentityKeyPair(self): def getIdentityKeyPair(self):
q = "SELECT public_key, private_key FROM identities " + \ q = "SELECT public_key, private_key FROM identities " + \
"WHERE recipient_id = -1" "WHERE recipient_id = -1"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q) c.execute(q)
result = c.fetchone() result = c.fetchone()
publicKey, privateKey = result publicKey, privateKey = result
return IdentityKeyPair( return IdentityKeyPair(
IdentityKey(DjbECPublicKey(publicKey[1:])), IdentityKey(DjbECPublicKey(publicKey[1:])),
DjbECPrivateKey(privateKey)) DjbECPrivateKey(privateKey))
def getLocalRegistrationId(self): def getLocalRegistrationId(self):
q = "SELECT registration_id FROM identities WHERE recipient_id = -1" q = "SELECT registration_id FROM identities WHERE recipient_id = -1"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q) c.execute(q)
result = c.fetchone() result = c.fetchone()
return result[0] if result else None return result[0] if result else None
def storeLocalData(self, registrationId, identityKeyPair): def storeLocalData(self, registrationId, identityKeyPair):
q = "INSERT INTO identities( " + \ q = "INSERT INTO identities( " + \
"recipient_id, registration_id, public_key, private_key) " + \ "recipient_id, registration_id, public_key, private_key) " + \
"VALUES(-1, ?, ?, ?)" "VALUES(-1, ?, ?, ?)"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, c.execute(q,
(registrationId, (registrationId,
identityKeyPair.getPublicKey().getPublicKey().serialize(), identityKeyPair.getPublicKey().getPublicKey().serialize(),
identityKeyPair.getPrivateKey().serialize())) identityKeyPair.getPrivateKey().serialize()))
self.dbConn.commit() self.dbConn.commit()
def saveIdentity(self, recipientId, identityKey): def saveIdentity(self, recipientId, identityKey):
q = "INSERT INTO identities (recipient_id, public_key, trust) " \ q = "INSERT INTO identities (recipient_id, public_key, trust) " \
"VALUES(?, ?, ?)" "VALUES(?, ?, ?)"
c = self.dbConn.cursor() c = self.dbConn.cursor()
if not self.getIdentity(recipientId, identityKey): if not self.getIdentity(recipientId, identityKey):
c.execute(q, (recipientId, c.execute(q, (recipientId,
identityKey.getPublicKey().serialize(), identityKey.getPublicKey().serialize(),
UNDECIDED)) UNDECIDED))
self.dbConn.commit() self.dbConn.commit()
def getIdentity(self, recipientId, identityKey): def getIdentity(self, recipientId, identityKey):
q = "SELECT * FROM identities WHERE recipient_id = ? " \ q = "SELECT * FROM identities WHERE recipient_id = ? " \
"AND public_key = ?" "AND public_key = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, identityKey.getPublicKey().serialize())) c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
result = c.fetchone() result = c.fetchone()
return result is not None return result is not None
def deleteIdentity(self, recipientId, identityKey): def deleteIdentity(self, recipientId, identityKey):
q = "DELETE FROM identities WHERE recipient_id = ? AND public_key = ?" q = "DELETE FROM identities WHERE recipient_id = ? AND public_key = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, c.execute(q, (recipientId,
identityKey.getPublicKey().serialize())) identityKey.getPublicKey().serialize()))
self.dbConn.commit() self.dbConn.commit()
def isTrustedIdentity(self, recipientId, identityKey): def isTrustedIdentity(self, recipientId, identityKey):
q = "SELECT trust FROM identities WHERE recipient_id = ? " \ q = "SELECT trust FROM identities WHERE recipient_id = ? " \
"AND public_key = ?" "AND public_key = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, identityKey.getPublicKey().serialize())) c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
result = c.fetchone() result = c.fetchone()
states = [UNTRUSTED, TRUSTED, UNDECIDED] states = [UNTRUSTED, TRUSTED, UNDECIDED]
if result and result[0] in states: if result and result[0] in states:
return result[0] return result[0]
else: else:
return True return True
def getAllFingerprints(self): def getAllFingerprints(self):
q = "SELECT _id, recipient_id, public_key, trust FROM identities " \ q = "SELECT _id, recipient_id, public_key, trust FROM identities " \
"WHERE recipient_id != -1 ORDER BY recipient_id ASC" "WHERE recipient_id != -1 ORDER BY recipient_id ASC"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q): for row in c.execute(q):
result.append((row[0], row[1], row[2], row[3])) result.append((row[0], row[1], row[2], row[3]))
return result return result
def getFingerprints(self, jid): def getFingerprints(self, jid):
q = "SELECT _id, recipient_id, public_key, trust FROM identities " \ q = "SELECT _id, recipient_id, public_key, trust FROM identities " \
"WHERE recipient_id =? ORDER BY trust ASC" "WHERE recipient_id =? ORDER BY trust ASC"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
c.execute(q, (jid,)) c.execute(q, (jid,))
rows = c.fetchall() rows = c.fetchall()
for row in rows: for row in rows:
result.append((row[0], row[1], row[2], row[3])) result.append((row[0], row[1], row[2], row[3]))
return result return result
def getTrustedFingerprints(self, jid): def getTrustedFingerprints(self, jid):
q = "SELECT public_key FROM identities WHERE recipient_id = ? AND trust = ?" q = "SELECT public_key FROM identities WHERE recipient_id = ? AND trust = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
c.execute(q, (jid, TRUSTED)) c.execute(q, (jid, TRUSTED))
rows = c.fetchall() rows = c.fetchall()
for row in rows: for row in rows:
result.append(row[0]) result.append(row[0])
return result return result
def getUndecidedFingerprints(self, jid): def getUndecidedFingerprints(self, jid):
q = "SELECT trust FROM identities WHERE recipient_id = ? AND trust = ?" q = "SELECT trust FROM identities WHERE recipient_id = ? AND trust = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
c.execute(q, (jid, UNDECIDED)) c.execute(q, (jid, UNDECIDED))
result = c.fetchall() result = c.fetchall()
return result return result
def getNewFingerprints(self, jid): def getNewFingerprints(self, jid):
q = "SELECT _id FROM identities WHERE shown = 0 AND " \ q = "SELECT _id FROM identities WHERE shown = 0 AND " \
"recipient_id = ?" "recipient_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q, (jid,)): for row in c.execute(q, (jid,)):
result.append(row[0]) result.append(row[0])
return result return result
def setShownFingerprints(self, fingerprints): def setShownFingerprints(self, fingerprints):
q = "UPDATE identities SET shown = 1 WHERE _id IN ({})" \ q = "UPDATE identities SET shown = 1 WHERE _id IN ({})" \
.format(', '.join(['?'] * len(fingerprints))) .format(', '.join(['?'] * len(fingerprints)))
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, fingerprints) c.execute(q, fingerprints)
self.dbConn.commit() self.dbConn.commit()
def setTrust(self, identityKey, trust): def setTrust(self, identityKey, trust):
q = "UPDATE identities SET trust = ? WHERE public_key = ?" q = "UPDATE identities SET trust = ? WHERE public_key = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (trust, identityKey.getPublicKey().serialize())) c.execute(q, (trust, identityKey.getPublicKey().serialize()))
self.dbConn.commit() self.dbConn.commit()

View File

@@ -1,87 +1,87 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Tarek Galal <tare2.galal@gmail.com> # Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
from axolotl.state.prekeyrecord import PreKeyRecord from axolotl.state.prekeyrecord import PreKeyRecord
from axolotl.state.prekeystore import PreKeyStore from axolotl.state.prekeystore import PreKeyStore
from axolotl.util.keyhelper import KeyHelper from axolotl.util.keyhelper import KeyHelper
class LitePreKeyStore(PreKeyStore): class LitePreKeyStore(PreKeyStore):
def __init__(self, dbConn): def __init__(self, dbConn):
""" """
:type dbConn: Connection :type dbConn: Connection
""" """
self.dbConn = dbConn self.dbConn = dbConn
def loadPreKey(self, preKeyId): def loadPreKey(self, preKeyId):
q = "SELECT record FROM prekeys WHERE prekey_id = ?" q = "SELECT record FROM prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, )) cursor.execute(q, (preKeyId, ))
result = cursor.fetchone() result = cursor.fetchone()
if not result: if not result:
raise Exception("No such prekeyRecord!") raise Exception("No such prekeyRecord!")
return PreKeyRecord(serialized=result[0]) return PreKeyRecord(serialized=result[0])
def loadPendingPreKeys(self): def loadPendingPreKeys(self):
q = "SELECT record FROM prekeys" q = "SELECT record FROM prekeys"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q) cursor.execute(q)
result = cursor.fetchall() result = cursor.fetchall()
return [PreKeyRecord(serialized=r[0]) for r in result] return [PreKeyRecord(serialized=r[0]) for r in result]
def storePreKey(self, preKeyId, preKeyRecord): def storePreKey(self, preKeyId, preKeyRecord):
q = "INSERT INTO prekeys (prekey_id, record) VALUES(?,?)" q = "INSERT INTO prekeys (prekey_id, record) VALUES(?,?)"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, preKeyRecord.serialize())) cursor.execute(q, (preKeyId, preKeyRecord.serialize()))
self.dbConn.commit() self.dbConn.commit()
def containsPreKey(self, preKeyId): def containsPreKey(self, preKeyId):
q = "SELECT record FROM prekeys WHERE prekey_id = ?" q = "SELECT record FROM prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, )) cursor.execute(q, (preKeyId, ))
return cursor.fetchone() is not None return cursor.fetchone() is not None
def removePreKey(self, preKeyId): def removePreKey(self, preKeyId):
q = "DELETE FROM prekeys WHERE prekey_id = ?" q = "DELETE FROM prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (preKeyId, )) cursor.execute(q, (preKeyId, ))
self.dbConn.commit() self.dbConn.commit()
def getCurrentPreKeyId(self): def getCurrentPreKeyId(self):
q = "SELECT MAX(prekey_id) FROM prekeys" q = "SELECT MAX(prekey_id) FROM prekeys"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q) cursor.execute(q)
return cursor.fetchone()[0] return cursor.fetchone()[0]
def getPreKeyCount(self): def getPreKeyCount(self):
q = "SELECT COUNT(prekey_id) FROM prekeys" q = "SELECT COUNT(prekey_id) FROM prekeys"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q) cursor.execute(q)
return cursor.fetchone()[0] return cursor.fetchone()[0]
def generateNewPreKeys(self, count): def generateNewPreKeys(self, count):
startId = self.getCurrentPreKeyId() + 1 startId = self.getCurrentPreKeyId() + 1
preKeys = KeyHelper.generatePreKeys(startId, count) preKeys = KeyHelper.generatePreKeys(startId, count)
for preKey in preKeys: for preKey in preKeys:
self.storePreKey(preKey.getId(), preKey) self.storePreKey(preKey.getId(), preKey)

View File

@@ -1,143 +1,143 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Tarek Galal <tare2.galal@gmail.com> # Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
from axolotl.state.sessionrecord import SessionRecord from axolotl.state.sessionrecord import SessionRecord
from axolotl.state.sessionstore import SessionStore from axolotl.state.sessionstore import SessionStore
class LiteSessionStore(SessionStore): class LiteSessionStore(SessionStore):
def __init__(self, dbConn): def __init__(self, dbConn):
""" """
:type dbConn: Connection :type dbConn: Connection
""" """
self.dbConn = dbConn self.dbConn = dbConn
def loadSession(self, recipientId, deviceId): def loadSession(self, recipientId, deviceId):
q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?" q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, deviceId)) c.execute(q, (recipientId, deviceId))
result = c.fetchone() result = c.fetchone()
if result: if result:
return SessionRecord(serialized=result[0]) return SessionRecord(serialized=result[0])
else: else:
return SessionRecord() return SessionRecord()
def getSubDeviceSessions(self, recipientId): def getSubDeviceSessions(self, recipientId):
q = "SELECT device_id from sessions WHERE recipient_id = ?" q = "SELECT device_id from sessions WHERE recipient_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, )) c.execute(q, (recipientId, ))
result = c.fetchall() result = c.fetchall()
deviceIds = [r[0] for r in result] deviceIds = [r[0] for r in result]
return deviceIds return deviceIds
def getJidFromDevice(self, device_id): def getJidFromDevice(self, device_id):
q = "SELECT recipient_id from sessions WHERE device_id = ?" q = "SELECT recipient_id from sessions WHERE device_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (device_id, )) c.execute(q, (device_id, ))
result = c.fetchone() result = c.fetchone()
return result[0].decode('utf-8') if result else None return result[0].decode('utf-8') if result else None
def getActiveDeviceTuples(self): def getActiveDeviceTuples(self):
q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1" q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q): for row in c.execute(q):
result.append((row[0].decode('utf-8'), row[1])) result.append((row[0].decode('utf-8'), row[1]))
return result return result
def storeSession(self, recipientId, deviceId, sessionRecord): def storeSession(self, recipientId, deviceId, sessionRecord):
self.deleteSession(recipientId, deviceId) self.deleteSession(recipientId, deviceId)
q = "INSERT INTO sessions(recipient_id, device_id, record) VALUES(?,?,?)" q = "INSERT INTO sessions(recipient_id, device_id, record) VALUES(?,?,?)"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, deviceId, sessionRecord.serialize())) c.execute(q, (recipientId, deviceId, sessionRecord.serialize()))
self.dbConn.commit() self.dbConn.commit()
def containsSession(self, recipientId, deviceId): def containsSession(self, recipientId, deviceId):
q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?" q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute(q, (recipientId, deviceId)) c.execute(q, (recipientId, deviceId))
result = c.fetchone() result = c.fetchone()
return result is not None return result is not None
def deleteSession(self, recipientId, deviceId): def deleteSession(self, recipientId, deviceId):
q = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?" q = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?"
self.dbConn.cursor().execute(q, (recipientId, deviceId)) self.dbConn.cursor().execute(q, (recipientId, deviceId))
self.dbConn.commit() self.dbConn.commit()
def deleteAllSessions(self, recipientId): def deleteAllSessions(self, recipientId):
q = "DELETE FROM sessions WHERE recipient_id = ?" q = "DELETE FROM sessions WHERE recipient_id = ?"
self.dbConn.cursor().execute(q, (recipientId, )) self.dbConn.cursor().execute(q, (recipientId, ))
self.dbConn.commit() self.dbConn.commit()
def getAllSessions(self): def getAllSessions(self):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" q = "SELECT _id, recipient_id, device_id, record, active from sessions"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q): for row in c.execute(q):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4])) result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result return result
def getSessionsFromJid(self, recipientId): def getSessionsFromJid(self, recipientId):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" \ q = "SELECT _id, recipient_id, device_id, record, active from sessions" \
" WHERE recipient_id = ?" " WHERE recipient_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q, (recipientId,)): for row in c.execute(q, (recipientId,)):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4])) result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result return result
def getSessionsFromJids(self, recipientId): def getSessionsFromJids(self, recipientId):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" \ q = "SELECT _id, recipient_id, device_id, record, active from sessions" \
" WHERE recipient_id IN ({})" \ " WHERE recipient_id IN ({})" \
.format(', '.join(['?'] * len(recipientId))) .format(', '.join(['?'] * len(recipientId)))
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q, recipientId): for row in c.execute(q, recipientId):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4])) result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result return result
def setActiveState(self, deviceList, jid): def setActiveState(self, deviceList, jid):
c = self.dbConn.cursor() c = self.dbConn.cursor()
q = "UPDATE sessions SET active = {} " \ q = "UPDATE sessions SET active = {} " \
"WHERE recipient_id = '{}' AND device_id IN ({})" \ "WHERE recipient_id = '{}' AND device_id IN ({})" \
.format(1, jid, ', '.join(['?'] * len(deviceList))) .format(1, jid, ', '.join(['?'] * len(deviceList)))
c.execute(q, deviceList) c.execute(q, deviceList)
q = "UPDATE sessions SET active = {} " \ q = "UPDATE sessions SET active = {} " \
"WHERE recipient_id = '{}' AND device_id NOT IN ({})" \ "WHERE recipient_id = '{}' AND device_id NOT IN ({})" \
.format(0, jid, ', '.join(['?'] * len(deviceList))) .format(0, jid, ', '.join(['?'] * len(deviceList)))
c.execute(q, deviceList) c.execute(q, deviceList)
self.dbConn.commit() self.dbConn.commit()
def getInactiveSessionsKeys(self, recipientId): def getInactiveSessionsKeys(self, recipientId):
q = "SELECT record FROM sessions WHERE active = 0 AND recipient_id = ?" q = "SELECT record FROM sessions WHERE active = 0 AND recipient_id = ?"
c = self.dbConn.cursor() c = self.dbConn.cursor()
result = [] result = []
for row in c.execute(q, (recipientId,)): for row in c.execute(q, (recipientId,)):
public_key = (SessionRecord(serialized=row[0]). public_key = (SessionRecord(serialized=row[0]).
getSessionState().getRemoteIdentityKey(). getSessionState().getRemoteIdentityKey().
getPublicKey()) getPublicKey())
result.append(public_key.serialize()) result.append(public_key.serialize())
return result return result

View File

@@ -1,113 +1,113 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Tarek Galal <tare2.galal@gmail.com> # Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
from axolotl.invalidkeyidexception import InvalidKeyIdException from axolotl.invalidkeyidexception import InvalidKeyIdException
from axolotl.state.signedprekeyrecord import SignedPreKeyRecord from axolotl.state.signedprekeyrecord import SignedPreKeyRecord
from axolotl.state.signedprekeystore import SignedPreKeyStore from axolotl.state.signedprekeystore import SignedPreKeyStore
from axolotl.util.medium import Medium from axolotl.util.medium import Medium
class LiteSignedPreKeyStore(SignedPreKeyStore): class LiteSignedPreKeyStore(SignedPreKeyStore):
def __init__(self, dbConn): def __init__(self, dbConn):
""" """
:type dbConn: Connection :type dbConn: Connection
""" """
self.dbConn = dbConn self.dbConn = dbConn
def loadSignedPreKey(self, signedPreKeyId): def loadSignedPreKey(self, signedPreKeyId):
q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?" q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, )) cursor.execute(q, (signedPreKeyId, ))
result = cursor.fetchone() result = cursor.fetchone()
if not result: if not result:
raise InvalidKeyIdException("No such signedprekeyrecord! %s " % raise InvalidKeyIdException("No such signedprekeyrecord! %s " %
signedPreKeyId) signedPreKeyId)
return SignedPreKeyRecord(serialized=result[0]) return SignedPreKeyRecord(serialized=result[0])
def loadSignedPreKeys(self): def loadSignedPreKeys(self):
q = "SELECT record FROM signed_prekeys" q = "SELECT record FROM signed_prekeys"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, ) cursor.execute(q, )
result = cursor.fetchall() result = cursor.fetchall()
results = [] results = []
for row in result: for row in result:
results.append(SignedPreKeyRecord(serialized=row[0])) results.append(SignedPreKeyRecord(serialized=row[0]))
return results return results
def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord): def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
q = "INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)" q = "INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, signedPreKeyRecord.serialize())) cursor.execute(q, (signedPreKeyId, signedPreKeyRecord.serialize()))
self.dbConn.commit() self.dbConn.commit()
def containsSignedPreKey(self, signedPreKeyId): def containsSignedPreKey(self, signedPreKeyId):
q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?" q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, )) cursor.execute(q, (signedPreKeyId, ))
return cursor.fetchone() is not None return cursor.fetchone() is not None
def removeSignedPreKey(self, signedPreKeyId): def removeSignedPreKey(self, signedPreKeyId):
q = "DELETE FROM signed_prekeys WHERE prekey_id = ?" q = "DELETE FROM signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, )) cursor.execute(q, (signedPreKeyId, ))
self.dbConn.commit() self.dbConn.commit()
def getNextSignedPreKeyId(self): def getNextSignedPreKeyId(self):
result = self.getCurrentSignedPreKeyId() result = self.getCurrentSignedPreKeyId()
if not result: if not result:
return 1 # StartId if no SignedPreKeys exist return 1 # StartId if no SignedPreKeys exist
else: else:
return (result % (Medium.MAX_VALUE - 1)) + 1 return (result % (Medium.MAX_VALUE - 1)) + 1
def getCurrentSignedPreKeyId(self): def getCurrentSignedPreKeyId(self):
q = "SELECT MAX(prekey_id) FROM signed_prekeys" q = "SELECT MAX(prekey_id) FROM signed_prekeys"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q) cursor.execute(q)
result = cursor.fetchone() result = cursor.fetchone()
if not result: if not result:
return None return None
else: else:
return result[0] return result[0]
def getSignedPreKeyTimestamp(self, signedPreKeyId): def getSignedPreKeyTimestamp(self, signedPreKeyId):
q = "SELECT strftime('%s', timestamp) FROM " \ q = "SELECT strftime('%s', timestamp) FROM " \
"signed_prekeys WHERE prekey_id = ?" "signed_prekeys WHERE prekey_id = ?"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (signedPreKeyId, )) cursor.execute(q, (signedPreKeyId, ))
result = cursor.fetchone() result = cursor.fetchone()
if not result: if not result:
raise InvalidKeyIdException("No such signedprekeyrecord! %s " % raise InvalidKeyIdException("No such signedprekeyrecord! %s " %
signedPreKeyId) signedPreKeyId)
return result[0] return result[0]
def removeOldSignedPreKeys(self, timestamp): def removeOldSignedPreKeys(self, timestamp):
q = "DELETE FROM signed_prekeys " \ q = "DELETE FROM signed_prekeys " \
"WHERE timestamp < datetime(?, 'unixepoch')" "WHERE timestamp < datetime(?, 'unixepoch')"
cursor = self.dbConn.cursor() cursor = self.dbConn.cursor()
cursor.execute(q, (timestamp, )) cursor.execute(q, (timestamp, ))
self.dbConn.commit() self.dbConn.commit()

View File

@@ -1,154 +1,154 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright 2015 Tarek Galal <tare2.galal@gmail.com> # Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
# #
# This file is part of Gajim-OMEMO plugin. # This file is part of Gajim-OMEMO plugin.
# #
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify # 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 # 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 # Software Foundation, either version 3 of the License, or (at your option) any
# later version. # later version.
# #
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # 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 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details. # 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 # You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>. # the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
# #
from .db_helpers import user_version from .db_helpers import user_version
class SQLDatabase(): class SQLDatabase():
""" SQL Database """ """ SQL Database """
def __init__(self, dbConn): def __init__(self, dbConn):
""" """
:type dbConn: Connection :type dbConn: Connection
""" """
self.dbConn = dbConn self.dbConn = dbConn
self.createDb() self.createDb()
self.migrateDb() self.migrateDb()
c = self.dbConn.cursor() c = self.dbConn.cursor()
c.execute("PRAGMA synchronous=NORMAL;") c.execute("PRAGMA synchronous=NORMAL;")
c.execute("PRAGMA journal_mode;") c.execute("PRAGMA journal_mode;")
mode = c.fetchone()[0] mode = c.fetchone()[0]
# WAL is a persistent DB mode, don't override it if user has set it # WAL is a persistent DB mode, don't override it if user has set it
if mode != 'wal': if mode != 'wal':
c.execute("PRAGMA journal_mode=MEMORY;") c.execute("PRAGMA journal_mode=MEMORY;")
self.dbConn.commit() self.dbConn.commit()
def createDb(self): def createDb(self):
if user_version(self.dbConn) == 0: if user_version(self.dbConn) == 0:
# Creates # Creates
# IdentityKeyStore # IdentityKeyStore
# PreKeyStore # PreKeyStore
# SignedPreKeyStore # SignedPreKeyStore
# SessionStore # SessionStore
# EncryptionStore # EncryptionStore
create_tables = ''' create_tables = '''
CREATE TABLE IF NOT EXISTS identities ( CREATE TABLE IF NOT EXISTS identities (
_id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id TEXT, _id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id TEXT,
registration_id INTEGER, public_key BLOB, private_key BLOB, registration_id INTEGER, public_key BLOB, private_key BLOB,
next_prekey_id INTEGER, timestamp INTEGER, trust INTEGER, next_prekey_id INTEGER, timestamp INTEGER, trust INTEGER,
shown INTEGER DEFAULT 0); shown INTEGER DEFAULT 0);
CREATE UNIQUE INDEX IF NOT EXISTS CREATE UNIQUE INDEX IF NOT EXISTS
public_key_index ON identities (public_key, recipient_id); public_key_index ON identities (public_key, recipient_id);
CREATE TABLE IF NOT EXISTS prekeys( CREATE TABLE IF NOT EXISTS prekeys(
_id INTEGER PRIMARY KEY AUTOINCREMENT, _id INTEGER PRIMARY KEY AUTOINCREMENT,
prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN, prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN,
record BLOB); record BLOB);
CREATE TABLE IF NOT EXISTS signed_prekeys ( CREATE TABLE IF NOT EXISTS signed_prekeys (
_id INTEGER PRIMARY KEY AUTOINCREMENT, _id INTEGER PRIMARY KEY AUTOINCREMENT,
prekey_id INTEGER UNIQUE, prekey_id INTEGER UNIQUE,
timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB); timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB);
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
_id INTEGER PRIMARY KEY AUTOINCREMENT, _id INTEGER PRIMARY KEY AUTOINCREMENT,
recipient_id TEXT, device_id INTEGER, recipient_id TEXT, device_id INTEGER,
record BLOB, timestamp INTEGER, active INTEGER DEFAULT 1, record BLOB, timestamp INTEGER, active INTEGER DEFAULT 1,
UNIQUE(recipient_id, device_id)); UNIQUE(recipient_id, device_id));
CREATE TABLE IF NOT EXISTS encryption_state ( CREATE TABLE IF NOT EXISTS encryption_state (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
jid TEXT UNIQUE, jid TEXT UNIQUE,
encryption INTEGER encryption INTEGER
); );
''' '''
create_db_sql = """ create_db_sql = """
BEGIN TRANSACTION; BEGIN TRANSACTION;
%s %s
PRAGMA user_version=5; PRAGMA user_version=5;
END TRANSACTION; END TRANSACTION;
""" % (create_tables) """ % (create_tables)
self.dbConn.executescript(create_db_sql) self.dbConn.executescript(create_db_sql)
def migrateDb(self): def migrateDb(self):
""" Migrates the DB """ Migrates the DB
""" """
# Find all double entries and delete them # Find all double entries and delete them
if user_version(self.dbConn) < 2: if user_version(self.dbConn) < 2:
delete_dupes = """ DELETE FROM identities WHERE _id not in ( delete_dupes = """ DELETE FROM identities WHERE _id not in (
SELECT MIN(_id) SELECT MIN(_id)
FROM identities FROM identities
GROUP BY GROUP BY
recipient_id, public_key recipient_id, public_key
); );
""" """
self.dbConn.executescript(""" BEGIN TRANSACTION; self.dbConn.executescript(""" BEGIN TRANSACTION;
%s %s
PRAGMA user_version=2; PRAGMA user_version=2;
END TRANSACTION; END TRANSACTION;
""" % (delete_dupes)) """ % (delete_dupes))
if user_version(self.dbConn) < 3: if user_version(self.dbConn) < 3:
# Create a UNIQUE INDEX so every public key/recipient_id tuple # Create a UNIQUE INDEX so every public key/recipient_id tuple
# can only be once in the db # can only be once in the db
add_index = """ CREATE UNIQUE INDEX IF NOT EXISTS add_index = """ CREATE UNIQUE INDEX IF NOT EXISTS
public_key_index public_key_index
ON identities (public_key, recipient_id); ON identities (public_key, recipient_id);
""" """
self.dbConn.executescript(""" BEGIN TRANSACTION; self.dbConn.executescript(""" BEGIN TRANSACTION;
%s %s
PRAGMA user_version=3; PRAGMA user_version=3;
END TRANSACTION; END TRANSACTION;
""" % (add_index)) """ % (add_index))
if user_version(self.dbConn) < 4: if user_version(self.dbConn) < 4:
# Adds column "active" to the sessions table # Adds column "active" to the sessions table
add_active = """ ALTER TABLE sessions add_active = """ ALTER TABLE sessions
ADD COLUMN active INTEGER DEFAULT 1; ADD COLUMN active INTEGER DEFAULT 1;
""" """
self.dbConn.executescript(""" BEGIN TRANSACTION; self.dbConn.executescript(""" BEGIN TRANSACTION;
%s %s
PRAGMA user_version=4; PRAGMA user_version=4;
END TRANSACTION; END TRANSACTION;
""" % (add_active)) """ % (add_active))
if user_version(self.dbConn) < 5: if user_version(self.dbConn) < 5:
# Adds DEFAULT Timestamp # Adds DEFAULT Timestamp
add_timestamp = """ add_timestamp = """
DROP TABLE signed_prekeys; DROP TABLE signed_prekeys;
CREATE TABLE IF NOT EXISTS signed_prekeys ( CREATE TABLE IF NOT EXISTS signed_prekeys (
_id INTEGER PRIMARY KEY AUTOINCREMENT, _id INTEGER PRIMARY KEY AUTOINCREMENT,
prekey_id INTEGER UNIQUE, prekey_id INTEGER UNIQUE,
timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB); timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB);
ALTER TABLE identities ADD COLUMN shown INTEGER DEFAULT 0; ALTER TABLE identities ADD COLUMN shown INTEGER DEFAULT 0;
UPDATE identities SET shown = 1; UPDATE identities SET shown = 1;
""" """
self.dbConn.executescript(""" BEGIN TRANSACTION; self.dbConn.executescript(""" BEGIN TRANSACTION;
%s %s
PRAGMA user_version=5; PRAGMA user_version=5;
END TRANSACTION; END TRANSACTION;
""" % (add_timestamp)) """ % (add_timestamp))

View File

@@ -19,16 +19,14 @@
import logging import logging
import time 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.ecc.djbec import DjbECPublicKey
from axolotl.identitykey import IdentityKey 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.untrustedidentityexception import UntrustedIdentityException
from axolotl.nosessionexception import NoSessionException
from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage
from axolotl.protocol.whispermessage import WhisperMessage from axolotl.protocol.whispermessage import WhisperMessage
from axolotl.sessionbuilder import SessionBuilder from axolotl.sessionbuilder import SessionBuilder
@@ -36,13 +34,12 @@ from axolotl.sessioncipher import SessionCipher
from axolotl.state.prekeybundle import PreKeyBundle from axolotl.state.prekeybundle import PreKeyBundle
from axolotl.util.keyhelper import KeyHelper 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, from .liteaxolotlstore import (LiteAxolotlStore, DEFAULT_PREKEY_AMOUNT,
MIN_PREKEY_AMOUNT, SPK_CYCLE_TIME, MIN_PREKEY_AMOUNT, SPK_CYCLE_TIME,
SPK_ARCHIVE_TIME) SPK_ARCHIVE_TIME)
log = logging.getLogger('gajim.plugin_system.omemo') log = logging.getLogger('gajim.plugin_system.omemo')
logAxolotl = logging.getLogger('axolotl')
UNTRUSTED = 0 UNTRUSTED = 0
@@ -78,23 +75,24 @@ class OmemoState:
str(self.store.preKeyStore.getPreKeyCount()) + str(self.store.preKeyStore.getPreKeyCount()) +
' PreKeys available') ' 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, sessionBuilder = SessionBuilder(self.store, self.store, self.store,
self.store, recipient_id, device_id) self.store, recipient_id, device_id)
registration_id = self.store.getLocalRegistrationId() 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'][ signedPreKeyPublic = DjbECPublicKey(bundle.spk['key'][1:])
1:]) identityKey = IdentityKey(DjbECPublicKey(bundle.ik[1:]))
identityKey = IdentityKey(DjbECPublicKey(bundle_dict['identityKey'][
1:]))
prekey_bundle = PreKeyBundle( prekey_bundle = PreKeyBundle(
registration_id, device_id, bundle_dict['preKeyId'], preKeyPublic, registration_id, device_id,
bundle_dict['signedPreKeyId'], signedPreKeyPublic, prekey['id'], preKeyPublic,
bundle_dict['signedPreKeySignature'], identityKey) bundle.spk['id'], signedPreKeyPublic,
bundle.spk_signature,
identityKey)
sessionBuilder.processPreKeyBundle(prekey_bundle) sessionBuilder.processPreKeyBundle(prekey_bundle)
return self.get_session_cipher(recipient_id, device_id) return self.get_session_cipher(recipient_id, device_id)
@@ -153,101 +151,85 @@ class OmemoState:
@property @property
def bundle(self): def bundle(self):
self.checkPreKeyAmount() self.checkPreKeyAmount()
prekeys = [
(k.getId(), b64encode(k.getKeyPair().getPublicKey().serialize())) bundle = {'otpks': []}
for k in self.store.loadPreKeys() for k in self.store.loadPreKeys():
] key = k.getKeyPair().getPublicKey().serialize()
bundle['otpks'].append({'key': key, 'id': k.getId()})
identityKeyPair = self.store.getIdentityKeyPair() identityKeyPair = self.store.getIdentityKeyPair()
bundle['ik'] = identityKeyPair.getPublicKey().serialize()
self.cycleSignedPreKey(identityKeyPair) self.cycleSignedPreKey(identityKeyPair)
signedPreKey = self.store.loadSignedPreKey( signedPreKey = self.store.loadSignedPreKey(
self.store.getCurrentSignedPreKeyId()) self.store.getCurrentSignedPreKeyId())
bundle['spk_signature'] = signedPreKey.getSignature()
bundle['spk'] = {'key': signedPreKey.getKeyPair().getPublicKey().serialize(),
'id': signedPreKey.getId()}
result = { return OMEMOBundle(**bundle)
'signedPreKeyId': signedPreKey.getId(),
'signedPreKeyPublic':
b64encode(signedPreKey.getKeyPair().getPublicKey().serialize()),
'signedPreKeySignature': b64encode(signedPreKey.getSignature()),
'identityKey':
b64encode(identityKeyPair.getPublicKey().serialize()),
'prekeys': prekeys
}
return result
def decrypt_msg(self, msg_dict): def decrypt_msg(self, omemo_message, jid):
own_id = self.own_device_id 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') log.info('Received previously sent message by us')
return 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') log.warning('OMEMO message does not contain our device key')
return return
iv = msg_dict['iv'] encrypted_key, prekey = omemo_message.keys[own_id]
sid = msg_dict['sid']
sender_jid = msg_dict['sender_jid']
payload = msg_dict['payload']
encrypted_key = msg_dict['keys'][own_id] if prekey:
try:
key = self.handlePreKeyWhisperMessage(sender_jid, sid,
encrypted_key)
except (InvalidVersionException, InvalidMessageException):
try: try:
key = self.handleWhisperMessage(sender_jid, sid, encrypted_key) key = self.handlePreKeyWhisperMessage(
except (NoSessionException, InvalidMessageException) as e: jid, omemo_message.sid, encrypted_key)
log.warning(e) except Exception as error:
log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' + log.warning(error)
str(sid))
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
return return
except (DuplicateMessageException) as e: else:
log.warning('Duplicate message found ' + str(e.args)) try:
return 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 result = None
log.debug("Decrypted Key Exchange Message") log.debug("Decrypted Key Exchange Message")
else: else:
result = decrypt(key, iv, payload) result = aes_decrypt(key, omemo_message.iv, omemo_message.payload)
log.debug("Decrypted Message => " + result) log.debug("Decrypted Message => %s", result)
return result return result
def create_msg(self, from_jid, jid, plaintext): def create_msg(self, from_jid, jid, plaintext):
key = os.urandom(16)
iv = os.urandom(16)
encrypted_keys = {} encrypted_keys = {}
devices_list = self.device_list_for(jid) devices_list = self.device_list_for(jid)
if len(devices_list) == 0: if not devices_list:
log.error('No known devices') log.error('No known devices')
return return
payload, tag = encrypt(key, iv, plaintext) result = aes_encrypt(plaintext)
key += tag
# Encrypt the message key with for each of receivers devices # Encrypt the message key with for each of receivers devices
for device in devices_list: for device in devices_list:
try: try:
if self.isTrusted(jid, device) == TRUSTED: if self.isTrusted(jid, device) == TRUSTED:
cipher = self.get_session_cipher(jid, device) cipher = self.get_session_cipher(jid, device)
cipher_key = cipher.encrypt(key) cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage) prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey) encrypted_keys[device] = (cipher_key.serialize(), prekey)
else: else:
log.debug('Skipped Device because Trust is: ' + log.debug('Skipped Device because Trust is: %s',
str(self.isTrusted(jid, device))) self.isTrusted(jid, device))
except: except Exception:
log.warning('Failed to find key for device ' + str(device)) log.warning('Failed to find key for device: %s', device)
if len(encrypted_keys) == 0: if not encrypted_keys:
log.error('Encrypted keys empty') log.error('Encrypted keys empty')
raise NoValidSessions('Encrypted keys empty') raise NoValidSessions('Encrypted keys empty')
@@ -257,36 +239,29 @@ class OmemoState:
try: try:
if self.isTrusted(from_jid, device) == TRUSTED: if self.isTrusted(from_jid, device) == TRUSTED:
cipher = self.get_session_cipher(from_jid, device) cipher = self.get_session_cipher(from_jid, device)
cipher_key = cipher.encrypt(key) cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage) prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey) encrypted_keys[device] = (cipher_key.serialize(), prekey)
else: else:
log.debug('Skipped own Device because Trust is: ' + log.debug('Skipped own Device because Trust is: %s',
str(self.isTrusted(from_jid, device))) self.isTrusted(from_jid, device))
except: except Exception:
log.warning('Failed to find key for device ' + str(device)) log.warning('Failed to find key for device: %s', device)
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
'jid': jid,
'iv': iv,
'payload': payload}
log.debug('Finished encrypting message') 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): def create_gc_msg(self, from_jid, jid, plaintext):
key = os.urandom(16)
iv = os.urandom(16)
encrypted_keys = {} encrypted_keys = {}
room = jid room = jid
encrypted_jids = [] encrypted_jids = []
devices_list = self.device_list_for(jid, True) devices_list = self.device_list_for(jid, True)
payload, tag = encrypt(key, iv, plaintext) result = aes_encrypt(plaintext, append_tag=True)
key += tag
for tup in devices_list: for tup in devices_list:
self.get_session_cipher(tup[0], tup[1]) self.get_session_cipher(tup[0], tup[1])
@@ -303,16 +278,15 @@ class OmemoState:
for rid, cipher in self.session_ciphers[jid_to].items(): for rid, cipher in self.session_ciphers[jid_to].items():
try: try:
if self.isTrusted(jid_to, rid) == TRUSTED: if self.isTrusted(jid_to, rid) == TRUSTED:
cipher_key = cipher.encrypt(key) cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage) prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[rid] = (cipher_key.serialize(), prekey) encrypted_keys[rid] = (cipher_key.serialize(), prekey)
else: else:
log.debug('Skipped Device because Trust is: ' + log.debug('Skipped Device because Trust is: %s',
str(self.isTrusted(jid_to, rid))) self.isTrusted(jid_to, rid))
except: except Exception:
log.exception('ERROR:') log.exception('ERROR:')
log.warning('Failed to find key for device ' + log.warning('Failed to find key for device %s', rid)
str(rid))
encrypted_jids.append(jid_to) encrypted_jids.append(jid_to)
my_other_devices = set(self.own_devices) - set({self.own_device_id}) my_other_devices = set(self.own_devices) - set({self.own_device_id})
@@ -321,28 +295,25 @@ class OmemoState:
try: try:
cipher = self.get_session_cipher(from_jid, dev) cipher = self.get_session_cipher(from_jid, dev)
if self.isTrusted(from_jid, dev) == TRUSTED: if self.isTrusted(from_jid, dev) == TRUSTED:
cipher_key = cipher.encrypt(key) cipher_key = cipher.encrypt(result.key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage) prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[dev] = (cipher_key.serialize(), prekey) encrypted_keys[dev] = (cipher_key.serialize(), prekey)
else: else:
log.debug('Skipped own Device because Trust is: ' + log.debug('Skipped own Device because Trust is: %s',
str(self.isTrusted(from_jid, dev))) self.isTrusted(from_jid, dev))
except: except Exception:
log.exception('ERROR:') 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: if not encrypted_keys:
log.error('Encrypted keys empty') log.error('Encrypted keys empty')
raise NoValidSessions('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') 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): def device_list_for(self, jid, gc=False):
""" Return a list of known device ids for the specified jid. """ Return a list of known device ids for the specified jid.
@@ -439,7 +410,7 @@ class OmemoState:
key = sessionCipher.decryptPkmsg(preKeyWhisperMessage) key = sessionCipher.decryptPkmsg(preKeyWhisperMessage)
# Publish new bundle after PreKey has been used # Publish new bundle after PreKey has been used
# for building a new Session # for building a new Session
self.xmpp_con.publish_bundle() self.xmpp_con.set_bundle()
self.add_device(recipient_id, device_id) self.add_device(recipient_id, device_id)
return key return key
except UntrustedIdentityException as e: except UntrustedIdentityException as e:
@@ -495,3 +466,7 @@ class OmemoState:
# Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME # Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME
timestamp = now - SPK_ARCHIVE_TIME timestamp = now - SPK_ARCHIVE_TIME
self.store.removeOldSignedPreKeys(timestamp) self.store.removeOldSignedPreKeys(timestamp)
class NoValidSessions(Exception):
pass

View File

@@ -1,21 +1,18 @@
# -*- coding: utf-8 -*- # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# Copyright 2017 Philipp Hörist <philipp@hoerist.com> # 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 # OMEMO Gajim Plugin is distributed in the hope that it will be useful,
# it under the terms of the GNU General Public License as published by the Free # but WITHOUT ANY WARRANTY; without even the implied warranty of
# Software Foundation, either version 3 of the License, or (at your option) any # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# later version. # GNU General Public License for more details.
#
# 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 <http://www.gnu.org/licenses/>.
# #
# You should have received a copy of the GNU General Public License
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import os import os
import hashlib import hashlib
@@ -32,29 +29,23 @@ from urllib.parse import urlparse, urldefrag
from io import BufferedWriter, FileIO, BytesIO from io import BufferedWriter, FileIO, BytesIO
from gi.repository import GLib from gi.repository import GLib
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gajim.gtk.dialogs import ErrorDialog, YesNoDialog from gajim.gtk.dialogs import ErrorDialog, YesNoDialog
from omemo.gtk.progress import ProgressWindow from omemo.gtk.progress import ProgressWindow
from omemo.backend.aes import aes_decrypt_file
if os.name == 'nt': if os.name == 'nt':
import certifi import certifi
log = logging.getLogger('gajim.plugin_system.omemo.filedecryption') 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') DIRECTORY = os.path.join(configpaths.get('MY_DATA'), 'downloads')
ERROR = False
try: try:
if not os.path.exists(DIRECTORY): if not os.path.exists(DIRECTORY):
os.makedirs(DIRECTORY) os.makedirs(DIRECTORY)
@@ -63,15 +54,6 @@ except Exception:
log.exception('Error') 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: class File:
def __init__(self, url, account): def __init__(self, url, account):
self.account = account self.account = account
@@ -105,7 +87,7 @@ class FileDecryption:
if not self.is_encrypted(file): if not self.is_encrypted(file):
log.info('Url not encrypted: %s', url) log.info('Url not encrypted: %s', url)
return return
print('ADASD')
self.create_paths(file) self.create_paths(file)
if os.path.exists(file.filepath): if os.path.exists(file.filepath):
@@ -181,7 +163,10 @@ class Download:
return return
GLib.idle_add(self.progressbar.set_text, _('Decrypting...')) 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( GLib.idle_add(
self.progressbar.set_text, _('Writing file to harddisk...')) self.progressbar.set_text, _('Writing file to harddisk...'))
@@ -203,11 +188,11 @@ class Download:
log.warning('CERT Verification disabled') log.warning('CERT Verification disabled')
get_request = urlopen(self.file.url, timeout=30, context=context) get_request = urlopen(self.file.url, timeout=30, context=context)
else: else:
cafile = None
if os.name == 'nt': if os.name == 'nt':
get_request = urlopen( cafile = certifi.where()
self.file.url, cafile=certifi.where(), timeout=30) context = ssl.create_default_context(cafile=cafile)
else: get_request = urlopen(self.file.url, timeout=30, context=context)
get_request = urlopen(self.file.url, timeout=30)
size = get_request.info()['Content-Length'] size = get_request.info()['Content-Length']
if not size: if not size:
@@ -241,17 +226,6 @@ class Download:
stream.close() stream.close()
return str(errormsg) 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): def write_file(self, data):
log.info('Writing data to %s', self.file.filepath) log.info('Writing data to %s', self.file.filepath)
try: try:

View File

@@ -1,22 +1,20 @@
''' # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de> # Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
Copyright 2015 Daniel Gultsch <daniel@cgultsch.de> # Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
Copyright 2016 Philipp Hörist <philipp@hoerist.com> #
# 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
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
it under the terms of the GNU General Public License as published by the Free # by the Free Software Foundation; version 3 only.
Software Foundation, either version 3 of the License, or (at your option) any #
later version. # OMEMO Gajim Plugin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR # GNU General Public License for more details.
A PARTICULAR PURPOSE. See the GNU General Public License for more details. #
# You should have received a copy of the GNU General Public License
You should have received a copy of the GNU General Public License along with # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
'''
import binascii import binascii
import logging import logging
@@ -171,7 +169,7 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
def cleardevice_button_clicked_cb(self, button, *args): def cleardevice_button_clicked_cb(self, button, *args):
active = self._ui.get_object('account_combobox').get_active() active = self._ui.get_object('account_combobox').get_active()
account = self.account_store[active][0] 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() self.update_context_list()
def refresh_button_clicked_cb(self, button, *args): def refresh_button_clicked_cb(self, button, *args):

View File

@@ -1,18 +1,18 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # 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 # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging import logging
import binascii import binascii

View File

@@ -1,18 +1,19 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # 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 # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder

View File

@@ -1,16 +1,18 @@
# This file is part of Gajim-OMEMO. # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # 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 # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim-OMEMO. If not, see <http://www.gnu.org/licenses/>. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from collections import namedtuple from collections import namedtuple

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +0,0 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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 <http://www.gnu.org/licenses/>.
# 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):
'''
<item>
<list xmlns='eu.siacs.conversations.axolotl'>
<device id='12345' />
<device id='4223' />
</list>
</item>
'''
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'

View File

@@ -1,46 +1,30 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# #
# 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 # it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only. # 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 # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OMEMO. If not, see <http://www.gnu.org/licenses/>. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# XEP-0384: OMEMO Encryption
import logging
import nbxmpp 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') def delete_nodes(stanza, name, namespace=None):
nodes = stanza.getTags(name, namespace=namespace)
NS_OMEMO = 'eu.siacs.conversations.axolotl' for node in nodes:
NS_DEVICE_LIST = NS_OMEMO + '.devicelist' stanza.delChild(node)
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

View File

@@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# 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 <http://www.gnu.org/licenses/>.
#
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

View File

@@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# 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 <http://www.gnu.org/licenses/>.
#
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

View File

@@ -1,24 +1,20 @@
# -*- coding: utf-8 -*- # Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
''' # Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de> #
Copyright 2015 Daniel Gultsch <daniel@cgultsch.de> # This file is part of OMEMO Gajim Plugin.
Copyright 2016 Philipp Hörist <philipp@hoerist.com> #
# OMEMO Gajim Plugin is free software; you can redistribute it and/or modify
This file is part of Gajim-OMEMO plugin. # 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 # OMEMO Gajim Plugin is distributed in the hope that it will be useful,
Software Foundation, either version 3 of the License, or (at your option) any # but WITHOUT ANY WARRANTY; without even the implied warranty of
later version. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
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 # You should have received a copy of the GNU General Public License
A PARTICULAR PURPOSE. See the GNU General Public License for more details. # along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License along with
the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
'''
import logging import logging
import binascii import binascii
@@ -36,11 +32,12 @@ from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gajim.groupchat_control import GroupchatControl from gajim.groupchat_control import GroupchatControl
from omemo import file_crypto
from omemo.gtk.key import KeyDialog from omemo.gtk.key import KeyDialog
from omemo.gtk.config import OMEMOConfigDialog 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' 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 " \ PROTOBUF_MISSING = "OMEMO can't import Google Protobuf, you can find help in " \
"the GitHub Wiki" "the GitHub Wiki"
@@ -48,12 +45,11 @@ ERROR_MSG = ''
log = logging.getLogger('gajim.plugin_system.omemo') log = logging.getLogger('gajim.plugin_system.omemo')
if log.getEffectiveLevel() == logging.DEBUG:
try: log_axolotl = logging.getLogger('axolotl')
from omemo import file_crypto log_axolotl.setLevel(logging.DEBUG)
except Exception as error: log_axolotl.addHandler(logging.StreamHandler())
log.exception(error) log_axolotl.propagate = False
ERROR_MSG = CRYPTOGRAPHY_MISSING
try: try:
import google.protobuf import google.protobuf
@@ -70,14 +66,10 @@ except Exception as error:
if not ERROR_MSG: if not ERROR_MSG:
try: try:
from omemo.modules import omemo from omemo.modules import omemo
from omemo.modules import omemo_devicelist
except Exception as error: except Exception as error:
log.error(error) log.error(error)
ERROR_MSG = 'Error: %s' % error ERROR_MSG = 'Error: %s' % error
# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init
@unique @unique
class UserMessages(IntEnum): class UserMessages(IntEnum):
@@ -87,7 +79,6 @@ class UserMessages(IntEnum):
class OmemoPlugin(GajimPlugin): class OmemoPlugin(GajimPlugin):
def init(self): def init(self):
""" Init """
if ERROR_MSG: if ERROR_MSG:
self.activatable = False self.activatable = False
self.available_text = ERROR_MSG self.available_text = ERROR_MSG
@@ -99,17 +90,13 @@ class OmemoPlugin(GajimPlugin):
'signed-in': (ged.PRECORE, self.signed_in), 'signed-in': (ged.PRECORE, self.signed_in),
'omemo-new-fingerprint': (ged.PRECORE, self._on_new_fingerprints), 'omemo-new-fingerprint': (ged.PRECORE, self._on_new_fingerprints),
} }
self.modules = [ self.modules = [omemo]
omemo,
omemo_devicelist,
]
self.config_dialog = OMEMOConfigDialog(self) self.config_dialog = OMEMOConfigDialog(self)
self.gui_extension_points = { self.gui_extension_points = {
'hyperlink_handler': (self._file_decryption, None), 'hyperlink_handler': (self._file_decryption, None),
'encrypt' + self.encryption_name: (self._encrypt_message, None), 'encrypt' + self.encryption_name: (self._encrypt_message, None),
'gc_encrypt' + self.encryption_name: ( 'gc_encrypt' + self.encryption_name: (
self._gc_encrypt_message, None), self._gc_encrypt_message, None),
'decrypt': (self._message_received, None),
'send_message' + self.encryption_name: ( 'send_message' + self.encryption_name: (
self.before_sendmessage, None), self.before_sendmessage, None),
'encryption_dialog' + self.encryption_name: ( 'encryption_dialog' + self.encryption_name: (
@@ -198,11 +185,6 @@ class OmemoPlugin(GajimPlugin):
return False return False
return True 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): def _gc_encrypt_message(self, conn, obj, callback):
if conn.name == 'Local': if conn.name == 'Local':
return return
@@ -225,12 +207,11 @@ class OmemoPlugin(GajimPlugin):
@staticmethod @staticmethod
def _encrypt_file_thread(file, callback, *args, **kwargs): def _encrypt_file_thread(file, callback, *args, **kwargs):
encrypted_data, key, iv = file_crypto.encrypt_file( result = aes_encrypt_file(file.get_data(full=True))
file.get_data(full=True))
file.encrypted = True file.encrypted = True
file.size = len(encrypted_data) file.size = len(result.payload)
file.user_data = binascii.hexlify(iv + key).decode('utf-8') file.user_data = binascii.hexlify(result.iv + result.key).decode()
file.data = encrypted_data file.data = result.payload
if file.event.isSet(): if file.event.isSet():
return return
GLib.idle_add(callback, file) GLib.idle_add(callback, file)
@@ -267,7 +248,7 @@ class OmemoPlugin(GajimPlugin):
else: else:
# check if we have devices for the contact # check if we have devices for the contact
if not self.get_omemo(account).device_list_for(contact.jid): if not 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) self.print_message(chat_control, UserMessages.QUERY_DEVICES)
chat_control.sendmessage = False chat_control.sendmessage = False
return return

View File

@@ -1,308 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
#
# 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 <http://www.gnu.org/licenses/>.
#
""" 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'