From 6f8083fdc28f6f7605c09c1ba0149d9ab1e339e6 Mon Sep 17 00:00:00 2001 From: SpookyDervish Date: Fri, 15 May 2026 10:24:49 +1000 Subject: [PATCH] added basic cryptography, advertising, and forwarding functionality to the LoRa handler. Also added a UUID class for generating unique IDs for each device. also, i have 2 radios now! :D --- relay/bluetooth_handler.py | 1 + relay/connection.py | 16 ++++++ relay/crypt_random.py | 39 +++++++++++++ relay/lora_handler.py | 115 +++++++++++++++++++++++++++++++++++-- relay/main.py | 15 +++-- relay/public_constants.txt | 2 + relay/uuid.py | 22 +++++++ test.py | 46 --------------- 8 files changed, 201 insertions(+), 55 deletions(-) create mode 100644 relay/connection.py create mode 100644 relay/crypt_random.py create mode 100644 relay/public_constants.txt create mode 100644 relay/uuid.py delete mode 100644 test.py diff --git a/relay/bluetooth_handler.py b/relay/bluetooth_handler.py index 70d320b..44fdbeb 100644 --- a/relay/bluetooth_handler.py +++ b/relay/bluetooth_handler.py @@ -57,6 +57,7 @@ class BluetoothHandler: def deserialize_msg(self, s: bytes): # returns packet type (int) and deserialized data + print(s[1:].decode()) return s[0], eval(s[1:].decode()) def _get_mac_address(self): diff --git a/relay/connection.py b/relay/connection.py new file mode 100644 index 0000000..eb9cf41 --- /dev/null +++ b/relay/connection.py @@ -0,0 +1,16 @@ +from crypt_random import secure_random + + + + +class Connection: + def load_public_constants(self): + with open("public_constants.txt", "r") as f: + self.P = int(f.readline(), 16) + self.G = int(f.readline(), 16) + + def __init__(self): + self.load_public_constants() + + self.private_key = int.from_bytes(secure_random(1024), "big") + self.public_key = pow(self.G, self.private_key) % self.P \ No newline at end of file diff --git a/relay/crypt_random.py b/relay/crypt_random.py new file mode 100644 index 0000000..181971a --- /dev/null +++ b/relay/crypt_random.py @@ -0,0 +1,39 @@ +from machine import ADC +import utime +import uhashlib + +adc = ADC(26) + + +def get_entropy(samples: int = 512): + entropy = bytearray() + + for _ in range(samples): + + value = adc.read_u16() + + mixed = ( + value ^ + (value >> 2) ^ + (value >> 5) ^ + utime.ticks_cpu() + ) & 0xFF + + entropy.append(mixed) + + utime.sleep_us(5) + + return entropy + +def secure_random(num_bytes: int = 32): + entropy = get_entropy() + + digest = uhashlib.sha256(entropy).digest() + + output = bytearray() + + while len(output) < num_bytes: + digest = uhashlib.sha256(digest).digest() + output.extend(digest) + + return bytes(output[:num_bytes]) \ No newline at end of file diff --git a/relay/lora_handler.py b/relay/lora_handler.py index 3da3992..4d76d18 100644 --- a/relay/lora_handler.py +++ b/relay/lora_handler.py @@ -1,17 +1,124 @@ +import time +import uuid +from sx1262 import SX1262 + +from machine import Pin +led = Pin(25, Pin.OUT) + +class FixedDeque: + def __init__(self, maxlen): + self.maxlen = maxlen + self.data = [] + + def append(self, x): + if len(self.data) >= self.maxlen: + self.data.pop(0) + self.data.append(x) + + def __iter__(self): + return iter(self.data) + +class LoRaMessage: + def __init__(self, sender_id: str, recipient_id: str, payload: bytes): + self.sender_id = sender_id + self.recipient_id = recipient_id + self.payload = payload + self.message_id = str(uuid.uuid4()) + self.lifetime = 7 + + def encode(self) -> bytes: + encoded = bytearray() + + encoded.append(len(self.sender_id)) + encoded.extend(self.sender_id.encode("utf-8")) + + encoded.append(len(self.recipient_id)) + encoded.extend(self.recipient_id.encode("utf-8")) + + encoded.extend(self.payload) + + return bytes(encoded) + + @staticmethod + def decode(data: bytes): + sender_id_len = data[0] + sender_id = data[1:1+sender_id_len].decode("utf-8") + + recipient_id_len = data[1+sender_id_len] + recipient_id = data[2+sender_id_len:2+sender_id_len+recipient_id_len].decode("utf-8") + + payload = data[2+sender_id_len+recipient_id_len:] + + return LoRaMessage(sender_id=sender_id, recipient_id=recipient_id, payload=payload) + class LoRaHandler: def __init__(self): print("Initializing LoRa...") + self.sender_id = str(uuid.uuid4()) + # initialize our radio, im using the HAT SX1262 hat for the pico self.radio = SX1262(spi_bus=1, clk=10, mosi=11, miso=12, cs=3, irq=20, rst=15, gpio=2) self.radio.begin(freq=915, bw=125, power=22) self.radio.setBlockingCallback(False, self.irq) + self.recent_messages = FixedDeque(32) + self.neighbours = set() + + def send(self, data: bytes, recipient_id: str): + msg_bytes = LoRaMessage(sender_id=self.sender_id, recipient_id=recipient_id, payload=data).encode() + print(msg_bytes) + self.radio.send(msg_bytes) + + def forward(self, msg: LoRaMessage): + msg_bytes = msg.encode() + self.radio.send(msg_bytes) + + def advertise(self): + # send an advertisement message to let neighbors know we're here + self.send(b"ADVERTISE", recipient_id="BROADCAST") + + def on_receive(self, msg: LoRaMessage): + # ensure we don't process a message twice + if msg.message_id in self.recent_messages: + return + self.recent_messages.append(msg.message_id) + + # messages has a lifetime to prevent infinite loops. + # if we receive a message with lifetime <= 0 we just drop it, + # otherwise we decrement the lifetime and forward it to all + # neighbors except the sender + if msg.lifetime <= 0: + return + msg.lifetime -= 1 + + # forward messages if they aren't meant for us + if (msg.recipient_id != self.sender_id) and msg.recipient_id != "BROADCAST": + print("Forwarding message...") + self.forward(msg) + return + + if msg.payload == b"ADVERTISE": + if not msg.sender_id in self.neighbours: + print(f"Neighbour discovered: {msg.sender_id}") + self.neighbours.add(msg.sender_id) + return + + print(f"Received message from {msg.sender_id}: {msg.payload}") + + + def irq(self, events): - print(f"LORA EVENT: {events}") + #print(f"LORA EVENT: {events}") if events & SX1262.RX_DONE: - msg, err = sx.recv() + msg, err = self.radio.recv() error = SX1262.STATUS[err] - print('Receive: {}, {}'.format(msg, error)) + + if error != "ERR_NONE": + print(f"Error receiving message: {error}") + return + + self.on_receive(LoRaMessage.decode(msg)) elif events & SX1262.TX_DONE: - print('TX done.') \ No newline at end of file + #print('TX done.') + pass \ No newline at end of file diff --git a/relay/main.py b/relay/main.py index a7a6d8f..4f0a06b 100644 --- a/relay/main.py +++ b/relay/main.py @@ -1,23 +1,28 @@ from sx1262 import SX1262 from _sx126x import * import time +import crypt_random -from bluetooth_handler import BluetoothHandler from lora_handler import LoRaHandler -LORA_ENABLED = False +LORA_ENABLED = True +BLUETOOTH_ENABLED = False def main(): - bluetooth_handler = BluetoothHandler() + bluetooth_handler = None + if BLUETOOTH_ENABLED: + from bluetooth_handler import BluetoothHandler + bluetooth_handler = BluetoothHandler() lora_handler = None if LORA_ENABLED: lora_handler = LoRaHandler() - print("Halting Pico...") + print("Ready!") while True: - pass + lora_handler.advertise() + time.sleep(5) if __name__ == "__main__": diff --git a/relay/public_constants.txt b/relay/public_constants.txt new file mode 100644 index 0000000..3040146 --- /dev/null +++ b/relay/public_constants.txt @@ -0,0 +1,2 @@ +FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF +2 \ No newline at end of file diff --git a/relay/uuid.py b/relay/uuid.py new file mode 100644 index 0000000..2736eba --- /dev/null +++ b/relay/uuid.py @@ -0,0 +1,22 @@ +import urandom + +def uuid4(): + b = bytearray(urandom.getrandbits(8) for _ in range(16)) + + # Set version (4) -> bits 12-15 of time_hi_and_version + b[6] = (b[6] & 0x0F) | 0x40 + + # Set variant (RFC 4122) + b[8] = (b[8] & 0x3F) | 0x80 + + def to_hex(x): + return '{:02x}'.format(x) + + return ( + to_hex(b[0]) + to_hex(b[1]) + to_hex(b[2]) + to_hex(b[3]) + '-' + + to_hex(b[4]) + to_hex(b[5]) + '-' + + to_hex(b[6]) + to_hex(b[7]) + '-' + + to_hex(b[8]) + to_hex(b[9]) + '-' + + to_hex(b[10]) + to_hex(b[11]) + to_hex(b[12]) + + to_hex(b[13]) + to_hex(b[14]) + to_hex(b[15]) + ) \ No newline at end of file diff --git a/test.py b/test.py deleted file mode 100644 index 952048f..0000000 --- a/test.py +++ /dev/null @@ -1,46 +0,0 @@ -from bleak import BleakScanner, BleakClient -import asyncio - - -async def main(): - devices = await BleakScanner.discover(service_uuids=["E1898FF7-5063-4441-a6eb-526073B00001"]) - for device in devices: - print() - print(f"Name: {device.name}") - print(f"Address: {device.address}") - print(f"Details: {device.details}") - - for device in devices: - try: - this_device = await BleakScanner.find_device_by_address(device.address, timeout=20) - async with BleakClient(this_device) as client: - print(f'Services found for device') - print(f'\tDevice address:{device.address}') - print(f'\tDevice name:{device.name}') - - client.write_gatt_char() - - print('\tServices:') - for service in client.services: - print() - print(f'\t\tDescription: {service.description}') - print(f'\t\tService: {service}') - - print('\t\tCharacteristics:') - for c in service.characteristics: - print() - print(f'\t\t\tUUID: {c.uuid}'), - print(f'\t\t\tDescription: {c.uuid}') - print(f'\t\t\tHandle: {c.uuid}'), - print(f'\t\t\tProperties: {c.uuid}') - - print('\t\tDescriptors:') - for descrip in c.descriptors: - print(f'\t\t\t{descrip}') - - except Exception as e: - print(f"Could not connect to device with info: {device}") - print(f"Error: {e}") - - -asyncio.run(main()) \ No newline at end of file