3
\$\begingroup\$

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.

  1. The server has already generated a RSA keypair.
  2. The client enters a password when running the client.py script.
  3. The client requests the public key from the server > the server sends this key to the client.
  4. The client encrypts the password using the public RSA key and sends the encrypted string to the server.
  5. The server decrypts the encrypted string using the private RSA-key so it knows the password the client has inputted.
  6. 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).
  7. The server sends the derived key back to the client. (Isn't this a vulnerability??)
  8. The client generates a AES-key (ECB).
  9. 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?
\$\endgroup\$
2
  • \$\begingroup\$ Do you have the non-simplified code around? "The simplified code does not support multiple clients." We'd much rather work with your actual code, as then when we give advice the likelihood of us missing the mark is reduced. No-one enjoys "my actual code does x instead" comments. \$\endgroup\$
    – Peilonrayz
    Dec 9, 2023 at 1:40
  • \$\begingroup\$ cross posted from SO \$\endgroup\$
    – Peilonrayz
    Dec 9, 2023 at 1:42

1 Answer 1

2
\$\begingroup\$

Is this a good implementation for encryption in a chat?

No.

I'd argue that most chat implementations are not dependent on the server to decrypt / reencrypt the information (this is called a proxy in cryptography). This is not the end-to-end encryption that e.g. Signal or even WhatsApp uses.

Step 3 is insecure. There is a need to trust the public key of the server. This is why your browser contain a list of trusted (root) certificates. Although most users are unaware, we trust the CA's that issued those root certificates by trusting the producers of the browser (and this is how many browser manufacturers also try and get some money).

You have used the correct primitives such as PBKDF2, AES and RSA. However, you do use AES in ECB mode, which is known to be insecure for encrypting arbitrary messages.

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?

Yes, they could and this is not a good protocol.

It is possible derive a master / session key using key agreement (DH). Generally it is best to only use the password for authentication.

It is said that in real life TLS is used, but I'd argue that in that case you might as well send the password unencrypted from client to server. That is what HTTP basic authentication does - as there is already an established secure channel where the server is trusted.

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?

Salts can be send to the other side unencrypted; they don't need to be secret.

If my implementation is wrong, what to change?

I don't know, but if I read this text then I'm pretty sure that you are a novice at this. In that case you'd start by studying what is already out there.

Other things to consider?

Yeah, don't be proud and don't think you can create a secure chat from scratch. To do that it is important to first learn what is out there.

Crypto remarks

AES should be using an authenticated mode such as AES-GCM. Currently the protocol uses ECB which is not secure for a transport mode. Actually, it seems the implementation doesn't even protect the confidentiality of messages as it fails to protect against plaintext and particularly padding oracle attacks.


Salts are usually just 16 to 32 random bytes. There is no need to depend on code of bcrypt if PBKDF2 is used for the actual implementation.


"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."

That's a claim that isn't true at all. I don't see how the server is supposed to authenticate & trust the clients. Nor do I see how the client is supposed to authenticate & trust the server. The protocol only partially protects against eavesdropping by an adversary, and that is not considered enough security by anyone.


It is always a good idea to create a good description of the protocol. Currently I don't see any way to e.g. update the iteration count, which is set at 600000 (using a literal, which is also not great).

Other remarks

Websockets are perfectly able to send out binary data. Funny enough we see that hexlify is used followed by a "decoding" to a string. If the HTTP protocol & API is followed then it is easy to see that binary can be send without converting to text.

Conclusion

Sorry to be a bit harsh here, but this protocol doesn't add much on top of TLS. The code is easy to read. It should however be considered a learning exercise and nothing more.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks for your answer. I just read that Whatsapp uses asymmetric encryption as well. Would it be secure to give the server a public key, and the users a USB-stick with the private key. So only the users with the correct private key can decrypt it? \$\endgroup\$
    – O'Niel
    Dec 9, 2023 at 21:46
  • \$\begingroup\$ That could be an option for users to authenticate for instance. If you can do key agreement + authentication of the handshake then you can establish the keys securely (this is also e.g. TLS does). Even better if that USB device is something that also protects the private key, e.g. a YubiKey. Generally the authentication part and the session key generation are largely decoupled. \$\endgroup\$ Dec 9, 2023 at 22:13
  • \$\begingroup\$ Okay thanks. And if I then generate a salt server side for each session. And use argond2id with that salt. I could generate the same AES-key server side using the password the two users input. That way it is E2EE? \$\endgroup\$
    – O'Niel
    Dec 9, 2023 at 22:15
  • \$\begingroup\$ Nah, if your server generates or is able to generate the keys then it knows the keys and so it is not E2E. If you e.g. use DH key agreement on both clients then the server can simply forward the DH public keys. The users then could e.g. authenticate the DH public keys using RSA. In that case the server cannot act as a man in the middle. Note that creating a transport protocol is hard; creating a chat protocol is significantly harder, even for me (and I'm top cryptography user on Stack Overflow). I've seen many attempts from crypto novices fail at the first hurdle; it is really tricky. \$\endgroup\$ Dec 9, 2023 at 22:29
  • \$\begingroup\$ I is indeed really complex but oh so interesting. I made a typo: I meant - generate salt server side > send salt to clients > Clients generate AES-key with the salt + password they enter client-side. Server can't generate AES-key because it doesn't know what password the clients agreed to use for the session. \$\endgroup\$
    – O'Niel
    Dec 9, 2023 at 22:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.