Encrypted Chat: Part III

This post is part three of a multi-part series. If you haven’t already, please read parts one and two.

It’s been a while…

Sorry for the extreme (four month!) delay on this series. I got busy with other things and it became hard to return to it, being unsure if anyone would still be interested. Anyways, I felt it should be completed so here we go!

The Code [Cont.]

Unless you have super-human memory, here’s a brief refresher of what I covered in Part II:

  1. Initializing the Server
    a. Creating the socket
    b. Generating p and g for the DHKE
    c. Listening for incoming clients
  2. Initializing a client (as the server)
    a. Creating a private/public key pair
    b. Sending p, g, and the public key in a standardized format
    c. Receiving the client’s public key
    d. Calculating the shared key from p, the private key, and the client’s public key

The last line of code shown started a new thread to listen for incoming messages from the newly initialized client:

# Listen for incoming messages from client
threading.Thread(target=self.listen, args=(client, )).start()

This allows the server to initialize new clients while still listening to this one. The server’s listen method does the following:

  1. Receive 1024 bytes of data from the client
  2. Decrypt the data with the client’s shared key
  3. Broadcast the decrypted message to the rest of the clients on the server
  4. Repeat!
def listen(self, client):
    """
    Receive and handle data from a client.
    :param client: client to receive data from
    """
    while True:
        try:
            # Wait for data from client
            data = client.connection.recv(1024)
            # Disconnect client if no data received
            if not data:
                self.disconnect(client)
                break
            print("{} [Raw]: {}".format(client.address[0], data))
            # Parse data as cipher-text message
            msg = Message(key=client.key, ciphertext=data)
            print("{} [Decrypted]: {}".format(client.address[0], msg.plaintext))
            if msg.plaintext == "!exit":
                client.send("Acknowledged")
                self.disconnect(client)
                continue
            self.broadcast(msg.plaintext, client)
        # Disconnect client if unable to read from connection
        except OSError:
            self.disconnect(client)
            break

The Message class takes care of the encryption and decryption of messages using the shared AES key we generated with the client. You can find it in cipher.py.

When the server broadcasts the message, it has to re-encrypt the plaintext with each recipient’s respective key. Here is the server’s method for doing so:

def broadcast(self, content, from_client, show_address=True):
    if show_address:
        msg = from_client.address[0] + ": " + content
    else:
        msg = content
    [client.send(msg) for client in self.clients if client is not from_client]

It looks through its list of clients and sends the message to everyone who is not the sender.
And here is the send method inside of the client:

def send(self, content):
    """
    Encrypt and send a message to the client
    :param content: plaintext content to be encrypted
    """
    msg = Message(key=self.key, plaintext=content)
    self.connection.sendall(msg.pack())

It encrypts the message with the client’s key and packages it (by concatenating the initialization vector and the ciphertext) to be sent over the socket.

And that’s it for the server!

3. The Client and the Command-Line Interface

I’m going to skip most of the interface-related code since it doesn’t relate to computer security and it isn’t very interesting anyways. I’ll go over its initialization though, since it may be a useful technique in other projects.

Here’s what happens when you run client.py:

  1. A new CLI object is created
    a. The screen/chat window is initialized (I’m using curses)
  2. A new Client object is created (which connects to the chat server) and the CLI object is passed in so the client can write to the screen.
  3. The CLI object is given the Client so that it can send the input it collects from the user.
  4. The client listener (awaiting messages from the server) is started on a separate thread.
  5. The command-line interface (awaiting message input from the user) is started on the main thread.
. . .
interface = CLI()
try:
    c = Client(interface, args.host, port=args.port)
except ConnectionRefusedError:
    interface.clean_exit()
    print("Connection Refused")
    sys.exit()
except OSError:
    interface.clean_exit()
    print("Connection Failed")
    sys.exit()
# Add the client object to the interface
interface.init_client(c)
# Start the client
client_thread = threading.Thread(target=c.start)
client_thread.start()
# Start the main input loop
try:
    interface.main()
except KeyboardInterrupt:
    interface.clean_exit()

Giving the client and the interface references to each-other allows them to communicate across separate threads—a useful approach that can be applied in various projects.

Let’s see what happens when we initialize the Client:

def __init__(self, interface, server_address, port=DEFAULT_PORT):
    """
    Initialize a new client.
    :param server_address: IP address of the server
    :param port: Server port to connect to
    """
    self.cli = interface
    self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.cli.add_msg("Connecting to {}...".format(server_address))
    try:
        self.connection.connect((server_address, port))
    except KeyboardInterrupt:
        self.cli.clean_exit()
        sys.exit()
    self.cli.add_msg("Connected!")
    self.key = None

You’ll see that a new socket is created and used to connect to the provided server, again using IPv4 and TCP. The client also has the ability to call add_msg on the interface to print to the screen, as previously mentioned.

When the client is started, via these two lines from earlier:

client_thread = threading.Thread(target=c.start)
client_thread.start()

the client’s start method is called on another thread. It does the following:

  1. Perform a DHKE with the server to generate a shared key.
  2. Listen for incoming messages
  3. Decrypt incoming messages and display them on the screen.

Let’s see how it accomplishes part 1:

def start(self):
    """
    Start the client: perform key exchange and start listening
    for incoming messages.
    """
    try:
        self.key = self.dh()
    except ConnectionError:
        self.cli.add_msg("Unable to Connect")
        return
. . .
def dh(self):
    """
    Perform Diffie-Hellman Key Exchange with the server.

    p: prime modulus declared by the server
    g: generator declared by the server
    server_key: the server's public key

    private_key: the client's private key
    public_key: the client's public key

    :return shared_key: the 256-bit key both the client and
    server now share
    """
    self.cli.add_msg("Establishing Encryption Key...")
    dh_message = self.connection.recv(DH_MSG_SIZE)
    # Unpack p, g, and server_key from the server's dh message
    p, g, server_key = DH.unpack(dh_message)
    # Generate a randomized private key
    private_key = DH.gen_private_key()
    # Send the server a public key which used the previously
    # Generated private key and both g and p
    public_key = DH.gen_public_key(g, private_key, p)
    self.connection.sendall(DH.package(public_key, LEN_PK))
    # Calculate shared key
    shared_key = DH.get_shared_key(server_key, private_key, p)
    # print("Shared Key: {}".format(shared_key))
    self.cli.add_msg("Encryption Key: {}".format(binascii.hexlify(shared_key).decode("utf-8")))
    return shared_key

As you’ll notice, the client’s dh method is quite similar to that of the server—the only difference being it receives p and g instead of generating them, and is the first to get the other’s public key. It then creates a private/public key pair sends the public key. Finally, it calculates the shared key with the server’s public key, its own private key, and p.

To wrap things up, let’s view the rest of the start method, which covers 2 and 3.

. . .
while True:
    try:
        # Wait for data from server
        data = self.connection.recv(1024)
        # Disconnect from server if no data received
        if not data:
            self.connection.close()
            self.cli.uninit_client()
            break
        # Parse data as cipher-text message
        msg = Message(key=self.key, ciphertext=data)
        if not self.cli:
            break
        # Add message to the command-line interface
        self.cli.add_msg(msg.plaintext)
    # Disconnect client if unable to read from connection
    except OSError:
        self.connection.close()
        self.cli.uninit_client()
        break

Again, this looks very similar to the server code. It attempts to receive 1024 bytes from the server, and once it does, it decrypts the data using the shared key. Lastly, it displays the plaintext message to the screen through the command-line interface.

When the user submits a message, client.send() is called by the interface, which encrypts it and sends it to the server:

def send(self, content):
    """
    Send a message to the server.
    :param content: string to encrypt and send
    """
    if not self.key:
        self.cli.add_msg("Error: Key Not Established")
        return
    msg = Message(key=self.key, plaintext=content)
    self.connection.sendall(msg.pack())

The End.

securechat_screenshot

That completes the encrypted chat series, props to those who stuck around for this long! If you have any questions, I’d be happy to answer them. Just leave a comment or send me a direct message.

P.S.

I’ve also been working on a version 2 of this program that works more like a modern chat program, with user accounts, message storage, chat groups, and cool security features (that were recommended by users of 0x00sec!). It’s still in its early stages, but I’ve already put the code on GitHub if anyone is interested: https://github.com/spec-sec/SecureChat2.

8 Likes

Just curious, is this secure against MitM? AFAIK, DH alone isn’t enough for that.

1 Like

Although the message contents are encrypted when sent over the network and can’t be read without the key, there’s no way for the client to verify the validity of the host they’re connecting to. Therefore, if an attacker pretends to be the server, they could intercept the components of the shared key while forwarding everything to the real server and have a copy of the shared key for themselves. If they did it this way, they would be able to read all of the messages.
I guess this program is kind of like having the encryption part of https, and not the host verification part. Without that, any networking-based program is susceptible to MitM.

2 Likes

You probably know this, but to add to @spec’s answer, the ways I am aware of for host verification (there may be others) are

  1. Manually adding trusted keys/certificates
  2. Key pinning - kind of like 1 but adding the hash of the key instead and check that the has of the received key matches
  3. Certificate authorities
  4. Web of trust
  5. Blockchain with proof of work or other

Of course each has it’s own issues: obviously the centralised ones can be compromised or turn malicious, and the distributed ones (afaik) rely on some heuristic, like participants nodes or a majority of participants being non malicious.

2 Likes

This probably could sound retarded, but, I was curious if you could use something like m2crypto? I saw the implementation briefly with another program in Python, but that’s a long story. Maybe something worth looking into?

Btw, nice post man… thank you for finishing it up and welcome back. With that being said… ~Cheers!

–Techno Forg–

1 Like

I think part of the point of the post was to implement the crypto to see how it could work instead of using an openssl wrapper.

2 Likes

Thanks!
I actually used m2crypto to generate the p and g parameters for diffie-hellman, since that was something I didn’t want to implement myself. I know m2crypto can do the whole process, but what @lkw said is correct, I wanted to try doing it myself so I could learn. It’s a great library though, definitely something I will use more of in the future.

2 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.