For learning purposes I am trying to make a encrypted chat in Python using Websockets. I am using AES and RSA to make it safe.
The idea of the chat is that it's a group-chat. Users can join a chat by running python3 client.py --password=pass123
.
This is how it works.
- The server has already generated a RSA keypair.
- The client enters a
password
when running theclient.py
script. - The client requests the public key from the server > the server sends this key to the client.
- The client encrypts the
password
using the public RSA key and sends the encrypted string to the server. - The server decrypts the encrypted string using the private RSA-key so it knows the
password
the client has inputted. - Using that
password
the server generates a derived key using PBKDF2_HMAC and a Bcrypt-salt (the Bcrypt-salt is rotated for each chat-session). - The server sends the derived key back to the client. (Isn't this a vulnerability??)
- The client generates a AES-key (ECB).
- All messages sent to the server are encrypted using this AES-key.
With this implementation multiple clients can communicate in a chat. But they'll only be able to read the messages sent if they all entered the same password. Someone trying to join the chat with a different password will only see encrypted garbage. Also the server only sees encrypted garbage.
I made a simplified code showing my implementation. server.py
import asyncio
import websockets
import binascii
import bcrypt
import hashlib
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
# 1. Generate a RSA-keypair.
keypair = RSA.generate(3072)
publicKey = keypair.publickey()
publicKeyPEM = publicKey.exportKey()
privateKeyPEM = keypair.exportKey()
# Function to handle connection (Only one connection and one client in this simplified code).
async def handle_connection(websocket, url):
# Server sends public key to client after client requested it.
await websocket.recv()
await websocket.send(publicKeyPEM.decode('ascii'))
# 5. Server decrypts encrypted password using private key.
decryptor = PKCS1_OAEP.new(keypair)
decryptedPassword = decryptor.decrypt(binascii.unhexlify(await websocket.recv()))
# 6. Generate derived key using password.
salt = bcrypt.gensalt() # (Salt unique for each chat session).
derivedKey = hashlib.pbkdf2_hmac('sha-256', decryptedPassword, salt, 600000)
# 7. Server sends derived key back to client.
# (Note: The derived key is sent unencrypted. Only SSL in real life).
await websocket.send(binascii.hexlify(derivedKey).decode('utf-8'))
# Finally: Receive and echo back some messages
incomingMessage = await websocket.recv()
print(incomingMessage)
await websocket.send(incomingMessage)
incomingMessage = await websocket.recv()
print(incomingMessage)
await websocket.send(incomingMessage)
start = websockets.serve(handle_connection, "localhost", 1234)
asyncio.get_event_loop().run_until_complete(start)
asyncio.get_event_loop().run_forever()
client.py
import asyncio
import websockets
import hashlib
import bcrypt
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
import binascii
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
# 2. Client enters a password.
password = b'pass123'
async def connect():
url = "ws://localhost:1234"
async with websockets.connect(url) as websocket:
# 3. Request public RSA key.
await websocket.send("rsa-key-request")
publicKey = RSA.import_key(await websocket.recv())
# 4. Encrypt password using public key and send it to server.
encryptor = PKCS1_OAEP.new(publicKey)
encryptedPassword = encryptor.encrypt(password)
await websocket.send(binascii.hexlify(encryptedPassword).decode('utf-8'))
# 8. Client receives derivedKey and makes AES key
derivedKey = binascii.unhexlify(await websocket.recv())
aesKey = AES.new(derivedKey, AES.MODE_ECB)
# Finally: Send few messages as example
# Msg 1
encryptedMessage = aesKey.encrypt(pad(b'Hello', AES.block_size, style='pkcs7'))
await websocket.send(binascii.hexlify(encryptedMessage).decode('utf-8'))
answer = binascii.unhexlify(await websocket.recv())
decryptedAnswer = unpad(aesKey.decrypt(answer), AES.block_size, style='pkcs7').decode('utf-8')
print(decryptedAnswer)
# Msg 2
encryptedMessage = aesKey.encrypt(pad(b'Rofl 123', AES.block_size, style='pkcs7'))
await websocket.send(binascii.hexlify(encryptedMessage).decode('utf-8'))
answer = binascii.unhexlify(await websocket.recv())
decryptedAnswer = unpad(aesKey.decrypt(answer), AES.block_size, style='pkcs7').decode('utf-8')
print(decryptedAnswer)
if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(connect())
Output:
# server.py
1a0dc63afa380c18190eed51a66d18a0
c85b61942e3115579cf4441a10020e20
# client.py
Hello
Rofl 123
Notes:
- In real I'd use SSL for my websocket connections.
- The simplified code does not support multiple clients.
Questions:
- Is this a good implementation for encryption in a chat?
- I used RSA because I read that RSA was a good way send the derived key from client to server. But I don't understand: The AES key is generated from the derived key. The
password
entered by the client is send encrypted to the server. But the derived key used for the AES is send unencrypted from the server to the client. Couldn't someone with bad intentions not just intercept this derived key and use that to generate the same AES-key? - I first thought it would make more sense to let the client generate a derived key and set it encrypted to the server. But each client would have a different salt?
- If my implementation is wrong, what to change?
- Other things to consider?