From 1301b1025920f0ab53fa23090b9c69f65c440981 Mon Sep 17 00:00:00 2001 From: study-faraphel Date: Sat, 4 Jan 2025 18:28:13 +0100 Subject: [PATCH] (untested) slave shall now ask for the server secret key for symmetric communications --- source/behaviors/events/DiscoveryEvent.py | 7 ++-- source/behaviors/events/KeyEvent.py | 20 +++++++++++ source/behaviors/events/RequestKeyEvent.py | 22 ++++++++++++ source/behaviors/events/__init__.py | 4 ++- source/behaviors/roles/MasterRole.py | 15 +++++---- source/behaviors/roles/SlaveRole.py | 17 ++++++++-- source/behaviors/roles/UndefinedRole.py | 39 +++++++++++++++++++--- source/behaviors/roles/__init__.py | 3 +- source/managers/CommunicationManager.py | 30 +++++++++++++++++ source/managers/Manager.py | 4 +++ source/packets/KeyPacket.py | 23 +++++++++++++ source/packets/PeerPacket.py | 4 +++ source/packets/RequestKeyPacket.py | 19 +++++++++++ source/packets/__init__.py | 2 ++ 14 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 source/behaviors/events/KeyEvent.py create mode 100644 source/behaviors/events/RequestKeyEvent.py create mode 100644 source/packets/KeyPacket.py create mode 100644 source/packets/RequestKeyPacket.py diff --git a/source/behaviors/events/DiscoveryEvent.py b/source/behaviors/events/DiscoveryEvent.py index bf1a022..8b60c1e 100644 --- a/source/behaviors/events/DiscoveryEvent.py +++ b/source/behaviors/events/DiscoveryEvent.py @@ -1,6 +1,6 @@ from . import base from source import packets -from ...packets import PeerPacket +from .. import roles from ...utils.crypto.type import CipherType @@ -11,6 +11,9 @@ class DiscoveryEvent(base.BaseEvent): def handle(self, packet: packets.DiscoveryPacket, address: tuple): # create a peer packet containing our important information - peerPacket = PeerPacket(self.manager.communication.public_key) + peerPacket = packets.PeerPacket( + self.manager.communication.public_key, + isinstance(self.manager.role, roles.MasterRole) + ) # send our information back self.manager.communication.send(peerPacket, CipherType.PLAIN, address) diff --git a/source/behaviors/events/KeyEvent.py b/source/behaviors/events/KeyEvent.py new file mode 100644 index 0000000..b79d2dd --- /dev/null +++ b/source/behaviors/events/KeyEvent.py @@ -0,0 +1,20 @@ +from source import packets +from source.behaviors import roles +from source.behaviors.events import base + + +class KeyEvent(base.BaseEvent): + """ + Event reacting to a machine sending us their secret key + """ + + def handle(self, packet: packets.KeyPacket, address: tuple): + # check if we are a slave + if not isinstance(self.manager.role, roles.SlaveRole): + return + + # TODO(Faraphel): check if this come from our server ? + + # use the secret key for further symmetric communication + # TODO(Faraphel): should not be an attribute of the manager communication system, create a dictionary with the secret key of the servers ? + self.manager.communication.secret_key = packet.secret_key diff --git a/source/behaviors/events/RequestKeyEvent.py b/source/behaviors/events/RequestKeyEvent.py new file mode 100644 index 0000000..83d4522 --- /dev/null +++ b/source/behaviors/events/RequestKeyEvent.py @@ -0,0 +1,22 @@ +from source import packets +from source.behaviors import roles +from source.behaviors.events import base +from source.utils.crypto.type import CipherType + + +class RequestKeyEvent(base.BaseEvent): + """ + Event reacting to a machine trying to get our secret symmetric key for secure communication + """ + + def handle(self, packet: packets.RequestKeyPacket, address: tuple): + # check if we are a master + if not isinstance(self.manager.role, roles.MasterRole): + return + + # create a packet containing our secret key + packet = packets.KeyPacket(self.manager.communication.secret_key) + # send it back to the slave + self.manager.communication.send(packet, CipherType.RSA, address) + + # TODO(Faraphel): check if we trust the slave ? diff --git a/source/behaviors/events/__init__.py b/source/behaviors/events/__init__.py index eb5f395..48e1df4 100644 --- a/source/behaviors/events/__init__.py +++ b/source/behaviors/events/__init__.py @@ -2,4 +2,6 @@ from . import base from .DiscoveryEvent import DiscoveryEvent from .PeerEvent import PeerEvent -from .AudioEvent import AudioEvent \ No newline at end of file +from .AudioEvent import AudioEvent +from .RequestKeyEvent import RequestKeyEvent +from .KeyEvent import KeyEvent diff --git a/source/behaviors/roles/MasterRole.py b/source/behaviors/roles/MasterRole.py index 1544dba..c7f4a34 100644 --- a/source/behaviors/roles/MasterRole.py +++ b/source/behaviors/roles/MasterRole.py @@ -27,7 +27,7 @@ class MasterRole(base.BaseRole): # prepare the audio file that will be streamed # TODO(Faraphel): use another audio source - self.audio = pydub.AudioSegment.from_file("../assets/Caravan Palace - Wonderland.mp3") + self.audio = pydub.AudioSegment.from_file("../assets/Queen - Another One Bites the Dust.mp3") self.play_time = datetime.now() # calculate the number of bytes per milliseconds in the audio @@ -36,6 +36,7 @@ class MasterRole(base.BaseRole): self.chunk_duration = timedelta(milliseconds=self.TARGET_SIZE / bytes_per_ms) # split the audio into chunks + self.chunk_count = 0 self.chunks = make_chunks(self.audio, self.chunk_duration.total_seconds() * 1000) @@ -44,8 +45,8 @@ class MasterRole(base.BaseRole): # TODO(Faraphel): share the secret key generated with the other *allowed* peers ! How to select them ? A file ? # TODO(Faraphel): check if another server is emitting sound in the network. Return to undefined if yes - # get the next chunk - chunk = self.chunks.pop(0) + # get the current chunk + chunk = self.chunks[self.chunk_count] # broadcast it in the network audio_packet = AudioPacket( @@ -58,6 +59,8 @@ class MasterRole(base.BaseRole): ) self.manager.communication.broadcast(audio_packet, CipherType.AES_CBC) - # wait for the audio to play - # TODO(Faraphel): should adapt to the compute time above - pause.until(datetime.now() + self.chunk_duration) + # increment the chunk count + self.chunk_count += 1 + + # wait for the next chunk time + pause.until(self.play_time + (self.chunk_duration * self.chunk_count)) diff --git a/source/behaviors/roles/SlaveRole.py b/source/behaviors/roles/SlaveRole.py index 35d7349..3e37f1c 100644 --- a/source/behaviors/roles/SlaveRole.py +++ b/source/behaviors/roles/SlaveRole.py @@ -1,4 +1,7 @@ +from source import managers, packets from source.behaviors.roles import base +from source.utils.crypto.type import CipherType + class SlaveRole(base.BaseRole): """ @@ -6,6 +9,16 @@ class SlaveRole(base.BaseRole): It shall listen for a master and check if everything is working properly """ + def __init__(self, manager: "managers.Manager", master_address: tuple): + super().__init__(manager) + + # the address of the server + self.master_address = master_address + def handle(self): - # TODO(Faraphel): ping the server and check if it is working properly. Return to undefined if no. - pass + # TODO(Faraphel): ping the master and check if it is working properly. Return to undefined if no. + + # NOTE(Faraphel): the secret key might be stored somewhere else than here, or need to be reset + if self.manager.communication.secret_key is None: + packet = packets.RequestKeyPacket() + self.manager.communication.send(packet, CipherType.AES_CBC, self.master_address) diff --git a/source/behaviors/roles/UndefinedRole.py b/source/behaviors/roles/UndefinedRole.py index cfdba22..a593897 100644 --- a/source/behaviors/roles/UndefinedRole.py +++ b/source/behaviors/roles/UndefinedRole.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from . import base, MasterRole from source import packets +from .SlaveRole import SlaveRole from ...managers import Manager from ...utils.crypto.type import CipherType @@ -35,8 +36,38 @@ class UndefinedRole(base.BaseRole): # check if no more peers have been found in the previous seconds if datetime.now() - self.previous_discovery >= timedelta(seconds=5): - # check if no peer have been found except ourselves. - # TODO(Faraphel): need a better check than just that - if len(self.manager.peers) == 1: - # if we are the only machine, become a master + # SCENARIO 1 - empty network + + # filter ourselves out of the remote peers + remote_peers = { + address: peer + for (address, peer) in self.manager.peers.items() + if not self.manager.communication.is_address_local(address) + } + + # if no other peers have been found + if len(remote_peers) == 0: + # declare ourselves as the master of the network self.manager.role.current = MasterRole(self.manager) + return + + # SCENARIO 2 - network with a master + + # list all the peers considered as masters + master_peers = { + address: peer + for (address, peer) in remote_peers.items() + if peer.master + } + + # if there is a master, become a slave + if len(master_peers) >= 1: + master_address, master_peer = master_peers[0] + + # declare ourselves as a slave of the network + self.manager.role.current = SlaveRole(self.manager, master_address) + + # SCENARIO 3 - network with no master + + # TODO(Faraphel): elect the machine with the lowest ping in the network + raise NotImplementedError("Not implemented: elect the machine with the lowest ping as a master.") \ No newline at end of file diff --git a/source/behaviors/roles/__init__.py b/source/behaviors/roles/__init__.py index 8f790cd..c51027f 100644 --- a/source/behaviors/roles/__init__.py +++ b/source/behaviors/roles/__init__.py @@ -1,4 +1,5 @@ from . import base from .MasterRole import MasterRole -from .UndefinedRole import UndefinedRole \ No newline at end of file +from .SlaveRole import SlaveRole +from .UndefinedRole import UndefinedRole diff --git a/source/managers/CommunicationManager.py b/source/managers/CommunicationManager.py index b6fece6..c30e96f 100644 --- a/source/managers/CommunicationManager.py +++ b/source/managers/CommunicationManager.py @@ -171,3 +171,33 @@ class CommunicationManager: payload, address = self.socket.recvfrom(65536) # decode the payload return self.packet_decode(payload), address + + @staticmethod + def get_local_addresses() -> list[tuple]: + """ + Get the local addresses of the machine + :return: the local addresses of the machine + """ + + return socket.getaddrinfo(socket.gethostname(), None) + + def is_address_local(self, address: tuple) -> bool: + """ + Is the given address local + :return: true if the address is local, false otherwise + """ + + host, _, _, scope = address + + # check for all the interfaces of our machine + for interface in self.get_local_addresses(): + # unpack the interface information + interface_family, _, _, _, interface_address = interface + interface_host, _, _, interface_scope = interface_address + + # check if it matches the address interface + if host == interface_host and scope == interface_scope: + return True + + # no matching interfaces have been found + return False diff --git a/source/managers/Manager.py b/source/managers/Manager.py index 1981e99..89a7722 100644 --- a/source/managers/Manager.py +++ b/source/managers/Manager.py @@ -17,12 +17,16 @@ class Manager: self.communication.register_packet_type(b"DISC", packets.DiscoveryPacket) self.communication.register_packet_type(b"PEER", packets.PeerPacket) self.communication.register_packet_type(b"AUDI", packets.AudioPacket) + self.communication.register_packet_type(b"RQSK", packets.RequestKeyPacket) + self.communication.register_packet_type(b"GTSK", packets.KeyPacket) # event manager self.event = EventManager(self) self.event.register_event_handler(packets.DiscoveryPacket, events.DiscoveryEvent(self)) self.event.register_event_handler(packets.PeerPacket, events.PeerEvent(self)) self.event.register_event_handler(packets.AudioPacket, events.AudioEvent(self)) + self.event.register_event_handler(packets.RequestKeyPacket, events.RequestKeyEvent(self)) + self.event.register_event_handler(packets.KeyPacket, events.KeyEvent(self)) # role manager self.role = RoleManager(self) diff --git a/source/packets/KeyPacket.py b/source/packets/KeyPacket.py new file mode 100644 index 0000000..c6cb2b5 --- /dev/null +++ b/source/packets/KeyPacket.py @@ -0,0 +1,23 @@ +import dataclasses + +import msgpack + +from source.packets import base + + +@dataclasses.dataclass +class KeyPacket(base.BasePacket): + """ + Represent a packet containing a secret symmetric key + """ + + secret_key: bytes = dataclasses.field(repr=False) + + def pack(self) -> bytes: + return msgpack.packb(( + self.secret_key + )) + + @classmethod + def unpack(cls, data: bytes): + return cls(*msgpack.unpackb(data)) diff --git a/source/packets/PeerPacket.py b/source/packets/PeerPacket.py index 51a04d9..80903c1 100644 --- a/source/packets/PeerPacket.py +++ b/source/packets/PeerPacket.py @@ -14,9 +14,13 @@ class PeerPacket(base.BasePacket): # public RSA key of the machine public_key: bytes = dataclasses.field(repr=False) + # is the machine a master + master: bool = dataclasses.field() + def pack(self) -> bytes: return msgpack.packb(( self.public_key, + self.master )) @classmethod diff --git a/source/packets/RequestKeyPacket.py b/source/packets/RequestKeyPacket.py new file mode 100644 index 0000000..13ed0d0 --- /dev/null +++ b/source/packets/RequestKeyPacket.py @@ -0,0 +1,19 @@ +import dataclasses + +import msgpack + +from source.packets import base + + +@dataclasses.dataclass +class RequestKeyPacket(base.BasePacket): + """ + Represent a packet used to request a secret symmetric key + """ + + def pack(self) -> bytes: + return msgpack.packb(()) + + @classmethod + def unpack(cls, data: bytes): + return cls() diff --git a/source/packets/__init__.py b/source/packets/__init__.py index c047519..ed5ed26 100644 --- a/source/packets/__init__.py +++ b/source/packets/__init__.py @@ -3,3 +3,5 @@ from . import base from .AudioPacket import AudioPacket from .DiscoveryPacket import DiscoveryPacket from .PeerPacket import PeerPacket +from .RequestKeyPacket import RequestKeyPacket +from .KeyPacket import KeyPacket