From 905e407b10de3eb56e212a8b07d1d6e60696086a Mon Sep 17 00:00:00 2001 From: Samuel Sloniker Date: Thu, 3 Nov 2022 18:40:33 -0700 Subject: [PATCH] Add code --- leonet/__main__.py | 79 +++++++++++++++++++++++ leonet/crypto.py | 22 +++++++ leonet/frames.py | 122 ++++++++++++++++++++++++++++++++++++ leonet/hashers.py | 22 +++++++ leonet/message_codec.py | 11 ++++ leonet/modem.py | 135 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+) create mode 100644 leonet/__main__.py create mode 100644 leonet/crypto.py create mode 100644 leonet/frames.py create mode 100644 leonet/hashers.py create mode 100644 leonet/message_codec.py create mode 100644 leonet/modem.py diff --git a/leonet/__main__.py b/leonet/__main__.py new file mode 100644 index 0000000..35e3fdc --- /dev/null +++ b/leonet/__main__.py @@ -0,0 +1,79 @@ +import os +import time +import threading +import argparse + +import modem +import frames + + +sent_messages = {} +received_messages = {} + + +def sends(): + while True: + try: + with open(input_file) as f: + recipient, message = f.read().split(" ", 1) + os.unlink(input_file) + except FileNotFoundError: + continue + + message = message.strip() + + if message.startswith("QRY "): + message_id = message.split(" ")[1] + modem.send( + modem.wrap_frame( + frames.QryFrame( + recipient, my_identifier, message_id + ).encode(), + callsign, + ) + ) + else: + frame = frames.MsgFrame( + recipient, + my_identifier, + message, + ) + + sent_messages[frame.message_id] = [ + frame, + False, + ] + modem.send(modem.wrap_frame(frame.encode(), callsign)) + + +parser = argparse.ArgumentParser(prog="leonet") +parser.add_argument("identifier", help="my identifier (e.g. CALLSIGN-1)") +parser.add_argument( + "callsign", help="my call sign (does not need to match identifier)" +) +parser.add_argument("input_file", help="file to watch for messages") +args = parser.parse_args() + +my_identifier = args.identifier +callsign = args.callsign +input_file = args.input_file + +threading.Thread(target=sends).start() + +transceiver = modem.Transceiver(my_identifier, callsign) + +while True: + frame = transceiver.receive() + print() + print(frame) + if isinstance(frame, frames.MsgFrame): + print(frame.timestamp, frame.originator, frame.message) + time.sleep(5) + received_messages[frame.message_id] = frame + transceiver.transmit(frames.gen_ack().encode()) + elif isinstance(frame, frames.QryFrame): + time.sleep(5) + if frame.message_id in received_messages: + transceiver.transmit(frames.gen_ack().encode()) + else: + transceiver.transmit(frames.gen_nak().encode()) diff --git a/leonet/crypto.py b/leonet/crypto.py new file mode 100644 index 0000000..f57a978 --- /dev/null +++ b/leonet/crypto.py @@ -0,0 +1,22 @@ +import hmac +import hashlib +import base64 + + +def find_auth_code(password, message): + if password: + return base64.b64encode( + hmac.digest( + password.encode("utf-8"), + message.encode("ascii"), + "sha256", + ) + ).decode("ascii") + else: + return "" + + +def find_checksum(data): + return base64.b64encode( + hashlib.sha256(data.encode("ascii")).digest()[:8] + ).decode("ascii") diff --git a/leonet/frames.py b/leonet/frames.py new file mode 100644 index 0000000..d2d2e29 --- /dev/null +++ b/leonet/frames.py @@ -0,0 +1,122 @@ +import typing +import datetime +import dataclasses + +import crypto +import message_codec + + +def create_frame(frame_type, recipient, originator, content, password): + main_frame = f"{frame_type}#{recipient}#{originator}#{content}" + + checksum = crypto.find_checksum(main_frame) + auth_code = crypto.find_auth_code(password, main_frame) + + return f"\nLEONET {main_frame} {checksum} {auth_code} TENOEL\n" + + +@dataclasses.dataclass +class MsgFrame: + recipient: str + originator: str + message: str + timestamp: dataclasses.InitVar[typing.Optional[str]] = None + message_id: dataclasses.InitVar[typing.Optional[str]] = None + + def __post_init__(self, message_id, timestamp): + if timestamp is not None: + self.timestamp = timestamp + else: + self.timestamp = datetime.datetime.utcnow().isoformat() + + if message_id is not None: + self.message_id = message_id + else: + safe_message = message_codec.encode(self.message) + body = f"{self.timestamp}&{safe_message}" + self.message_id = crypto.find_checksum(body) + + self.frame_recipient = self.recipient + + def encode(self, password=""): + safe_message = message_codec.encode(self.message) + body = f"{self.timestamp}&{safe_message}" + message_id = crypto.find_checksum(body) + full_body = f"{body};{message_id}" + + return create_frame( + "MSG", self.recipient, self.originator, full_body, password + ) + + def gen_ack(self): + return frames.AckFrame( + self.recipient, + self.originator, + self.message_id, + ) + + def gen_nak(self): + return frames.NakFrame( + self.recipient, + self.originator, + self.message_id, + ) + + +@dataclasses.dataclass +class QryFrame: + recipient: str + originator: str + message_id: str + + def __post_init__(self): + self.frame_recipient = self.recipient + + def encode(self, password=""): + return create_frame( + "QRY", self.recipient, self.originator, self.message_id, password + ) + + def gen_ack(self): + return frames.AckFrame( + self.recipient, + self.originator, + self.message_id, + ) + + def gen_nak(self): + return frames.NakFrame( + self.recipient, + self.originator, + self.message_id, + ) + + +@dataclasses.dataclass +class AckFrame: + recipient: str + originator: str + message_id: str + + def __post_init__(self): + self.frame_recipient = self.originator + + def encode(self, password=""): + return create_frame( + "ACK", self.recipient, self.originator, self.message_id, password + ) + + +@dataclasses.dataclass +class NakFrame: + recipient: str + originator: str + message_id: str + + def __post_init__(self): + self.frame_recipient = self.originator + + def encode(self, password=""): + return create_frame( + "NAK", self.recipient, self.originator, self.message_id, password + ) diff --git a/leonet/hashers.py b/leonet/hashers.py new file mode 100644 index 0000000..f57a978 --- /dev/null +++ b/leonet/hashers.py @@ -0,0 +1,22 @@ +import hmac +import hashlib +import base64 + + +def find_auth_code(password, message): + if password: + return base64.b64encode( + hmac.digest( + password.encode("utf-8"), + message.encode("ascii"), + "sha256", + ) + ).decode("ascii") + else: + return "" + + +def find_checksum(data): + return base64.b64encode( + hashlib.sha256(data.encode("ascii")).digest()[:8] + ).decode("ascii") diff --git a/leonet/message_codec.py b/leonet/message_codec.py new file mode 100644 index 0000000..42ec925 --- /dev/null +++ b/leonet/message_codec.py @@ -0,0 +1,11 @@ +import base64 + + +def encode(message): + return "B64:" + base64.b64encode(message.encode("utf-8")).decode("ascii") + + +def decode(message): + code, content = message.split(":", 1) + if code == "B64": + return base64.b64decode(content).decode("utf-8") diff --git a/leonet/modem.py b/leonet/modem.py new file mode 100644 index 0000000..ae98a2a --- /dev/null +++ b/leonet/modem.py @@ -0,0 +1,135 @@ +import queue +import threading +import subprocess + +import frames +import crypto + + +def wrap_frame(message, callsign): + return f"Syncronizing... Synchronizing... Synchronizing...{message}https://kj7rrv.com/leonet DE {callsign}\n\n" + + +class Transmitter: + def __init__(self, callsign): + self.callsign = callsign + self.queue = queue.Queue() + self.transmit = self.queue.put + threading.Thread(target=self.tx_loop).start() + + def tx_loop(self): + while True: + send(wrap_frame(self.queue.get(), callsign)) + + +def send(data): + print(data) + subprocess.run( + ["minimodem", "--tx", "300"], + input=data, + encoding="ascii", + ) + + +class InvalidFrameError(BaseException): + pass + + +class FrameDecodeError(InvalidFrameError): + pass + + +class FrameAuthenticationError(InvalidFrameError): + pass + + +def decode(frame_bytes, passwords): + try: + frame = frame_bytes.decode("ascii") + + main, checksum, auth_code = frame.split(" ")[1:-1] + + if checksum != crypto.find_checksum(main): + raise FrameDecodeError() + + frame_type, recipient, originator, payload = main.split("#") + + if passwords is not None: + password = passwords.get( + originator if directions[frame_type] == "OR" else recipient, "" + ) + + if auth_code != crypto.find_auth_code(password, main): + raise FrameAuthenticationError + + if frame_type == "MSG": + body, message_id = payload.split(";") + timestamp, safe_message = body.split("&") + message = make_message_unsafe(safe_message) + return frame_objects.MsgFrame( + recipient, + originator, + message_id, + timestamp, + message, + ) + elif frame_type == "QRY": + return frame_objects.QryFrame(recipient, originator, payload) + elif frame_type == "ACK": + return frame_objects.AckFrame(recipient, originator, payload) + elif frame_type == "Nak": + return frame_objects.NakFrame(recipient, originator, payload) + else: + raise FrameDecodeError() + except BaseException: + raise FrameDecodeError() + + +class Receiver: + def __init__(self, identifier): + self.modem = subprocess.Popen( + ["minimodem", "--quiet", "--rx", "300"], stdout=subprocess.PIPE + ) + self.running = True + self.queue = queue.Queue() + self.receive = self.queue.get + self.identifier = identifier + + threading.Thread(target=self.rx_loop).start() + + def rx_loop(self): + rx_buffer = b"" + + while True: + if not self.running: + break + + byte = self.modem.stdout.read(1) + if byte == b"\n": + if rx_buffer.startswith(b"LEONET") and rx_buffer.endswith( + b"TENOEL" + ): + try: + frame = decode(rx_buffer, None) # passwords dict + if ( + self.identifier is None + or frame.frame_recipient == self.identifier + ): + self.queue.put(frame) + except InvalidFrameError as e: + pass + rx_buffer = b"" + else: + rx_buffer += byte + + +class Transceiver: + def __init__(self, identifier, callsign): + self.transmitter = Transmitter(callsign) + self.receiver = Receiver(identifier) + + self.tx_queue = self.transmitter.queue + self.rx_queue = self.receiver.queue + + self.transmit = self.transmitter.transmit + self.receive = self.receiver.receive