diff --git a/communication.py b/communication.py new file mode 100644 index 0000000..d4457f4 --- /dev/null +++ b/communication.py @@ -0,0 +1,151 @@ +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +from threading import Thread +from time import sleep +from datetime import datetime +import sys,socket,time,binascii + + +""" Documentation for UPGmrp (UPGRAIT mqtt replacement protocoll) + +Basic information: +------------------ +UPGmrp is based on AES-CBC-256. + +The mrpKey is stored at the UPGRAIT database and is unique for every customer and has an hex representation. +(256 bit = 32 Byte, stored as hex: 64 Byte, so two hex chars represent one byte.) +All customer devices use the mrpKey to broadcast their outgoing mqtt-messages to as wide as possible (255.255.255.255) to udp port 8677. + +IV and CIPHERTEXT are always representated as HEX. + +It is called "mqtt replacement protocol" because it is used to broadcast mqtt publishes to the network with the same structure: + += + +Things like retained messages or QoS-Flags are not (yet) supported. + +Building plaintext message: +--------------------------- +The plaintext message contains the actual message, suffixed by a timestamp (milliseconds since 01.01.1970) +An @-Sign is used as seperator. + +So the actual data looks like this (without spaces): +TIMESTAMP @ MESSAGE + +This data will be padded with \0 until it meets AES Block Size (16 Bytes) requirements. + +Example: +1234567890@module1/status/online=1 (this are 34 bytes, so it has to be padded with \0 to 48 bytes) +1234567890@module1/status/online=1.............. + + +Ciphering: +---------- +The plaintext message is ciphered using an random initialization vector ('IV'). +Through the timestamp (which is unique) and a random initialization vector it is guaranteed, that there will never be the same plaintext-message with differnet IVs. + +The result is called ciphertext. + +Example: +IV = 4ef259a39e65c310704c0df0614717d9 (32 hex-chars = 16 byte) +CIPHERTEXT = 6540826baa38dab5e46d857ec8afc1c5 (actual 32 hex-chars = 16 byte, can be longer as multiple of 16) + +Building UDP-Data: +------------------ +The recipient of the broadcast needs the correct IV to decrypt the message. +(The IV is not important for security if it is guaranteed that there will never be the same message ciphered with different IVs) + +To send the IV to the recipient, the IV is prefixed to the ciphertext, so the UDP-Data looks like this (without spaces): + +IV CIPHERTEXT + +Example: +4ef259a39e65c310704c0df0614717d96540826baa38dab5e46d857ec8afc1c5 +\--IV--------------------------/\--CIPHERTEXT---------------.../ + +Sending UDP-Data: +----------------- +This UDP-Data is being broadcasted as far as possible using the destination-Broadcast-IP 255.255.255.255 and the destination port 8677. +(Normally this is absolutely perfect, until customers decide to use multiple subnets, Guest-Wifis, VLANS or other techniques to block UDP-Broadcast-Traffic) + +Every message will be send 3 times. + +Receiving UDP-Data: +------------------- +To receive these UDP-Packets, all modules and other customer devices has to listen to port 8677. +Incoming packets will be decrypted vice-versa. (cut out IV, using mrpKey and this IV to decrypt ciphertext). + +Validation: +----------- +The decrypted ciphertext starts with a timestamp. +This timestamp will be compared to the recievers timestamp. If the message timestamp is within a range of +- 2.5secs it is trusted, otherwise it has to be ignored. + + +Copyright 2019, UPGRAIT GmbH (Sebastian Heun) +""" + + +class UPGmrp: + mrpKey = "" + def listenThread(self): + print("UPGmrp\t Listen Thread started...") + while True: + sleep(.1) + try: + data,addr = self.rxSocket.recvfrom(1024) + # Split + iv = data[:16] + ct = data[16:] + dc = AES.new(self.mrpKey, AES.MODE_CBC, iv).decrypt(ct).decode('utf-8') + timestamp,payload = dc.split('@',1) + now = int(round(time.time() * 1000)) + timestamp = int(timestamp) + if int(timestamp) != int(self.lastTransmit): + if self.lastTransmit < 0 or abs(now - timestamp) < 2500: + self.lastTransmit = timestamp + self.callbackfunction(payload) + except: + pass + def __init__(self,key,cbfunction): + self.lastTransmit = -1 + print("UPGmrp\t Init....") + self.mrpKey = binascii.unhexlify(key) + # Init receiving socket + print("UPGmrp\t Creating socket...") + self.rxSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.rxSocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.rxSocket.bind(('',8677)) + self.rxSocket.setblocking(0) + Thread(target=self.listenThread,args=()).start() + self.callbackfunction=cbfunction + def send(self,message): + message = str(message) + print("Sending message... "+message) + # Build message + self.lastTransmit = round(time.time()*1000) + output = str(self.lastTransmit)+str('@')+str(message) + # Justify length to multiple of 16 + length = int((int(len(output)/16)+1)*16) + output = output.ljust(length,'\0') + iv = get_random_bytes(AES.block_size) + backupiv = iv + ec = AES.new(self.mrpKey, AES.MODE_CBC,iv).encrypt(output.encode()) + output = binascii.hexlify(backupiv) + binascii.hexlify(ec) + txSocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + txSocket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + txSocket.setblocking(0) + # Send 3 Times, because UDP has alzheimer's + txSocket.sendto(output,("255.255.255.255",8677)) + txSocket.sendto(output,("255.255.255.255",8677)) + txSocket.sendto(output,("255.255.255.255",8677)) + + +def messageHandler(message): + print("Incoming Message : "+message) + +if __name__ == '__main__': + mrp = UPGmrp("c6d3fb35e130c6bde4f262a4647649347cc6e8609259d3a28ac1d7534598949a",messageHandler) + while True: + msg = input('Message...: ') + mrp.send(msg) + sleep(1)