added trusted / untrusted peers mechanism

This commit is contained in:
study-faraphel 2025-01-05 12:46:21 +01:00
parent 84f76c0892
commit f28454c2b2
15 changed files with 140 additions and 13 deletions

4
.gitignore vendored
View file

@ -1,3 +1,7 @@
# IDE
.idea .idea
cmake-build-* cmake-build-*
# Local
assets assets
storage

View file

@ -2,11 +2,13 @@ from source import packets
from source.behaviors.events import base from source.behaviors.events import base
class AudioEvent(base.BaseEvent): class AudioEvent(base.BaseTrustedEvent):
""" """
Event reacting to receiving audio data. Event reacting to receiving audio data.
""" """
def handle(self, packet: packets.AudioPacket, address: tuple): def handle(self, packet: packets.AudioPacket, address: tuple):
super().handle(packet, address)
# add the audio chunk to the buffer of audio packet to play # add the audio chunk to the buffer of audio packet to play
self.manager.audio.add_audio(packet) self.manager.audio.add_audio(packet)

View file

@ -3,12 +3,14 @@ from source.behaviors import roles
from source.behaviors.events import base from source.behaviors.events import base
class KeyEvent(base.BaseEvent): class KeyEvent(base.BaseTrustedEvent):
""" """
Event reacting to a machine sending us their secret key Event reacting to a machine sending us their secret key
""" """
def handle(self, packet: packets.KeyPacket, address: tuple): def handle(self, packet: packets.KeyPacket, address: tuple):
super().handle(packet, address)
# check if we are a slave # check if we are a slave
if not isinstance(self.manager.role.current, roles.SlaveRole): if not isinstance(self.manager.role.current, roles.SlaveRole):
return return

View file

@ -8,8 +8,16 @@ class PeerEvent(base.BaseEvent):
""" """
def handle(self, packet: packets.PeerPacket, address: tuple): def handle(self, packet: packets.PeerPacket, address: tuple):
# ignore peers with a banned key
if self.manager.communication.is_peer_banned(packet.public_key):
return
# TODO(Faraphel): SHOULD NOT BE TRUSTED AUTOMATICALLY !
self.manager.communication.trust_peer(packet.public_key)
# update our peers database to add new peer information # update our peers database to add new peer information
self.manager.peer.peers[address] = structures.Peer( self.manager.peer.peers[address] = structures.Peer(
public_key=packet.public_key, public_key=packet.public_key,
master=packet.master, master=packet.master,
trusted=self.manager.communication.is_peer_trusted(packet.public_key)
) )

View file

@ -4,12 +4,14 @@ from source.behaviors.events import base
from source.utils.crypto.type import CipherType from source.utils.crypto.type import CipherType
class RequestKeyEvent(base.BaseEvent): class RequestKeyEvent(base.BaseTrustedEvent):
""" """
Event reacting to a machine trying to get our secret symmetric key for secure communication Event reacting to a machine trying to get our secret symmetric key for secure communication
""" """
def handle(self, packet: packets.RequestKeyPacket, address: tuple): def handle(self, packet: packets.RequestKeyPacket, address: tuple):
super().handle(packet, address)
# check if we are a master # check if we are a master
if not isinstance(self.manager.role.current, roles.MasterRole): if not isinstance(self.manager.role.current, roles.MasterRole):
return return
@ -18,5 +20,3 @@ class RequestKeyEvent(base.BaseEvent):
packet = packets.KeyPacket(self.manager.role.current.secret_key) packet = packets.KeyPacket(self.manager.role.current.secret_key)
# send it back to the slave # send it back to the slave
self.manager.communication.send(packet, CipherType.RSA, address) self.manager.communication.send(packet, CipherType.RSA, address)
# TODO(Faraphel): check if we trust the slave ? in a list of trusted public / private key ?

View file

@ -0,0 +1,18 @@
import abc
from source import packets
from source.behaviors.events.base import BaseEvent
from source.error import UntrustedPeerException
class BaseTrustedEvent(BaseEvent, abc.ABC):
"""
Event that can only be triggered if the distant peer is trusted
"""
def handle(self, packet: packets.base.BasePacket, address: tuple) -> None:
# get the peer that sent the message
peer = self.manager.peer.peers.get(address)
# check if it is trusted
if peer is None or not peer.trusted:
raise UntrustedPeerException(peer)

View file

@ -1 +1,2 @@
from .BaseEvent import BaseEvent from .BaseEvent import BaseEvent
from .BaseTrustedEvent import BaseTrustedEvent

View file

@ -27,7 +27,7 @@ class MasterRole(base.BaseActiveRole):
# prepare the audio file that will be streamed # prepare the audio file that will be streamed
# TODO(Faraphel): use another audio source # TODO(Faraphel): use another audio source
self.audio = pydub.AudioSegment.from_file("../assets/Queen - Another One Bites the Dust.mp3") self.audio = pydub.AudioSegment.from_file("./assets/Queen - Another One Bites the Dust.mp3")
self.play_time = datetime.now() self.play_time = datetime.now()
# calculate the number of bytes per milliseconds in the audio # calculate the number of bytes per milliseconds in the audio

View file

@ -0,0 +1,8 @@
import typing
from source import structures
class UntrustedPeerException(Exception):
def __init__(self, peer: typing.Optional[structures.Peer]):
super().__init__(f"Peer not trusted: {peer}")

1
source/error/__init__.py Normal file
View file

@ -0,0 +1 @@
from .UntrustedPeerException import UntrustedPeerException

View file

@ -1,3 +1,5 @@
import hashlib
import json
import socket import socket
import typing import typing
import zlib import zlib
@ -35,8 +37,27 @@ class CommunicationManager:
# create a dictionary to hold the types of packets and their headers. # create a dictionary to hold the types of packets and their headers.
self.packet_types: bidict.bidict[bytes, typing.Type[packets.base.BasePacket]] = bidict.bidict() self.packet_types: bidict.bidict[bytes, typing.Type[packets.base.BasePacket]] = bidict.bidict()
# create a private and public key for RSA communication # load or create a private and public key for asymmetric communication
private_key_path = self.manager.storage / "private_key.der"
public_key_path = self.manager.storage / "public_key.der"
if public_key_path.exists() and private_key_path.exists():
self.private_key = private_key_path.read_bytes()
self.public_key = public_key_path.read_bytes()
else:
self.private_key, self.public_key = rsa_create_key_pair() self.private_key, self.public_key = rsa_create_key_pair()
private_key_path.write_bytes(self.private_key)
public_key_path.write_bytes(self.public_key)
self._trusted_peers_path = self.manager.storage / "trusted-peers.json"
self._trusted_peers: set[str] = set()
if self._trusted_peers_path.exists():
self._trusted_peers = set(json.loads(self._trusted_peers_path.read_text()))
self._banned_peers_path = self.manager.storage / "banned-peers.json"
self._banned_peers: set[str] = set()
if self._banned_peers_path.exists():
self._banned_peers = set(json.loads(self._banned_peers_path.read_text()))
def __del__(self): def __del__(self):
# close the socket # close the socket
@ -229,3 +250,55 @@ class CommunicationManager:
# no matching interfaces have been found # no matching interfaces have been found
return False return False
def save_trusted_peers(self) -> None:
"""
Save the list of trusted peers
"""
self._trusted_peers_path.write_text(json.dumps(list(self._trusted_peers)))
def save_banned_peers(self) -> None:
"""
Save the list of banned peers
"""
self._banned_peers_path.write_text(json.dumps(list(self._banned_peers)))
def trust_peer(self, public_key: bytes) -> None:
"""
Mark a peer as trusted for future connexion
Automatically save it to a file
:param public_key: the public key of the peer
"""
self._trusted_peers.add(hashlib.sha256(public_key).hexdigest())
self.save_trusted_peers()
def ban_peer(self, public_key: bytes) -> None:
"""
Ban a peer from being used for any future connexion
Automatically save it to a file
:param public_key: the public key of the peer
"""
self._banned_peers.add(hashlib.sha256(public_key).hexdigest())
self.save_banned_peers()
def is_peer_trusted(self, public_key: bytes) -> bool:
"""
Determinate is a peer is trusted or not
:param public_key: the public key of the peer
:return: True if the peer is trusted, False otherwise
"""
return hashlib.sha256(public_key).hexdigest() in self._trusted_peers
def is_peer_banned(self, public_key: bytes) -> bool:
"""
Determinate if a peer is banned or not
:param public_key: the public key of the peer
:return: True if the peer is banned, False otherwise
"""
return hashlib.sha256(public_key).hexdigest() in self._banned_peers

View file

@ -4,6 +4,7 @@ import warnings
from source import packets from source import packets
from source.behaviors import events from source.behaviors import events
from source.error import UntrustedPeerException
from source.managers import Manager from source.managers import Manager
@ -55,8 +56,8 @@ class EventManager:
# give it to the event handler # give it to the event handler
self.manager.event.handle(packet, address) self.manager.event.handle(packet, address)
except KeyboardInterrupt: except UntrustedPeerException:
break print("Ignored: untrusted peer.")
except: except Exception: # NOQA
warnings.warn(traceback.format_exc()) warnings.warn(traceback.format_exc())

View file

@ -1,8 +1,8 @@
import threading import threading
from pathlib import Path
from source import packets, structures from source import packets
from source.behaviors import events from source.behaviors import events
from source.utils.dict import TimestampedDict
class Manager: class Manager:
@ -13,6 +13,9 @@ class Manager:
def __init__(self, interface: str): def __init__(self, interface: str):
from . import CommunicationManager, EventManager, RoleManager, AudioManager, PeerManager from . import CommunicationManager, EventManager, RoleManager, AudioManager, PeerManager
self.storage = Path("./storage/")
self.storage.mkdir(exist_ok=True)
# communication manager # communication manager
self.communication = CommunicationManager(self, interface) self.communication = CommunicationManager(self, interface)
self.communication.register_packet_type(b"DISC", packets.DiscoveryPacket) self.communication.register_packet_type(b"DISC", packets.DiscoveryPacket)

View file

@ -17,6 +17,9 @@ class PeerPacket(base.BasePacket):
# is the machine a master # is the machine a master
master: bool = dataclasses.field() master: bool = dataclasses.field()
# TODO(Faraphel): share our trusted / banned peers with the other peer so that only one machine need to trust / ban it
# to propagate it to the whole network ?
def pack(self) -> bytes: def pack(self) -> bytes:
return msgpack.packb(( return msgpack.packb((
self.public_key, self.public_key,

View file

@ -13,5 +13,8 @@ class Peer:
# secret symmetric key # secret symmetric key
secret_key: Optional[bytes] = dataclasses.field(default=None) secret_key: Optional[bytes] = dataclasses.field(default=None)
# is the machine trusted
trusted: bool = dataclasses.field(default=False)
# when did the peer last communication with us occurred # when did the peer last communication with us occurred
last_interaction: datetime = dataclasses.field(default_factory=datetime.now) last_interaction: datetime = dataclasses.field(default_factory=datetime.now)