From a85b08f050055713b07fee65057ff0e4ba5a947b Mon Sep 17 00:00:00 2001 From: hornet <43388240+hornetfighter515@users.noreply.github.com> Date: Sun, 28 Nov 2021 19:00:46 +0000 Subject: [PATCH] Feature/py iot (#4) * Changing files but having issues with space tabs. * Removed config. Fixed indents. * Removed config. * Fixed test config. Fixed syntax in cli. * Editing cli and hub to finally commit transactions. * Added better logging logic. Will clean up in the morning. * Hub is more functional. * Edited IoT to gen keys. * Public keys are now generated, sent, and stored. However, they are not transmitted when a new client joins. * Added crypto testing * Communication is now functional. --- .gitignore | 1 + src/cipher.py | 55 ++++++ src/cli.py | 162 ++++++++++++++++++ src/client/cli.py | 41 ----- src/client/config.yml | 3 - src/client/encrypt_decrypt.py | 35 ---- src/client/protocol.yml | 0 src/client/test_config.yml | 3 - src/e_d_test.py | 33 ++++ src/encrypt_decrypt.py | 34 ++++ src/hub.py | 215 ++++++++++++++++++++++++ src/hub/hub.py | 36 ---- src/hub/test_config.yml | 7 - src/iot.py | 79 +++++++++ src/protocol.json | 70 ++++++++ src/{hub/config.yml => test_config.yml} | 3 + 16 files changed, 652 insertions(+), 125 deletions(-) create mode 100644 src/cipher.py create mode 100644 src/cli.py delete mode 100644 src/client/cli.py delete mode 100644 src/client/config.yml delete mode 100755 src/client/encrypt_decrypt.py delete mode 100644 src/client/protocol.yml delete mode 100644 src/client/test_config.yml create mode 100644 src/e_d_test.py create mode 100755 src/encrypt_decrypt.py create mode 100644 src/hub.py delete mode 100644 src/hub/hub.py delete mode 100644 src/hub/test_config.yml create mode 100644 src/iot.py create mode 100644 src/protocol.json rename src/{hub/config.yml => test_config.yml} (67%) diff --git a/.gitignore b/.gitignore index 1ae7480..e817604 100755 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.py[cod] *$py.class config.yaml +config.yml # C extensions *.so diff --git a/src/cipher.py b/src/cipher.py new file mode 100644 index 0000000..378eb4c --- /dev/null +++ b/src/cipher.py @@ -0,0 +1,55 @@ +import time +import json +from random import random + +class Client: + def __init__(self, ws): + self.new = True + self.ws = ws + self.n = None + self.e = None + + +def log_prefix(t, client=None): + if client is None: + return str(time.time()) + ' [' + t + ':srv] ' + return str(time.time()) + ' [' + t + ':' + str(client['id']) + '] ' + + +def load_msg(message): + try: + msg = json.loads(message) + except: + msg = message + return msg + + +def load_pub_key(cli, msg): + try: + cli.n = msg['pub_key']['n'] + cli.e = msg['pub_key']['e'] + return True + except: + print('\tMessage had no attribute pub_key. Failed.') + return False + + +def gen_comm_msg(to, ident, msg): + message = { + 'type':'message', + 'to': to, + 'id': ident, + 'contents': msg + } + return json.dumps(message) + + +def gen_pub_key_msg(n, e): + message = { + 'type': 'pub_key', + 'pub_key':{ + 'n':n, + 'e':e + } + } + return json.dumps(message) diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..53a1fb1 --- /dev/null +++ b/src/cli.py @@ -0,0 +1,162 @@ +""" +Python client based on https://pypi.org/project/websocket-client/ + +@author hornetfighter515 +@author Todorov-Lachezar +""" + +import websocket +import _thread +import time +import ssl +import yaml +import json +import rsa +import random # for message id generation +import cipher + +ext = False +authed = False +clis = [] +messages = {} + +def authed_ext_send(ws, msg): + msg = rsa.encrypt(msg.encode(), clis[0]).decode('latin') + message_id = random.random() + if len(clis) <= 1: # if there are no IoTs + # send the message once-encrypted + message = cipher.gen_comm_msg(0, message_id, msg) + ws.send(message) + return + # otherwise, slice and twice encrypt it + twice_es = [] + modifier = len(msg)/(len(clis)-1) + for i in range(1,len(clis)): + start = int( (i-1) * modifier) + end = int(i*modifier) + # makes a slice of the encrypted message, and indicates the intended + # recipient + contents = rsa.encrypt(msg[start:end].encode('latin'), + clis[i]).decode('latin') + message = cipher.gen_comm_msg(i, message_id, contents) + twice_es.append(message) + for m in twice_es: + ws.send(m) + + +def authed_int_send(ws, msg): + to = int(input(f"Who's it to? 0-{len(clis)}>")) + e_msg = rsa.encrypt(msg.encode(), clis[int(to)]).decode('latin') + message_id = random.random() + msg = cipher.gen_comm_msg(to, message_id, e_msg) + ws.send(msg) + + +def init_send(ws): + message = cipher.gen_pub_key_msg(pub['n'], pub['e']) + ws.send(message) + + +def send(ws, msg): + try: + if ext: + authed_ext_send(ws, msg) + else: + authed_int_send(ws, msg) + except Exception as e: + print(f'Sending message {msg} failed: {e}') + + +def on_message(ws, message): + msg = json.loads(message) + global authed + if msg['type'] == 'pub_key': + authed = True + if ext: + k = rsa.PublicKey(msg['available_cli']['n'], + msg['available_cli']['e']) + if len(clis) == 0: + clis.append(k) + else: + clis[0] = k + for i,p in msg['available_iots'].items(): + k = rsa.PublicKey(p['n'], p['e']) + clis.append(k) + else: + k = rsa.PublicKey(msg['available_cli']['n'], + msg['available_cli']['e']) + clis.append(k) + + elif msg['type'] == 'message': + # check if it's only a partial message + try: + if 'from' in msg: + if msg['id'] not in messages: + messages[msg['id']] = {} + messages[msg['id']][msg['from']] = msg['contents'] + contents = None + for i in range(0, 3): + if i in messages[msg['id']]: + if contents is None: + contents = messages[msg['id']][i].encode('latin') + else: + contents += messages[msg['id']][i].encode('latin') + if len(contents) == 256: + m = rsa.decrypt(contents, priv).decode() + else: + m = ' ' + else: + contents = msg['contents'].encode('latin') + m = rsa.decrypt(contents, priv).decode() + if m is not None: + print(f"\033[F\033[1G<< {m}\n> ") + except Exception as e: + print(f'Failed to receive message due to {e}') + + +def on_error(ws, error): + print(error) + + +def on_close(ws, close_status_code, close_msg): + print(f"### closed. reason: {close_msg} ###") + + +def on_open(ws): + def run(*args): + init_send(ws) + running = True + while running: + outbound = input(">") + if outbound == 'q': + running = False + else: + send(ws, outbound) + ws.close() + _thread.start_new_thread(run, ()) + + +def open_socket(url, port): + ws = websocket.WebSocketApp(f"ws://{url}:{port}", + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close) + ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) + + +if __name__ == "__main__": + with open('src/config.yml', 'r') as f: + conf = yaml.safe_load(f) + + e = input("Are you outside of the network? (Y/n)") + if e == 'y' or e == 'Y' or e == 'yes' or e == 'Yes': + ext = True + ws_conf = conf['ws']['ext'] + else: + ws_conf = conf['ws']['int'] + print('Generating keys...') + pub, priv = rsa.newkeys(2048) + print('Keys generated') + open_socket(ws_conf['url'], ws_conf['port']) + diff --git a/src/client/cli.py b/src/client/cli.py deleted file mode 100644 index ea434ec..0000000 --- a/src/client/cli.py +++ /dev/null @@ -1,41 +0,0 @@ -import websocket -import _thread -import time -import ssl -import yaml - -def on_message(ws, message): - print(f"<< {message}") - -def on_error(ws, error): - print(error) - -def on_close(ws, close_status_code, close_msg): - print(f"### closed. reason: {close_msg} ###") - -def on_open(ws): - def run(*args): - running = True - while running: - outbound = input(">") - if outbound == 'q': - running = False - else: - ws.send(outbound) - ws.close() - _thread.start_new_thread(run, ()) - -def open_socket(url, port): - ws = websocket.WebSocketApp(f"ws://{url}:{port}", - on_open=on_open, - on_message=on_message, - on_error=on_error, - on_close=on_close) - ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) - - -if __name__ == "__main__": - with open('config.yaml', 'r') as f: - conf = yaml.safe_load(f) - open_socket(conf['ws']['url'], conf['ws']['port']) - diff --git a/src/client/config.yml b/src/client/config.yml deleted file mode 100644 index f030a27..0000000 --- a/src/client/config.yml +++ /dev/null @@ -1,3 +0,0 @@ -ws: - url: localhost - port: 6873 diff --git a/src/client/encrypt_decrypt.py b/src/client/encrypt_decrypt.py deleted file mode 100755 index 76ef455..0000000 --- a/src/client/encrypt_decrypt.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Encrypting and decrypting a message - -https://www.geeksforgeeks.org/how-to-encrypt-and-decrypt-strings-in-python/ - -@author Todorov-Lachezar -""" - -import rsa - -class EncryptDecrypt(): - - def make_keys(key_length): - publicKey, privateKey = "" - # Generates public and private keys - # Method takes in key length as a parameter - # Note: the key length should be >16 - if(key_length < 16): - publicKey, privateKey = rsa.newkeys(key_length) - return publicKey, privateKey - else: - return "Enter a key_length of >16" - - - def encryption(plaintext, publicKey): - # Encrypts the message with the public key - # Note: make sure to encode the message before encrypting - encMessage = rsa.encrypt(plaintext.encode(), publicKey) - return encMessage - - def decryption(ciphertext, privateKey): - # Decrypts the encrypted message with the private key - # Note: make sure to decode the message after decryption - decMessage = rsa.decrypt(ciphertext, privateKey).decode() - return decMessage diff --git a/src/client/protocol.yml b/src/client/protocol.yml deleted file mode 100644 index e69de29..0000000 diff --git a/src/client/test_config.yml b/src/client/test_config.yml deleted file mode 100644 index 23ac0a0..0000000 --- a/src/client/test_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -ws: - url: localhost - port: 6873 diff --git a/src/e_d_test.py b/src/e_d_test.py new file mode 100644 index 0000000..eb589d4 --- /dev/null +++ b/src/e_d_test.py @@ -0,0 +1,33 @@ +""" +oh no + +@author hornetfighter515 +""" +import rsa + +print('Generating keys...') + +(pub_one, priv_one) = rsa.newkeys(2048) +(pub_two, priv_two) = rsa.newkeys(2048) +(pub_three, priv_three) = rsa.newkeys(2048) + +print('Keys generated!') + +message = 'hello world' + +e_one = rsa.encrypt(message.encode(), pub_one) +e_two = rsa.encrypt(e_one[:128], pub_two) +e_three = rsa.encrypt(e_one[128:], pub_three) + +print('Successfully encrypted!') + +d_three = rsa.decrypt(e_three, priv_three) +d_two = rsa.decrypt(e_two, priv_two) +print(str(e_one)) +print('-----------------------------') +print(str(d_two + d_three)) +print(str(e_one) == str(d_two + d_three)) + +d_one = rsa.decrypt(d_two + d_three, priv_one) + +print('Successfully decrypted ' + d_one.decode()) diff --git a/src/encrypt_decrypt.py b/src/encrypt_decrypt.py new file mode 100755 index 0000000..e265350 --- /dev/null +++ b/src/encrypt_decrypt.py @@ -0,0 +1,34 @@ +""" +Encrypting and decrypting a message + +https://www.geeksforgeeks.org/how-to-encrypt-and-decrypt-strings-in-python/ + +@author hornetfighter515 +@author Todorov-Lachezar +""" + +import rsa + +def make_keys(key_length): + publicKey, privateKey = None,None + # Generates public and private keys + # Method takes in key length as a parameter + # Note: the key length should be >16 + if(key_length > 16): + (publicKey, privateKey) = rsa.newkeys(key_length) + return publicKey, privateKey + else: + return "Enter a key_length of >16" + + +def encryption(plaintext, publicKey): + # Encrypts the message with the public key + # Note: make sure to encode the message before encrypting + encMessage = rsa.encrypt(plaintext.encode(), publicKey) + return encMessage + +def decryption(ciphertext, privateKey): + # Decrypts the encrypted message with the private key + # Note: make sure to decode the message after decryption + decMessage = rsa.decrypt(ciphertext, privateKey).decode() + return decMessage diff --git a/src/hub.py b/src/hub.py new file mode 100644 index 0000000..b69d664 --- /dev/null +++ b/src/hub.py @@ -0,0 +1,215 @@ +""" +Creates hub for distributed IoT + +Websocket Server docs: https://github.com/Pithikos/python-websocket-server + +@author hornetfighter515 +@author Lachezar Todorov +""" + +import threading +from websocket_server import WebsocketServer +import time +import json +from random import random + +iot_devs = {} +e_clients = {} +i_client = None +name='' + +ext_srv = None +int_srv = None +iot_srv = None + +class Client: + def __init__(self, ws): + self.new = True + self.ws = ws + self.n = None + self.e = None + + +def _log_prefix(t, client=None): + if client is None: + return str(time.time()) + ' [' + t + ':srv] ' + return str(time.time()) + ' [' + t + ':' + str(client['id']) + '] ' + +# # +#################### INCOMING MESSAGES ######################################## +# # + +def _load_msg(message): + try: + msg = json.loads(message) + except: + msg = message + return msg + + +def _pub_key(cli, msg): + try: + cli.n = msg['pub_key']['n'] + cli.e = msg['pub_key']['e'] + return True + except: + print('\tMessage had no attribute pub_key. Failed.') + return False + + +def _cli_pubs(coll): + res = {} + if len(coll) == 0: + return res + for c, cli in coll.items(): + res[c] = { + "n":cli.n, + "e":cli.e + } + return res + + +def ext_msg(client, server, message): + # external coming in to network + print(_log_prefix('ext', client), 'sent message.') + cli = e_clients[client['id']] + msg = _load_msg(message) + if cli.new: + if _pub_key(cli, msg): + # give IoTs or cli + if len(iot_devs) > 0: + print('\tThere are IoTs to send to.') + else: + print('\tCommunicate directly with client.') + cli.new = False + int_srv.send_message(i_client.ws, json.dumps({ + "type":"pub_key", + "available_cli":{ + "n":cli.n, + "e":cli.e + } + })) + ext_srv.send_message(client, json.dumps({ + "type":"pub_key", + "name":name, + "available_cli":{ + "n":i_client.n, + "e":i_client.e + }, + "available_iots": _cli_pubs(iot_devs) + })) + else: + print(f'\tMessage {msg["id"]} goes to {msg["to"]}') + if msg['to'] == 0: + print('\tTo internal client') + int_srv.send_message(i_client.ws, message) + else: + iot_srv.send_message(iot_devs[msg['to']].ws, message) + print('\tTo IoT device') + + +def int_msg(client, server, message): + # internal leaving network + print(_log_prefix('int', client), 'Sent message.' ) + msg = _load_msg(message) + if msg['type'] == 'pub_key': + if i_client.new: + if _pub_key(i_client, msg): + print(f'\tinternal client successfully authed.') + i_client.new = False + return + elif msg['type'] == 'message': + global e_clients + to = msg['to'] + 1 + ext_srv.send_message(e_clients[to].ws, message) + print('\tMessage sent to external client') + + +def iot_msg(client, server, message): + # Partially decrypted message coming back from IoT devices + print(_log_prefix('iot', client), 'sent message.') + iot = iot_devs[client['id']] + msg = _load_msg(message) + if iot.new: + if _pub_key(iot, msg): + print(f'\tIoT {client["id"]} successfully authed.') + iot.new = False + else: + if i_client is not None: + int_srv.send_message(i_client.ws, message) + print('\tInternal client safe to send to.') + else: + print('\tInternal client disconnected.') + +# # +######################### NEW CONNECTIONS ##################################### +# # + +def ext_new(client, server): + global e_clients + if client['id'] not in e_clients: + e_clients[client['id']] = Client(client) + print(_log_prefix('ext', client) + 'Client joined.') + # give list of iots and their pubkeys + +def int_new(client, server): + print(_log_prefix('int', client) + 'Client joined.') + global i_client + if i_client is None: + i_client = Client(client) + print('\tInternal client set.') + +def iot_new(client, server): + global iot_devs + if client['id'] not in iot_devs: + iot_devs[client['id']] = Client(client) + print(_log_prefix('iot', client) + 'Device joined.') + + +def start_server(conf, new, msg, prefix): + ws = WebsocketServer(host=conf['url'], port=conf['port']) + ws.set_fn_new_client(new) + ws.set_fn_message_received(msg) + wst = threading.Thread(target=ws.run_forever) + wst.daemon = True + wst.start() + print(_log_prefix(prefix), 'Running...') + return ws + +def start_servers(ext_conf, int_conf, iot_conf): + """ + Starts a server for each type of device. + Why create a different server for each client type? Separation of concerns + is easier this way. Messages don't need to be sorted as heavily by which + client they came from. Internal ports can more easily be firewalled, or not + exposed to the outside internet at all. + @param host: the host it is being run on + @param port_e: the externally exposed port for clients outside the network + @param port_i: the internal port for clients to connect to + @param port_iot: the port for IoT devices to connect to + """ + # websocket server exposed, or external, for external clients + global ext_srv + ext_srv = start_server(ext_conf, ext_new, ext_msg, 'ext') + + # websocket server internal + global int_srv + int_srv = start_server(int_conf, int_new, int_msg, 'int') + + # websocket server for IoT devices + global iot_srv + iot_srv = start_server(iot_conf, iot_new, iot_msg, 'iot') + + +if __name__ == "__main__": + import yaml + with open('src/config.yml', 'r') as f: + conf = yaml.safe_load(f) + ext_conf = conf['ws']['ext'] + int_conf = conf['ws']['int'] + iot_conf = conf['ws']['iot'] + start_servers(ext_conf, int_conf, iot_conf) + while True: + time.sleep(1) + + diff --git a/src/hub/hub.py b/src/hub/hub.py deleted file mode 100644 index aee6c43..0000000 --- a/src/hub/hub.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Creates hub for distributed IoT - -@author hornetfighter515 -@author Lachezar Todorov -""" - -from websocket_server import WebsocketServer -import time - -class Serv(): - def __init__(self, host, port): - self.host = host - self.port = port - self.ws_s = WebsocketServer(host=host, port=port) - self.ws_s.set_fn_message_received(self.message_received) - self.ws_s.set_fn_new_client(self.new_client) - self.ws_s.run_forever() - - def _log_prefix(self): - return str(time.time()) + ' [' + self.host + ':' + str(self.port) + '] ' - - def new_client(self, client, server): - print(self._log_prefix() + 'Client joined.') - - def message_received(self, client, server, message): - # figure out how to identify clients... - print(self._log_prefix() + message ) - - -if __name__ == "__main__": - import yaml - with open('config.yml', 'r') as f: - conf = yaml.safe_load(f) - ws_conf = conf['ws'] - ext_server = Serv(ws_conf['ext']['url'], ws_conf['ext']['port']) diff --git a/src/hub/test_config.yml b/src/hub/test_config.yml deleted file mode 100644 index e37f41a..0000000 --- a/src/hub/test_config.yml +++ /dev/null @@ -1,7 +0,0 @@ -ws: - ext: - url: localhost - port: 6873 - int: - url: localhost - port: 6872 diff --git a/src/iot.py b/src/iot.py new file mode 100644 index 0000000..b70aab9 --- /dev/null +++ b/src/iot.py @@ -0,0 +1,79 @@ +""" +A middleman IoT websocket device which decrypts and re-encrypts a message + +@author hornetfighter515 +@author Lachezar Todorov +""" +import websocket +import _thread +import time +import yaml +import json +import rsa + + +def send(ws, message): + try: + ws.send(json.dumps(message)) + except: + print(f'Sending of message {message} failed.') + + +def on_message(ws, message): + # decrypt the message + #try: + if True: + msg = json.loads(message) + contents = msg['contents'].encode('latin') + res = { + "from":msg['to'], + "to":0, + "type":'message', + "id":msg['id'], + "contents": rsa.decrypt(contents, priv).decode('latin') + } + print('Forwarding message...') + send(ws, res) + #except: + # print('Sending failed.') + # send(ws, message) + + +def on_error(ws, error): + print(error) + +def on_close(ws, close_status_code, close_msg): + print(f"### closed. reason: {close_msg} ###") + +def on_open(ws): + def run(*args): + send(ws, { + "pub_key":{ + "n":pub.n, + "e":pub.e + } + }) + running = True + while running: + time.sleep(1) + _thread.start_new_thread(run, ()) + +def open_socket(url, port): + ws = websocket.WebSocketApp(f"ws://{url}:{port}", + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close) + ws.run_forever() + + +if __name__ == "__main__": + with open('src/config.yml', 'r') as f: + conf = yaml.safe_load(f) + global pub, priv + # eventually put this in config + print('Generating keys...') + pub, priv = rsa.newkeys(2048) + print('Keys generated!') + open_socket(conf['ws']['iot']['url'], conf['ws']['iot']['port']) + diff --git a/src/protocol.json b/src/protocol.json new file mode 100644 index 0000000..54ecc8b --- /dev/null +++ b/src/protocol.json @@ -0,0 +1,70 @@ +{ + "ext-srv":{ + "init":{ + "name":"name", + "pub_key":"pub_key" + }, + "msg":{ + "id":0, + "to":0, + "contents":"contents" + } + }, + "srv-ext":{ + "init":{ + "name":"name", + "available_cli": "public_key_zero", + "available_iots":[ + "public_key_one", + "public_key_two" + ] + }, + "msg":{ + "contents":"contents" + } + }, + "int-srv":{ + "init":{ + "name":"name", + "pub_key":"public_key_zero" + }, + "msg":{ + "to":0, + "contents":"contents" + } + }, + "srv-int":{ + "init":{ + "available_clients":[ + "clients" + ] + }, + "msg":{ + "id":0, + "position":0, + "contents":"contents" + }, + "sys":{ + "msg" + } + }, + "iot-srv":{ + "init":{ + "id":0, + "pub_key":"public_key_one" + }, + "msg":{ + "id":0, + "contents":"contents" + } + }, + "srv-iot":{ + "init":{ + "client_available":false + }, + "msg":{ + "id":0, + "contents":"contents" + } + } +} diff --git a/src/hub/config.yml b/src/test_config.yml similarity index 67% rename from src/hub/config.yml rename to src/test_config.yml index 3a4146a..124448f 100644 --- a/src/hub/config.yml +++ b/src/test_config.yml @@ -5,3 +5,6 @@ ws: int: url: localhost port: 6872 + iot: + url: localhost + port: 6871