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.
This commit is contained in:
hornet 2021-11-28 19:00:46 +00:00 committed by GitHub
parent 5d0a26ca7e
commit a85b08f050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 652 additions and 125 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ __pycache__/
*.py[cod]
*$py.class
config.yaml
config.yml
# C extensions
*.so

55
src/cipher.py Normal file
View File

@ -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)

162
src/cli.py Normal file
View File

@ -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'])

View File

@ -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'])

View File

@ -1,3 +0,0 @@
ws:
url: localhost
port: 6873

View File

@ -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

View File

@ -1,3 +0,0 @@
ws:
url: localhost
port: 6873

33
src/e_d_test.py Normal file
View File

@ -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())

34
src/encrypt_decrypt.py Executable file
View File

@ -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

215
src/hub.py Normal file
View File

@ -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)

View File

@ -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'])

View File

@ -1,7 +0,0 @@
ws:
ext:
url: localhost
port: 6873
int:
url: localhost
port: 6872

79
src/iot.py Normal file
View File

@ -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'])

70
src/protocol.json Normal file
View File

@ -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"
}
}
}

View File

@ -5,3 +5,6 @@ ws:
int:
url: localhost
port: 6872
iot:
url: localhost
port: 6871