UPGMRP/communication.py

152 lines
5.9 KiB
Python
Raw Normal View History

2019-08-26 10:49:39 +02:00
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:
<topic>=<payload>
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)