xep_0384: Update with current python-omemo version
- Update the plugin to integrate changes from the omemo library we are using. - Disable handlers for now to be able to gradually see if they are still up-to-date Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
parent
04fb45bdea
commit
e58c50338a
5 changed files with 211 additions and 269 deletions
|
@ -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]
|
15
otpkpolicy.py
Normal file
15
otpkpolicy.py
Normal file
|
@ -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
|
60
plugin.py
60
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)
|
||||
|
||||
|
|
73
session.py
73
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)
|
||||
|
|
317
storage.py
317
storage.py
|
@ -1,205 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2018 Philipp Hörist <philipp@hoerist.com>
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
"""
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue