pantalaimon: Add a config option to drop old room keys

This patch adds a config option that enables a mode wher a proxy will
only keep the latest room key from a sender in a certain room. This is
useful for bots that use pantalaimon since they are mostly only
interested in the latest messages and don't care about room history.
This commit is contained in:
Damir Jelić 2021-05-28 14:20:18 +02:00
parent 4cdf1be376
commit eabd5f5b51
6 changed files with 71 additions and 4 deletions

View file

@ -12,3 +12,4 @@ Proxy = http://localhost:8080
SSL = False SSL = False
IgnoreVerification = False IgnoreVerification = False
UseKeyring = True UseKeyring = True
DropOldKeys = False

View file

@ -51,6 +51,11 @@ This option configures if a proxy instance should use the OS keyring to store
its own access tokens. The access tokens are required for the daemon to resume its own access tokens. The access tokens are required for the daemon to resume
operation. If this is set to "No", access tokens are stored in the pantalaimon operation. If this is set to "No", access tokens are stored in the pantalaimon
database in plaintext. Defaults to "Yes". database in plaintext. Defaults to "Yes".
.It Cm DropOldKeys
This option configures if a proxy instance should only keep the latest version
of a room key from a certain user around. This effectively means that only newly
incoming messages will be decryptable, the proxy will be unable to decrypt the
room history. Defaults to "No".
.It Cm SearchRequests .It Cm SearchRequests
This option configures if the proxy should make additional HTTP requests to the This option configures if the proxy should make additional HTTP requests to the
server when clients use the search API endpoint. Some data that is required to server when clients use the search API endpoint. Some data that is required to

View file

@ -62,6 +62,13 @@ The following keys are optional in the proxy instance sections:
> operation. If this is set to "No", access tokens are stored in the pantalaimon > operation. If this is set to "No", access tokens are stored in the pantalaimon
> database in plaintext. Defaults to "Yes". > database in plaintext. Defaults to "Yes".
**DropOldKeys**
> This option configures if a proxy instance should only keep the latest version
> of a room key from a certain user around. This effectively means that only newly
> incoming messages will be decryptable, the proxy will be unable to decrypt the
> room history. Defaults to "No".
Aditional to the homeserver section a special section with the name Aditional to the homeserver section a special section with the name
**Default** **Default**
can be used to configure the following values for all homeservers: can be used to configure the following values for all homeservers:
@ -150,4 +157,4 @@ pantalaimon(8)
was written by was written by
Damir Jelić <[poljar@termina.org.uk](mailto:poljar@termina.org.uk)>. Damir Jelić <[poljar@termina.org.uk](mailto:poljar@termina.org.uk)>.
Linux 5.1.3-arch2-1-ARCH - May 8, 2019 Linux 5.11.16-arch1-1 - May 8, 2019

View file

@ -39,6 +39,7 @@ class PanConfigParser(configparser.ConfigParser):
"IndexingBatchSize": "100", "IndexingBatchSize": "100",
"HistoryFetchDelay": "3000", "HistoryFetchDelay": "3000",
"DebugEncryption": "False", "DebugEncryption": "False",
"DropOldKeys": "False",
}, },
converters={ converters={
"address": parse_address, "address": parse_address,
@ -121,6 +122,8 @@ class ServerConfig:
the room history. the room history.
history_fetch_delay (int): The delay between room history fetching history_fetch_delay (int): The delay between room history fetching
requests in seconds. requests in seconds.
drop_old_keys (bool): Should Pantalaimon only keep the most recent
decryption key around.
""" """
name = attr.ib(type=str) name = attr.ib(type=str)
@ -137,6 +140,7 @@ class ServerConfig:
index_encrypted_only = attr.ib(type=bool, default=True) index_encrypted_only = attr.ib(type=bool, default=True)
indexing_batch_size = attr.ib(type=int, default=100) indexing_batch_size = attr.ib(type=int, default=100)
history_fetch_delay = attr.ib(type=int, default=3) history_fetch_delay = attr.ib(type=int, default=3)
drop_old_keys = attr.ib(type=bool, default=False)
@attr.s @attr.s
@ -229,6 +233,7 @@ class PanConfig:
f"already defined before." f"already defined before."
) )
listen_set.add(listen_tuple) listen_set.add(listen_tuple)
drop_old_keys = section.getboolean("DropOldKeys")
server_conf = ServerConfig( server_conf = ServerConfig(
section_name, section_name,
@ -243,6 +248,7 @@ class PanConfig:
index_encrypted_only, index_encrypted_only,
indexing_batch_size, indexing_batch_size,
history_fetch_delay / 1000, history_fetch_delay / 1000,
drop_old_keys,
) )
self.servers[section_name] = server_conf self.servers[section_name] = server_conf

View file

@ -29,6 +29,7 @@ from logbook import StderrHandler
from pantalaimon.config import PanConfig, PanConfigError, parse_log_level from pantalaimon.config import PanConfig, PanConfigError, parse_log_level
from pantalaimon.daemon import ProxyDaemon from pantalaimon.daemon import ProxyDaemon
from pantalaimon.log import logger from pantalaimon.log import logger
from pantalaimon.store import KeyDroppingSqliteStore
from pantalaimon.thread_messages import DaemonResponse from pantalaimon.thread_messages import DaemonResponse
from pantalaimon.ui import UI_ENABLED from pantalaimon.ui import UI_ENABLED
@ -47,6 +48,8 @@ def create_dirs(data_dir, conf_dir):
async def init(data_dir, server_conf, send_queue, recv_queue): async def init(data_dir, server_conf, send_queue, recv_queue):
"""Initialize the proxy and the http server.""" """Initialize the proxy and the http server."""
store_class = KeyDroppingSqliteStore if server_conf.drop_old_keys else None
proxy = ProxyDaemon( proxy = ProxyDaemon(
server_conf.name, server_conf.name,
server_conf.homeserver, server_conf.homeserver,
@ -56,6 +59,7 @@ async def init(data_dir, server_conf, send_queue, recv_queue):
recv_queue=recv_queue.async_q if recv_queue else None, recv_queue=recv_queue.async_q if recv_queue else None,
proxy=server_conf.proxy.geturl() if server_conf.proxy else None, proxy=server_conf.proxy.geturl() if server_conf.proxy else None,
ssl=None if server_conf.ssl is True else False, ssl=None if server_conf.ssl is True else False,
client_store_class=store_class,
) )
# 100 MB max POST size # 100 MB max POST size
@ -101,7 +105,6 @@ async def init(data_dir, server_conf, send_queue, recv_queue):
r"/_matrix/client/r0/profile/{userId}/avatar_url", r"/_matrix/client/r0/profile/{userId}/avatar_url",
proxy.profile, proxy.profile,
), ),
] ]
) )
app.router.add_route("*", "/" + "{proxyPath:.*}", proxy.router) app.router.add_route("*", "/" + "{proxyPath:.*}", proxy.router)

View file

@ -18,10 +18,12 @@ from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import attr import attr
from nio.crypto import TrustState from nio.crypto import TrustState, GroupSessionStore
from nio.store import ( from nio.store import (
Accounts, Accounts,
MegolmInboundSessions,
DeviceKeys, DeviceKeys,
SqliteStore,
DeviceTrustState, DeviceTrustState,
use_database, use_database,
use_database_atomic, use_database_atomic,
@ -29,7 +31,6 @@ from nio.store import (
from peewee import SQL, DoesNotExist, ForeignKeyField, Model, SqliteDatabase, TextField from peewee import SQL, DoesNotExist, ForeignKeyField, Model, SqliteDatabase, TextField
from cachetools import LRUCache from cachetools import LRUCache
MAX_LOADED_MEDIA = 10000 MAX_LOADED_MEDIA = 10000
MAX_LOADED_UPLOAD = 10000 MAX_LOADED_UPLOAD = 10000
@ -452,3 +453,47 @@ class PanStore:
store[account.user_id] = device_store store[account.user_id] = device_store
return store return store
class KeyDroppingSqliteStore(SqliteStore):
@use_database
def save_inbound_group_session(self, session):
"""Save the provided Megolm inbound group session to the database.
Args:
session (InboundGroupSession): The session to save.
"""
account = self._get_account()
assert account
MegolmInboundSessions.delete().where(
MegolmInboundSessions.sender_key == session.sender_key,
MegolmInboundSessions.account == account,
MegolmInboundSessions.room_id == session.room_id,
).execute()
super().save_inbound_group_session(session)
@use_database
def load_inbound_group_sessions(self):
store = super().load_inbound_group_sessions()
return KeyDroppingGroupSessionStore.from_group_session_store(store)
class KeyDroppingGroupSessionStore(GroupSessionStore):
def from_group_session_store(store):
new_store = KeyDroppingGroupSessionStore()
new_store._entries = store._entries
return new_store
def add(self, session) -> bool:
room_id = session.room_id
sender_key = session.sender_key
if session in self._entries[room_id][sender_key].values():
return False
self._entries[room_id][sender_key].clear()
self._entries[room_id][sender_key][session.id] = session
return True