diff --git a/db_helpers.py b/db_helpers.py deleted file mode 100644 index 79050a3..0000000 --- a/db_helpers.py +++ /dev/null @@ -1,15 +0,0 @@ -''' 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/otpkpolicy.py b/otpkpolicy.py new file mode 100644 index 0000000..2557808 --- /dev/null +++ b/otpkpolicy.py @@ -0,0 +1,15 @@ +""" + Slixmpp: The Slick XMPP Library + + Shamelessly copied from Syndace's python-omemo examples. +""" + +import omemo + + +class KeepingOTPKPolicy(omemo.OTPKPolicy): + @staticmethod + def decideOTPK(preKeyMessages): + # Always keep the OTPK. + # This is the unsafest behaviour possible and should be avoided at all costs. + return True diff --git a/plugin.py b/plugin.py index 0cbc780..8f78f07 100644 --- a/plugin.py +++ b/plugin.py @@ -8,6 +8,9 @@ import logging +import os +import json +import time import base64 import asyncio from slixmpp.plugins.xep_0384.stanza import OMEMO_BASE_NS @@ -20,8 +23,11 @@ log = logging.getLogger(__name__) HAS_OMEMO = True try: - import omemo - from slixmpp.plugins.xep_0384.session import SessionManager + from omemo.util import generateDeviceID + from omemo_backend_signal import BACKEND as SignalBackend + from slixmpp.plugins.xep_0384.session import WrappedSessionManager as SessionManager + from slixmpp.plugins.xep_0384.storage import AsyncInMemoryStorage + from slixmpp.plugins.xep_0384.otpkpolicy import KeepingOTPKPolicy except ImportError as e: HAS_OMEMO = False @@ -46,6 +52,10 @@ def format_fingerprint(fp): return ":".join(splitn(fp, 4)) +# XXX: This should probably be moved in plugins/base.py? +class PluginCouldNotLoad(Exception): pass + + class XEP_0384(BasePlugin): """ @@ -65,20 +75,36 @@ class XEP_0384(BasePlugin): def plugin_init(self): if not self.backend_loaded: - log.debug("xep_0384 cannot be loaded as the backend omemo library " + log.info("xep_0384 cannot be loaded as the backend omemo library " "is not available") return - self._omemo = SessionManager( - self.xmpp.boundjid, - self.cache_dir, + storage = AsyncInMemoryStorage(self.cache_dir) + otpkpolicy = KeepingOTPKPolicy() + backend = SignalBackend + bare_jid = self.xmpp.boundjid.bare + device_id = self._load_device_id(self.cache_dir) + + future = SessionManager.create( + storage, + otpkpolicy, + backend, + bare_jid, + device_id, ) - self._device_id = self._omemo.get_own_device_id() + asyncio.ensure_future(future) - self.xmpp.add_event_handler('pubsub_publish', self._get_device_list) + # XXX: This is crap. Rewrite slixmpp plugin system to use async. + # The issue here is that I can't declare plugin_init as async because + # it's not awaited on. + while not future.done(): + time.sleep(0.1) - asyncio.ensure_future(self._publish_bundle()) - asyncio.ensure_future(self._set_device_list()) + try: + self._omemo = future.result() + except: + log.error("Couldn't load the OMEMO object; ¯\_(ツ)_/¯") + raise PluginCouldNotLoad def plugin_end(self): if not self.backend_loaded: @@ -87,6 +113,20 @@ class XEP_0384(BasePlugin): self.xmpp.del_event_handler('pubsub_publish', self._get_device_list) self.xmpp['xep_0163'].remove_interest(OMEMO_DEVICES_NS) + def _load_device_id(self, cache_dir): + filepath = os.path.join(cache_dir, 'device_id.json') + # Try reading file first, decoding, and if file was empty generate + # new DeviceID + try: + with open(filepath, 'r') as f: + did = json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + did = generateDeviceID() + with open(filepath, 'w') as f: + json.dump(did, f) + + return did + def session_bind(self, _jid): self.xmpp['xep_0163'].add_interest(OMEMO_DEVICES_NS) diff --git a/session.py b/session.py index 3b318e8..70f08de 100644 --- a/session.py +++ b/session.py @@ -1,61 +1,32 @@ -import omemo -from slixmpp.plugins.xep_0384.storage import SQLiteDatabase -from omemo.util import generateDeviceID -import base64 +""" + Wrap omemo.SessionManager object to return futures +""" -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()) +from omemo import SessionManager - def build_session(self, bundle): - self._store.createSession() +from asyncio import Future - def get_bundle(self): - return self._sm.state.getPublicBundle() - def set_devicelist(self, device_list, jid=None): - self._sm.newDeviceList(device_list, jid) +def wrap(method, *args, **kwargs): + future = Future() + promise = method(*args, **kwargs) + promise.then(future.set_result, future.set_exception) + return future - def get_devicelist(self, jid): - return self._sm.getDevices(jid) - def get_own_device_id(self): - return self._sm.__my_device_id +class WrappedSessionManager(SessionManager): + @classmethod + def create(cls, *args, **kwargs) -> Future: + return wrap(super().create, *args, **kwargs) - 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 encryptMessage(self, *args, **kwargs) -> Future: + return wrap(super().encryptMessage, *args, **kwargs) - def get_devices_without_session(self, jid): - return self._store.getDevicesWithoutSession(jid) + def decryptMessage(self, *args, **kwargs) -> Future: + return wrap(super().decryptMessage, *args, **kwargs) - def get_trusted_fingerprints(self, jid): - return self._store.getTrustedFingerprints(jid) + def newDeviceList(self, *args, **kwargs) -> Future: + return wrap(super().newDeviceList, *args, **kwargs) - 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): - iv = base64.b64decode(iv.get_value()) - payload = base64.b64decode(payload.get_value()) - message = base64.b64decode(message) - sid = int(sid) - 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) + def getDevices(self, *args, **kwargs) -> Future: + return wrap(super().getDevices, *args, **kwargs) diff --git a/storage.py b/storage.py index 057222f..e306ea4 100644 --- a/storage.py +++ b/storage.py @@ -1,205 +1,136 @@ -# -*- 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 . -# +""" + Slixmpp: The Slick XMPP Library -import sqlite3 -import pickle -import logging -from collections import namedtuple + Shamelessly copied from Syndace's python-omemo examples. +""" -from omemo import Storage -from omemo.x3dhdoubleratchet import X3DHDoubleRatchet -from omemo.signal.doubleratchet.doubleratchet import DoubleRatchet +import omemo -from .db_helpers import user_version - -log = logging.getLogger(__name__) +import os +import copy +import json -class SQLiteDatabase(Storage): - """ SQLite Database """ +class AsyncInMemoryStorage(omemo.Storage): + def __init__(self, storage_dir): + self.storage_dir = storage_dir + self.__state = None + self.__own_data = None + self.__sessions = {} + self.__devices = {} + self.__trusted = True - def __init__(self, db_path): - sqlite3.register_adapter(X3DHDoubleRatchet, self._pickle_object) - sqlite3.register_adapter(DoubleRatchet, self._pickle_object) + def dump(self): + return copy.deepcopy({ + "state" : self.__state, + "sessions" : self.__sessions, + "devices" : self.__devices + }) - 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 trust(self, trusted): + self.__trusted = trusted - 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: + def loadOwnData(self, callback): + if self.__own_data is None: 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() + filepath = os.path.join(self.storage_dir, 'own_data.json') + with open(filepath, 'r') as f: + self.__own_data = json.load(f) + except OSError: + return callback(True, None) + except json.JSONDecodeError as e: + return callback(False, e) - 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 [] + return callback(True, self.__own_data) - 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): + def storeOwnData(self, callback, own_bare_jid, own_device_id): + self.__own_data = { + 'own_bare_jid': own_bare_jid, + 'own_device_id': own_device_id, + } + + try: + filepath = os.path.join(self.storage_dir, 'own_data.json') + with open(filepath, 'w') as f: + json.dump(self.__own_data, f) + return callback(True, None) + except Exception as e: + return callback(False, e) + + def loadState(self, callback): + if self.__state is None: + try: + filepath = os.path.join(self.storage_dir, 'omemo.json') + with open(filepath, 'r') as f: + self.__state = json.load(f) + except OSError: + return callback(True, None) + except json.JSONDecodeError as e: + return callback(False, e) + + return callback(True, self.__state) + + def storeState(self, callback, state): + self.__state = state + try: + filepath = os.path.join(self.storage_dir, 'omemo.json') + with open(filepath, 'w') as f: + json.dump(self.__state, f) + return callback(True, None) + except Exception as e: + return callback(False, e) + + def loadSession(self, callback, bare_jid, device_id): + callback(True, self.__sessions.get(bare_jid, {}).get(device_id, None)) + + def storeSession(self, callback, bare_jid, device_id, session): + self.__sessions[bare_jid] = self.__sessions.get(bare_jid, {}) + self.__sessions[bare_jid][device_id] = session + + callback(True, None) + + def loadActiveDevices(self, callback, bare_jid): + if self.__devices is None: + try: + filepath = os.path.join(self.storage_dir, 'devices.json') + with open(filepath, 'r') as f: + self.__devices = json.load(f) + except OSError: + return callback(True, None) + except json.JSONDecodeError as e: + return callback(False, e) + + return callback(True, self.__devices.get(bare_jid, {}).get("active", [])) + + def storeActiveDevices(self, callback, bare_jid, devices): + self.__devices[bare_jid] = self.__devices.get(bare_jid, {}) + self.__devices[bare_jid]["active"] = devices + + try: + filepath = os.path.join(self.storage_dir, 'devices.json') + with open(filepath, 'w') as f: + json.dump(self.__devices, f) + return callback(True, None) + except Exception as e: + return callback(False, e) + + + def storeInactiveDevices(self, callback, bare_jid, devices): + self.__devices[bare_jid] = self.__devices.get(bare_jid, {}) + self.__devices[bare_jid]["inactive"] = devices + + callback(True, None) + + def isTrusted(self, callback, bare_jid, device): + result = False + + if self.__trusted == True: + result = True + else: + result = bare_jid in self.__trusted and device in self.__trusted[bare_jid] + + callback(True, result) + + @property + def is_async(self): return True