diff --git a/db_helpers.py b/db_helpers.py new file mode 100644 index 0000000..79050a3 --- /dev/null +++ b/db_helpers.py @@ -0,0 +1,15 @@ +''' Database helper functions ''' + + +def table_exists(db_con, name): + """ Check if the specified table exists in the db. """ + + query = """ SELECT name FROM sqlite_master + WHERE type='table' AND name=?; + """ + return db_con.execute(query, (name, )).fetchone() is not None + + +def user_version(db_con): + """ Return the value of PRAGMA user_version. """ + return db_con.execute('PRAGMA user_version').fetchone()[0] diff --git a/session.py b/session.py new file mode 100644 index 0000000..aaca555 --- /dev/null +++ b/session.py @@ -0,0 +1,57 @@ +import omemo +from slixmpp.plugins.xep_0384.storage import SQLiteDatabase +from omemo.util import generateDeviceID + + +class SessionManager: + def __init__(self, own_jid, db_path): + # Database Inferface + self._store = SQLiteDatabase(db_path) + # OmemoSessionManager + self._sm = omemo.SessionManager(own_jid, self._store, generateDeviceID()) + + def build_session(self, bundle): + self._store.createSession() + + def get_bundle(self): + return self._sm.state.getPublicBundle() + + def set_devicelist(self, device_list, jid=None): + self._sm.newDeviceList(device_list, jid) + + def get_devicelist(self, jid): + return self._sm.getDevices(jid) + + def get_own_device_id(self): + return self._sm.__my_device_id + + def get_own_devices(self): + devices = self._sm.getDevices()['active'] + if self._sm.__my_device_id not in devices: + devices = list(devices) + devices.append(self._sm.__my_device_id) + return devices + + def get_devices_without_session(self, jid): + return self._store.getDevicesWithoutSession(jid) + + def get_trusted_fingerprints(self, jid): + return self._store.getTrustedFingerprints(jid) + + def save_bundle(self, jid, device_id, bundle): + fingerprint = bundle.fingerprint + self._store.storeBundle(jid, device_id, fingerprint) + + def clear_devicelist(self): + return + + def encrypt(self, jids, plaintext, bundles=None, devices=None, callback=None): + return self._sm.encryptMessage(jids, plaintext, bundles, devices, callback) + + def decrypt(self, jid, sid, iv, message, payload, prekey): + if prekey: + return self._sm.decryptMessage(jid, sid, iv, message, payload) + return self._sm.decryptPreKeyMessage(jid, sid, iv, message, payload) + + def buid_session(self, jid, device, bundle, callback): + return self._sm.buildSession(jid, device, bundle, callback) diff --git a/storage.py b/storage.py new file mode 100644 index 0000000..057222f --- /dev/null +++ b/storage.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2018 Philipp Hörist +# +# This file is part of Gajim-OMEMO plugin. +# +# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# the Gajim-OMEMO plugin. If not, see . +# + +import sqlite3 +import pickle +import logging +from collections import namedtuple + +from omemo import Storage +from omemo.x3dhdoubleratchet import X3DHDoubleRatchet +from omemo.signal.doubleratchet.doubleratchet import DoubleRatchet + +from .db_helpers import user_version + +log = logging.getLogger(__name__) + + +class SQLiteDatabase(Storage): + """ SQLite Database """ + + def __init__(self, db_path): + sqlite3.register_adapter(X3DHDoubleRatchet, self._pickle_object) + sqlite3.register_adapter(DoubleRatchet, self._pickle_object) + + sqlite3.register_converter("omemo_state", self._unpickle_object) + sqlite3.register_converter("omemo_session", self._unpickle_object) + self._con = sqlite3.connect(db_path, + detect_types=sqlite3.PARSE_DECLTYPES) + self._con.text_factory = bytes + self._con.row_factory = self.namedtuple_factory + self._create_database() + self._migrate_database() + self._con.execute("PRAGMA synchronous=FULL;") + self._con.commit() + self._own_device_id = None + + def _create_database(self): + if user_version(self._con) == 0: + create_tables = ''' + CREATE TABLE IF NOT EXISTS sessions ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + jid TEXT, + device_id INTEGER, + session omemo_session BLOB, + state omemo_state BLOB, + fingerprint TEXT, + active INTEGER DEFAULT 1, + trust INTEGER DEFAULT 1, + UNIQUE(jid, device_id)); + + CREATE TABLE IF NOT EXISTS state ( + _id INTEGER PRIMARY KEY, + device_id INTEGER, + state omemo_state BLOB + ); + ''' + + create_db_sql = """ + BEGIN TRANSACTION; + %s + PRAGMA user_version=1; + END TRANSACTION; + """ % (create_tables) + self._con.executescript(create_db_sql) + + def _migrate_database(self): + """ Migrates the DB + """ + pass + + @staticmethod + def _pickle_object(session): + return pickle.dumps(session, pickle.HIGHEST_PROTOCOL) + + @staticmethod + def _unpickle_object(session): + return pickle.loads(session) + + @staticmethod + def namedtuple_factory(cursor, row): + fields = [col[0] for col in cursor.description] + Row = namedtuple("Row", fields) + named_row = Row(*row) + return named_row + + def loadState(self): + log.info('Load State') + q = 'SELECT device_id, state FROM state' + result = self._con.execute(q).fetchone() + if result is not None: + self._own_device_id = result.device_id + return {'state': result.state, 'device_id': result.device_id} + + def storeState(self, state, device_id): + log.info('Store State') + self._own_device_id = device_id + q = 'INSERT OR REPLACE INTO state(device_id, state) VALUES(?, ?)' + self._con.execute(q, (device_id, state)) + self._con.commit() + + def loadSession(self, jid, device_id): + log.info('Load Session') + q = 'SELECT session FROM sessions WHERE jid = ? AND device_id = ?' + result = self._con.execute(q, (jid, device_id)).fetchone() + if result is not None: + return result.session + + def storeSession(self, jid, device_id, session): + log.info('Store Session: %s, %s', jid, device_id) + q = 'UPDATE sessions SET session = ? WHERE jid= ? AND device_id = ?' + self._con.execute(q, (session, jid, device_id)) + self._con.commit() + + def createSession(self, jid, device_id, session): + log.info('Create Session') + q = '''INSERT INTO sessions(jid, device_id, session, trust, active) + VALUES (?, ?, ?, 1, 1)''' + self._con.execute(q, (jid, device_id, session)) + self._con.commit() + + def loadActiveDevices(self, jid): + return self.loadDevices(jid, 1) + + def loadInactiveDevices(self, jid): + return self.loadDevices(jid, 0) + + def loadDevices(self, jid, active): + q = 'SELECT device_id FROM sessions WHERE jid = ? AND active = ?' + result = self._con.execute(q, (jid, active)).fetchall() + if result: + devices = [row.device_id for row in result] + state = 'Active' if active else 'Inactive' + log.info('Load %s Devices: %s, %s', state, jid, devices) + return devices + return [] + + def storeActiveDevices(self, jid, devices): + if not devices: + return + # python-omemo returns own device as active, + # dont store it in this table + if self._own_device_id in devices: + devices.remove(self._own_device_id) + log.info('Store Active Devices: %s, %s', jid, devices) + self.storeDevices(jid, devices, 1) + + def storeInactiveDevices(self, jid, devices): + if not devices: + return + log.info('Store Inactive Devices: %s, %s', jid, devices) + self.storeDevices(jid, devices, 0) + + def storeDevices(self, jid, devices, active): + for device_id in devices: + try: + insert = '''INSERT INTO sessions(jid, device_id, active) + VALUES(?, ?, ?)''' + self._con.execute(insert, (jid, device_id, active)) + self._con.commit() + except sqlite3.IntegrityError: + update = '''UPDATE sessions SET active = ? + WHERE jid = ? AND device_id = ?''' + self._con.execute(update, (active, jid, device_id)) + self._con.commit() + + def getDevicesWithoutSession(self, jid): + log.info('Get Devices without Session') + q = '''SELECT device_id FROM sessions + WHERE jid = ? AND (session IS NULL OR session = "")''' + result = self._con.execute(q, (jid,)).fetchall() + if result: + devices = [row.device_id for row in result] + log.info('Get Devices without Session: %s', devices) + return devices + log.info('Get Devices without Session: []') + return [] + + def getTrustedFingerprints(self, jid): + return True + + def storeBundle(self, jid, device_id, fingerprint): + log.info('Store Bundle') + q = '''UPDATE sessions SET fingerprint = ? + WHERE jid = ? and device_id = ?''' + self._con.execute(q, (fingerprint, jid, device_id)) + self._con.commit() + + def isTrusted(self, *args): + return True