mirror of
https://github.com/matrix-org/pantalaimon.git
synced 2025-01-25 23:06:04 -05:00
658 lines
24 KiB
Python
658 lines
24 KiB
Python
# Copyright 2019 The Matrix.org Foundation CIC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from importlib import util
|
|
|
|
UI_ENABLED = (
|
|
util.find_spec("gi") is not None
|
|
and util.find_spec("gi.repository") is not None
|
|
and util.find_spec("pydbus") is not None
|
|
)
|
|
|
|
if UI_ENABLED:
|
|
from collections import defaultdict
|
|
from queue import Empty
|
|
|
|
import attr
|
|
import dbus
|
|
import notify2
|
|
from gi.repository import GLib
|
|
from pydbus import SessionBus
|
|
from pydbus.generic import signal
|
|
|
|
from nio import RoomKeyRequest, RoomKeyRequestCancellation
|
|
|
|
from pantalaimon.log import logger
|
|
from pantalaimon.thread_messages import (
|
|
AcceptSasMessage,
|
|
CancelSasMessage,
|
|
CancelSendingMessage,
|
|
ConfirmSasMessage,
|
|
DaemonResponse,
|
|
DeviceBlacklistMessage,
|
|
DeviceUnblacklistMessage,
|
|
DeviceUnverifyMessage,
|
|
DeviceVerifyMessage,
|
|
ExportKeysMessage,
|
|
ImportKeysMessage,
|
|
InviteSasSignal,
|
|
SasDoneSignal,
|
|
SendAnywaysMessage,
|
|
ShowSasSignal,
|
|
StartSasMessage,
|
|
UnverifiedDevicesSignal,
|
|
UpdateDevicesMessage,
|
|
UpdateUsersMessage,
|
|
KeyRequestMessage,
|
|
ContinueKeyShare,
|
|
CancelKeyShare,
|
|
)
|
|
|
|
UI_ENABLED = True
|
|
|
|
class IdCounter:
|
|
def __init__(self):
|
|
self._message_id = 0
|
|
|
|
@property
|
|
def message_id(self):
|
|
ret = self._message_id
|
|
self._message_id += 1
|
|
|
|
return ret
|
|
|
|
class Control:
|
|
"""
|
|
<node>
|
|
<interface name='org.pantalaimon1.control'>
|
|
<method name='ListServers'>
|
|
<arg type='a{sa(ss)}' name='servers' direction='out'/>
|
|
</method>
|
|
|
|
<method name='ExportKeys'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='file_path' direction='in'/>
|
|
<arg type='s' name='passphrase' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='ImportKeys'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='file_path' direction='in'/>
|
|
<arg type='s' name='passphrase' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='SendAnyways'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='room_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='CancelSending'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='room_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<signal name="Response">
|
|
<arg direction="out" type="i" name="id"/>
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="a{ss}" name="message"/>
|
|
</signal>
|
|
|
|
<signal name="UnverifiedDevices">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="room_id"/>
|
|
<arg direction="out" type="s" name="room_display_name"/>
|
|
</signal>
|
|
|
|
</interface>
|
|
</node>
|
|
"""
|
|
|
|
Response = signal()
|
|
UnverifiedDevices = signal()
|
|
|
|
def __init__(self, queue, server_list, id_counter):
|
|
self.queue = queue
|
|
self.server_list = server_list
|
|
self.id_counter = id_counter
|
|
self.users = defaultdict(set)
|
|
|
|
def update_users(self, message):
|
|
self.users[message.server].add((message.user_id, message.device_id))
|
|
|
|
@property
|
|
def message_id(self):
|
|
return self.id_counter.message_id
|
|
|
|
def ListServers(self):
|
|
"""Return the list of pan users."""
|
|
return self.users
|
|
|
|
def ExportKeys(self, pan_user, filepath, passphrase):
|
|
message = ExportKeysMessage(self.message_id, pan_user, filepath, passphrase)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def ImportKeys(self, pan_user, filepath, passphrase):
|
|
message = ImportKeysMessage(self.message_id, pan_user, filepath, passphrase)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def SendAnyways(self, pan_user, room_id):
|
|
message = SendAnywaysMessage(self.message_id, pan_user, room_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def CancelSending(self, pan_user, room_id):
|
|
message = CancelSendingMessage(self.message_id, pan_user, room_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
class Devices:
|
|
"""
|
|
<node>
|
|
<interface name='org.pantalaimon1.devices'>
|
|
<method name='List'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='aa{ss}' name='devices' direction='out'/>
|
|
</method>
|
|
|
|
<method name='ListUserDevices'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='aa{ss}' name='devices' direction='out'/>
|
|
</method>
|
|
|
|
<method name='Verify'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='Unverify'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='Blacklist'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='Unblacklist'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='StartKeyVerification'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='CancelKeyVerification'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='AcceptKeyVerification'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='ConfirmKeyVerification'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<signal name="VerificationInvite">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="user_id"/>
|
|
<arg direction="out" type="s" name="device_id"/>
|
|
<arg direction="out" type="s" name="transaction_id"/>
|
|
</signal>
|
|
|
|
<signal name="VerificationString">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="user_id"/>
|
|
<arg direction="out" type="s" name="device_id"/>
|
|
<arg direction="out" type="s" name="transaction_id"/>
|
|
<arg direction="out" type="a(ss)" name="emoji"/>
|
|
</signal>
|
|
|
|
<signal name="VerificationCancel">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="user_id"/>
|
|
<arg direction="out" type="s" name="device_id"/>
|
|
<arg direction="out" type="s" name="transaction_id"/>
|
|
<arg direction="out" type="s" name="reason"/>
|
|
<arg direction="out" type="s" name="code"/>
|
|
</signal>
|
|
|
|
<signal name="VerificationDone">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="user_id"/>
|
|
<arg direction="out" type="s" name="device_id"/>
|
|
<arg direction="out" type="s" name="transaction_id"/>
|
|
</signal>
|
|
|
|
<method name='ContinueKeyShare'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<method name='CancelKeyShare'>
|
|
<arg type='s' name='pan_user' direction='in'/>
|
|
<arg type='s' name='user_id' direction='in'/>
|
|
<arg type='s' name='device_id' direction='in'/>
|
|
<arg type='u' name='id' direction='out'/>
|
|
</method>
|
|
|
|
<signal name="KeyRequest">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="user_id"/>
|
|
<arg direction="out" type="s" name="device_id"/>
|
|
<arg direction="out" type="s" name="request_id"/>
|
|
</signal>
|
|
|
|
<signal name="KeyRequestCancel">
|
|
<arg direction="out" type="s" name="pan_user"/>
|
|
<arg direction="out" type="s" name="user_id"/>
|
|
<arg direction="out" type="s" name="device_id"/>
|
|
<arg direction="out" type="s" name="request_id"/>
|
|
</signal>
|
|
|
|
</interface>
|
|
</node>
|
|
"""
|
|
|
|
VerificationInvite = signal()
|
|
VerificationCancel = signal()
|
|
VerificationString = signal()
|
|
VerificationDone = signal()
|
|
|
|
KeyRequest = signal()
|
|
KeyRequestCancel = signal()
|
|
|
|
def __init__(self, queue, id_counter):
|
|
self.device_list = dict()
|
|
self.queue = queue
|
|
self.id_counter = id_counter
|
|
|
|
self.key_requests = dict()
|
|
|
|
@property
|
|
def message_id(self):
|
|
return self.id_counter.message_id
|
|
|
|
def List(self, pan_user):
|
|
device_store = self.device_list.get(pan_user, None)
|
|
|
|
if not device_store:
|
|
return []
|
|
|
|
device_list = [
|
|
device
|
|
for device_list in device_store.values()
|
|
for device in device_list.values()
|
|
]
|
|
|
|
return device_list
|
|
|
|
def ListUserDevices(self, pan_user, user_id):
|
|
device_store = self.device_list.get(pan_user, None)
|
|
|
|
if not device_store:
|
|
return []
|
|
|
|
device_list = device_store.get(user_id, None)
|
|
|
|
if not device_list:
|
|
return []
|
|
|
|
return device_list.values()
|
|
|
|
def Verify(self, pan_user, user_id, device_id):
|
|
message = DeviceVerifyMessage(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def Unverify(self, pan_user, user_id, device_id):
|
|
message = DeviceUnverifyMessage(
|
|
self.message_id, pan_user, user_id, device_id
|
|
)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def Blacklist(self, pan_user, user_id, device_id):
|
|
message = DeviceBlacklistMessage(
|
|
self.message_id, pan_user, user_id, device_id
|
|
)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def Unblacklist(self, pan_user, user_id, device_id):
|
|
message = DeviceUnblacklistMessage(
|
|
self.message_id, pan_user, user_id, device_id
|
|
)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def StartKeyVerification(self, pan_user, user_id, device_id):
|
|
message = StartSasMessage(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def CancelKeyVerification(self, pan_user, user_id, device_id):
|
|
message = CancelSasMessage(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def ConfirmKeyVerification(self, pan_user, user_id, device_id):
|
|
message = ConfirmSasMessage(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def AcceptKeyVerification(self, pan_user, user_id, device_id):
|
|
message = AcceptSasMessage(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def ContinueKeyShare(self, pan_user, user_id, device_id):
|
|
message = ContinueKeyShare(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def CancelKeyShare(self, pan_user, user_id, device_id):
|
|
message = CancelKeyShare(self.message_id, pan_user, user_id, device_id)
|
|
self.queue.put(message)
|
|
return message.message_id
|
|
|
|
def update_devices(self, message):
|
|
if message.pan_user not in self.device_list:
|
|
self.device_list[message.pan_user] = defaultdict(dict)
|
|
|
|
device_list = self.device_list.get(message.pan_user)
|
|
|
|
for user_devices in message.devices.values():
|
|
for device in user_devices.values():
|
|
if device["deleted"]:
|
|
try:
|
|
device_list[device["user_id"]].pop(device["device_id"])
|
|
except KeyError:
|
|
pass
|
|
continue
|
|
|
|
device.pop("deleted")
|
|
device_list[device["user_id"]][device["device_id"]] = device
|
|
|
|
def update_key_requests(self, message):
|
|
# type: (KeyRequestMessage) -> None
|
|
event = message.event
|
|
|
|
if isinstance(event, RoomKeyRequest):
|
|
self.key_requests[event.request_id] = event
|
|
self.KeyRequest(
|
|
message.pan_user,
|
|
event.sender,
|
|
event.requesting_device_id,
|
|
event.request_id,
|
|
)
|
|
|
|
elif isinstance(event, RoomKeyRequestCancellation):
|
|
self.key_requests.pop(event.request_id, None)
|
|
self.KeyRequestCancel(
|
|
message.pan_user,
|
|
event.sender,
|
|
event.requesting_device_id,
|
|
event.request_id,
|
|
)
|
|
|
|
@attr.s
|
|
class GlibT:
|
|
receive_queue = attr.ib()
|
|
send_queue = attr.ib()
|
|
data_dir = attr.ib()
|
|
server_list = attr.ib()
|
|
config = attr.ib()
|
|
|
|
loop = attr.ib(init=False)
|
|
store = attr.ib(init=False)
|
|
users = attr.ib(init=False)
|
|
devices = attr.ib(init=False)
|
|
bus = attr.ib(init=False)
|
|
control_if = attr.ib(init=False)
|
|
device_if = attr.ib(init=False)
|
|
notifications = attr.ib(type=bool, default=False, init=False)
|
|
|
|
def __attrs_post_init__(self):
|
|
self.loop = None
|
|
|
|
id_counter = IdCounter()
|
|
|
|
self.control_if = Control(self.send_queue, self.server_list, id_counter)
|
|
self.device_if = Devices(self.send_queue, id_counter)
|
|
|
|
self.bus = SessionBus()
|
|
self.bus.publish("org.pantalaimon1", self.control_if, self.device_if)
|
|
|
|
def unverified_notification(self, message):
|
|
notificaton = notify2.Notification(
|
|
"Unverified devices.",
|
|
message=(
|
|
f"There are unverified devices in the room "
|
|
f"{message.room_display_name}."
|
|
),
|
|
)
|
|
notificaton.set_category("im")
|
|
|
|
def send_cb(notification, action_key, user_data):
|
|
message = user_data
|
|
self.control_if.SendAnyways(message.pan_user, message.room_id)
|
|
|
|
def cancel_cb(notification, action_key, user_data):
|
|
message = user_data
|
|
self.control_if.CancelSending(message.pan_user, message.room_id)
|
|
|
|
if "actions" in notify2.get_server_caps():
|
|
notificaton.add_action("send", "Send anyways", send_cb, message)
|
|
notificaton.add_action("cancel", "Cancel sending", cancel_cb, message)
|
|
|
|
notificaton.show()
|
|
|
|
def sas_invite_notification(self, message):
|
|
notificaton = notify2.Notification(
|
|
"Key verification invite",
|
|
message=(
|
|
f"{message.user_id} via {message.device_id} has started "
|
|
f"a key verification process."
|
|
),
|
|
)
|
|
notificaton.set_category("im")
|
|
|
|
def accept_cb(notification, action_key, user_data):
|
|
message = user_data
|
|
self.device_if.AcceptKeyVerification(
|
|
message.pan_user, message.user_id, message.device_id
|
|
)
|
|
|
|
def cancel_cb(notification, action_key, user_data):
|
|
message = user_data
|
|
self.device_if.CancelKeyVerification(
|
|
message.pan_user, message.user_id, message.device_id
|
|
)
|
|
|
|
if "actions" in notify2.get_server_caps():
|
|
notificaton.add_action("accept", "Accept", accept_cb, message)
|
|
notificaton.add_action("cancel", "Cancel", cancel_cb, message)
|
|
|
|
notificaton.show()
|
|
|
|
def sas_show_notification(self, message):
|
|
emojis = [x[0] for x in message.emoji]
|
|
|
|
emoji_str = " ".join(emojis)
|
|
|
|
notificaton = notify2.Notification(
|
|
"Short authentication string",
|
|
message=(
|
|
f"Short authentication string for the key verification of"
|
|
f" {message.user_id} via {message.device_id}:\n"
|
|
f"{emoji_str}"
|
|
),
|
|
)
|
|
notificaton.set_category("im")
|
|
|
|
def confirm_cb(notification, action_key, user_data):
|
|
message = user_data
|
|
self.device_if.ConfirmKeyVerification(
|
|
message.pan_user, message.user_id, message.device_id
|
|
)
|
|
|
|
def cancel_cb(notification, action_key, user_data):
|
|
message = user_data
|
|
self.device_if.CancelKeyVerification(
|
|
message.pan_user, message.user_id, message.device_id
|
|
)
|
|
|
|
if "actions" in notify2.get_server_caps():
|
|
notificaton.add_action("confirm", "Confirm", confirm_cb, message)
|
|
notificaton.add_action("cancel", "Cancel", cancel_cb, message)
|
|
|
|
notificaton.show()
|
|
|
|
def sas_done_notification(self, message):
|
|
notificaton = notify2.Notification(
|
|
"Device successfully verified.",
|
|
message=(
|
|
f"Device {message.device_id} of user {message.user_id} "
|
|
f"successfully verified."
|
|
),
|
|
)
|
|
notificaton.set_category("im")
|
|
notificaton.show()
|
|
|
|
def message_callback(self):
|
|
try:
|
|
message = self.receive_queue.get_nowait()
|
|
except Empty:
|
|
return True
|
|
|
|
logger.debug(f"UI loop received message {message}")
|
|
|
|
if isinstance(message, UpdateDevicesMessage):
|
|
self.device_if.update_devices(message)
|
|
|
|
elif isinstance(message, UpdateUsersMessage):
|
|
self.control_if.update_users(message)
|
|
|
|
elif isinstance(message, UnverifiedDevicesSignal):
|
|
self.control_if.UnverifiedDevices(
|
|
message.pan_user, message.room_id, message.room_display_name
|
|
)
|
|
|
|
if self.notifications:
|
|
self.unverified_notification(message)
|
|
|
|
elif isinstance(message, InviteSasSignal):
|
|
self.device_if.VerificationInvite(
|
|
message.pan_user,
|
|
message.user_id,
|
|
message.device_id,
|
|
message.transaction_id,
|
|
)
|
|
|
|
if self.notifications:
|
|
self.sas_invite_notification(message)
|
|
|
|
elif isinstance(message, ShowSasSignal):
|
|
self.device_if.VerificationString(
|
|
message.pan_user,
|
|
message.user_id,
|
|
message.device_id,
|
|
message.transaction_id,
|
|
message.emoji,
|
|
)
|
|
|
|
if self.notifications:
|
|
self.sas_show_notification(message)
|
|
|
|
elif isinstance(message, SasDoneSignal):
|
|
self.device_if.VerificationDone(
|
|
message.pan_user,
|
|
message.user_id,
|
|
message.device_id,
|
|
message.transaction_id,
|
|
)
|
|
|
|
if self.notifications:
|
|
self.sas_done_notification(message)
|
|
|
|
elif isinstance(message, DaemonResponse):
|
|
self.control_if.Response(
|
|
message.message_id,
|
|
message.pan_user,
|
|
{"code": message.code, "message": message.message},
|
|
)
|
|
|
|
elif isinstance(message, KeyRequestMessage):
|
|
self.device_if.update_key_requests(message)
|
|
|
|
self.receive_queue.task_done()
|
|
return True
|
|
|
|
def run(self):
|
|
self.loop = GLib.MainLoop()
|
|
|
|
if self.config.notifications:
|
|
try:
|
|
notify2.init("pantalaimon", mainloop=self.loop)
|
|
self.notifications = True
|
|
except dbus.DBusException:
|
|
logger.error(
|
|
"Notifications are enabled but no notification "
|
|
"server could be found, disabling notifications."
|
|
)
|
|
self.notifications = False
|
|
|
|
GLib.timeout_add(100, self.message_callback)
|
|
if not self.loop:
|
|
return
|
|
|
|
self.loop.run()
|
|
|
|
def stop(self):
|
|
if self.loop:
|
|
self.loop.quit()
|
|
self.loop = None
|