I'm a highschool student with a passion for Python, and this is my first try at networking.
server.py
import select
import socket
import sys
import time
from encrypt import AESCipher
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(0)
server_addr = ('localhost',46643)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(server_addr)
passwd = input('You can now enter a password for your server, or press enter for none: ')
enc = AESCipher(passwd)
server.listen(12)
SOCKS = [server]
nicknames = {}
def chat_server():
global SOCKS, nicknames, passwd
while SOCKS:
readable, writable, exception = select.select(SOCKS,[],[],0)
for s in readable:
if s == server:
sock, addr = server.accept()
SOCKS.append(sock)
print('{} connected to server'.format(addr))
nicknames[sock] = addr
broadcast(s,'{} connected to server'.format(addr))
else:
try:
data = s.recv(4096)
except ConnectionResetError:
broadcast(s,'{} killed the connection'.format(nicknames[s]))
print('{} killed the connection'.format(nicknames[s]))
if s in SOCKS:
SOCKS.remove(s)
s.close()
continue
try:
plaintext = enc.decrypt(data)
assert plaintext
except:
continue
if data:
if plaintext.startswith('/help'):
s.send(enc.encrypt('/nick <nickname> - change nickname'))
elif plaintext.startswith('/nick '):
broadcast(s,'{} changed their nickname to {}'.format(nicknames[s],plaintext[6:]))
nicknames[s] = plaintext[6:].strip()
else:
broadcast(s,'{}: {}'.format(nicknames[s],plaintext))
else:
broadcast(s,'{} killed the connection'.format(nicknames[s]))
print('{} killed the connection\n'.format(nicknames[s]))
if s in SOCKS:
SOCKS.remove(s)
s.close()
for s in exception:
broadcast(s,'{} killed the connection'.format(nicknames[s]))
print('{} killed the connection'.format(nicknames[s]))
if s in SOCKS:
SOCKS.remove(s)
s.close()
def broadcast(sock,message):
global server, SOCKS
for s in SOCKS:
if s != server:
try:
s.send(enc.encrypt(message))
except:
s.close()
if s in SOCKS:
SOCKS.remove(s)
chat_server()
client.py
from kivy.app import App
from kivy.lang.builder import Builder
from kivy.uix.screenmanager import ScreenManager
from kivy.properties import ObjectProperty
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.clock import Clock
import select
import socket
import sys
from encrypt import AESCipher
class Chat(Widget):
tout = ObjectProperty(None)
tin = ObjectProperty(None)
send_btn = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super(Chat, self).__init__(*args, **kwargs)
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.settimeout(2)
self.enc = AESCipher('')
self.connected = 0
Clock.schedule_interval(self.chat_client, 0)
def chat_client(self,e):
self.tin.focus = True
if self.connected:
readable, writable, exception = select.select([self.client],[],[],0)
if self.client in readable:
data = self.client.recv(4096)
if not data:
self.tout.text = 'Server connection killed.'
else:
try:
self.tout.text += '\n' + self.enc.decrypt(data)
except:
pass
def send_msg(self):
text = self.tin.text
if text.startswith('#passwd '):
self.enc = AESCipher(text[8:])
elif text.startswith('#connect '):
try:
self.client.connect((text.split(' ')[1], int(text.split(' ')[2])))
self.connected = 1
except:
self.tout.text += '\nConnection to {}:{} failed. If you\'ve already connected to a server, try restarting your client.'.format(text.split(' ')[1], text.split(' ')[2])
self.connected = 0
elif text == '#exit':
sys.exit()
elif self.connected:
self.client.send(self.enc.encrypt(text))
self.tin.text = ''
class Manager(ScreenManager):
pass
class Main(App):
def build(self):
Builder.load_string('''
<Chat>:
tout: tout
tin: tin
send_btn: send_btn
TextInput:
id: tout
width: root.width
height: root.height * 0.94
y: root.height * 0.06
text: "Use #connect <ip> <port> to connect to a server. Use the command `/help` to view a server's commands. Use #passwd <password> if the server you're connected to has encryption."
TextInput:
id: tin
focus: True
multiline: False
width: root.width * 0.9
height: root.height * 0.06
on_text_validate: root.send_msg()
Button:
id: send_btn
width: root.width * 0.1
height: root.height * 0.06
x: root.width * 0.9
text: 'Send'
on_press: root.send_msg()
<Manager>:
Screen:
name: 'chat'
Chat:
id: chat''')
return Manager()
Main().run()
encrypt.py (the encryption snippet was taken from elsewhere - I wanted to focus on networking rather than learning how to use PyCrypto):
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES
# Padding for the input string --not
# related to encryption itself.
BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
class AESCipher:
def __init__(self, key):
self.key = md5(key.encode('utf8')).hexdigest()
def encrypt(self, raw):
raw = pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key.encode(), AES.MODE_CBC, iv)
return b64encode(iv + cipher.encrypt(raw.encode()))
def decrypt(self, enc):
enc = b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key.encode(), AES.MODE_CBC, iv)
return unpad(cipher.decrypt(enc[16:])).decode('utf8')
What I've aimed for is a secure chat server/client, where a number of people can connect. I'd like to focus on looking at:
How secure is this? Could it easily be wiretapped and am I making some stupid mistakes?
Is my code styled ok? Are my variable names properly representative of what they are etc.?
And most of all, is the server/client relationship fairly responsible and not ridiculous?
2 Answers 2
This is pretty clean code, good job. I'll be reviewing each file in order, and then I'll you some extra advice at the end.
server.py
General
If a statement consists of multiple names, like in a tuple, where the names are elements, or in a function call, where the names are arguments, the names should be separated by a comma and a single space. So:
server_addr = ('localhost',46643)
Becomes:
server_addr = ('localhost', 46643)
And similarly:
readable, writable, exception = select.select(SOCKS,[],[],0)
Becomes:
readable, writable, exception = select.select(SOCKS, [], [], 0)
In Python, we typically don't abbreviate names (
passwd
should bepassword
,SOCKS
should beSOCKETS
).Each function or class definition should be separated by two blank lines, except if it is inside a class:
def foo(): print("foo!") def bar(): print("bar!") class Spam: def __init__(self): print("Spam!") def eggs(self): print("eggs!")
Remember, you don't have to follow these rules.
In many programming languages (including in Python), constants follow the
UPPERCASE_WITH_UNDERSCORES
naming convention.SOCK
is initially just the server socket, however, we're appending new sockets to it later, so it isn't a constant.
General
In production level code, using
assert
is barely ever a good idea. Typically, we should be more specific about what went wrong. In case we expectedx
to be aFoo
instance and it is aBar
instance, we raise aTypeError
. If we expectx
to be a positive integer, but it is-5
, we raise aValueError
, etc. This makes debugging a lot easier.a is b
checks ifa
andb
reference the exact same underlying object in memory, whilea == b
returnsa.__eq__(b)
. In the case of theselect.select
call, using the former makes more sense:if s is server:
data = s.recv(4096)
:4096
is a magic number. We can fix this by declaringBUFFER_SIZE = 4096
. There's a couple of reasons to do this:It improves readability;
If we want to change the buffer size later, we don't have to go around replacing every
4096
with our new size, but we just changeBUFFER_SIZE
once. This may not seem like such a big problem right now, but it's a pain to have to do this in big libraries.
Global state in a program is almost never a good thing, and there are most certainly solutions to it in this case. Perhaps we can change
chat_server
's signature to acceptSOCKS
,nicknames
andpassword
.
Security
Using
input
to get a password from a user is potentially insecure, as it is echoed back to the screen while typing, which allows for shoulder surfing. Luckily, the standard library includesgetpass
which solves this problem perfectly:>>> from getpass import getpass >>> getpass("Enter your password: ") Enter your password: 'spam'
client.py
Disclaimer: I don't know Kivy, so I can't tell if you're following Kivy best practices.
General
- It's probably best to be explicit about
Chat.connected
representing a boolean value, and switch betweenTrue
/False
.
Other concerns
TCP is not a message-based protocol, which means that there's no guarantee that a
send
call on one machine will match arecv
call on the other. This is very important, because it means naive implementations will break if lots of data is being sent simultaneously. There's no way to tell if data that we receive is a fragment of a previous message, the start of a new one, or a combination of both. To fix this, we have to implement our own 'message' protocol. There's three well-known ways to do this:Messages are fixed-length. This works if we can split up each message into chunks of the same size, but we can't use this in a chat, because it'll interrupt the natural flow of text.
Messages are delimited. This is a possibility, but it's easy to mess up if you forget escaping user's messages.
Messages are prefixed by their size. This is, in my opinion, the way to go.
Although PyCrypto is the de facto cryptography library for Python, I think it's time to switch to
cryptography
, because it's more modern, easier to work with, and harder to mess up with. It features Fernet, a high-level wrapper around AES implementing AES 128-CBC (as well as HMAC-SHA256).The snippet implementing AES is treating MD5 as a key derivation function, which it definitely is not! I strongly suggest using PBKDF2 instead.
cryptography
implementation hereBy sending the password in plaintext to the server, anyone listening in on the network can intercept it and decrypt all future messages, which is why it's essential to use an asymmetric encryption algorithm, like RSA. Unlike symmetric encryption, asymmetric encryption involves a (private_key, public_key) keypair. Let's say Alice creates a keypair. Bob wants to talk to Alice, but he can't have anyone listening in, so he asks Alice for her public key. Alice sends it to Bob and Bob encrypts his message with it. When Alice receives the encrypted message, she can decrypt it using her private key, which she keeps all to herself. The way in which the public key & private key are involved in encryption / decryption is really complicated, so I won't bore you with the details. (If you do want to know more, this is a nice demo.)
-
\$\begingroup\$ Thank you so much! This is really helpful advice. Although I'm unsure about some of the concerns, I will research them further to gain a greater understanding. And, for the encryption bit, I don't think the password is ever sent to the server- just used to decrypt? I'm sorry if I'm incorrect in my understanding. There are some great videos on encryption over at khan academy, which I'll go look at to refresh my memory again. Thank you once again, I will keep all this in mind :) \$\endgroup\$user153161– user1531612017年11月11日 22:24:05 +00:00Commented Nov 11, 2017 at 22:24
-
\$\begingroup\$ You're welcome! And indeed, regarding the password being sent over the network, that was my mistake for mixing the
tin
,tout
'streams' =) \$\endgroup\$Daniel– Daniel2017年11月11日 22:33:00 +00:00Commented Nov 11, 2017 at 22:33
A few minor things:
- Indentation matter in Python, and PEP8 says to use indentation of 4 spaces. It will make your code more readable
- Remove unused modules, I dont think you use
md5
anywhere, secondlymd5
is not consided secure. Better to use a better hashing algorithm like Bcrypt - You could use a
if __name__ == '__main__'
block - I'm guessing this is run from the command line, you might want to consider using argparse instead of the
input('An input:')
you have.
Security
How secure is this? Could it easily be wiretapped?
- Yes, it could be easily tapped. You can assume any
localhost
connection can be tapped (With for instance a MiTM). - The only way to negate this is to ensure the data is being encrypted before sending it.