Categories

Sending Encrypted Messages with a Simple Chat Program in Python

In the era of cyber threats, privacy and data security have become paramount. One of the most effective ways to protect data during transmission is through encryption. In this article, we will guide you to create a simple chat program in Python that sends encrypted messages.

Before we start, you need to have Python installed on your computer. You also need to install a couple of Python libraries – socket for creating a network connection, and cryptography for encryption. If not already installed, you can add these with pip:

pip install cryptography

Step 1: Creating the Chat Server

Here’s a simple chat server that can accept connections from multiple clients:

import socket
import threading

class Server:
    def __init__(self, host = '127.0.0.1', port = 55555):
        self.host = host
        self.port = port
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.bind((self.host, self.port))
        self.server.listen()
        self.clients = []
        self.nicknames = []
        

    def broadcast(self, message):
        for client in self.clients:
            client.send(message)

    def handle(self, client):
        while True:
            try:
                message = client.recv(1024)
                self.broadcast(message)
            except:
                index = self.clients.index(client)
                self.clients.remove(client)
                client.close()
                nickname = self.nicknames[index]
                self.nicknames.remove(nickname)
                self.broadcast(f'{nickname} left the chat!'.encode('ascii'))
                break

    def receive(self):
        while True:
            client, address = self.server.accept()
            print(f'Connected with {str(address)}')

            client.send('NICK'.encode('ascii'))
            nickname = client.recv(1024).decode('ascii')
            self.nicknames.append(nickname)
            self.clients.append(client)

            print(f'Nickname of the client is {nickname}!')
            self.broadcast(f'{nickname} joined the chat!'.encode('ascii'))
            client.send('Connected to the server!'.encode('ascii'))

            thread = threading.Thread(target=self.handle, args=(client,))
            thread.start()

if __name__ == "__main__":
    server = Server()
    server.receive()

This server will accept connections, handle incoming messages, and broadcast messages to all connected clients.

Step 2: Creating the Chat Client

Next, we create a chat client that can connect to the server and send/receive messages:

import socket
import threading

class Client:
    def __init__(self, host = '127.0.0.1', port = 55555):
        self.nickname = input("Enter your nickname: ")
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect((host, port))

    def receive(self):
        while True:
            try:
                message = self.client.recv(1024).decode('ascii')
                if message == 'NICK':
                    self.client.send(self.nickname.encode('ascii'))
                else:
                    print(message)
            except:
                print("Error!")
                self.client.close()
                break

    def write(self):
        while True:
            message = f'{self.nickname}: {input("")}'
            self.client.send(message.encode('ascii'))

    def run(self):
        receive_thread = threading.Thread(target=self.receive)
        receive_thread.start()

        write_thread = threading.Thread(target=self.write)
        write_thread.start()

if __name__ == "__main__":
    client = Client()
    client.run()

This client will connect to the server, send its nickname, and then send/receive messages.

Step 3: Adding Encryption

So far, we have created a simple chat program, but the messages are sent in plain text. To enhance security, we need to encrypt the messages using the cryptography library. We’ll use Fernet symmetric encryption for simplicity.

First, we generate a key and save it to a file:

from cryptography.fernet import Fernet

key = Fernet.generate_key()
with open("secret.key", "wb") as key_file:
    key_file.write(key)

Both the server and client should have access to this key file. We can load the key with:

def load_key():
    return open("secret.key", "rb").read()

We can create a couple of helper functions for encryption and decryption:

from cryptography.fernet import Fernet

def encrypt_message(message, key):
    encoded_message = message.encode()
    f = Fernet(key)
    encrypted_message = f.encrypt(encoded_message)
    return encrypted_message

def decrypt_message(encrypted_message, key):
    f = Fernet(key)
    decrypted_message = f.decrypt(encrypted_message)
    return decrypted_message.decode()

Now, we can modify our server and client code to use these encryption and decryption functions. For the server, before broadcasting a message, we encrypt it, and before displaying a received message, we decrypt it. Similarly, for the client, before sending a message, we encrypt it, and before printing a received message, we decrypt it.

Before we dive deeper.

The code presented here is a basic implementation and can be further enhanced by handling more edge cases, enhancing user experience, and improving security. Encryption is a vital aspect of secure communication, and using Python to achieve it is straightforward due to its powerful libraries.

How the Server Works

In the previous section, we’ve introduced a Server class that is designed to accept connections from multiple clients, receive messages, and broadcast them to all connected clients. Here, we’ll go through this Server class in detail, breaking down its various components and how they work.

class Server:
    def __init__(self, host = '127.0.0.1', port = 55555):
        self.host = host
        self.port = port
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.bind((self.host, self.port))
        self.server.listen()
        self.clients = []
        self.nicknames = []

In the __init__ method, we initialize our Server class. We create a socket object and bind it to the provided host and port. We then set the server to listen for incoming connections. We also initialize two empty lists, clients and nicknames, to keep track of all connected clients and their corresponding nicknames.

def broadcast(self, message):
    for client in self.clients:
        client.send(message)

The broadcast method is used to send a message to all connected clients. It iterates through the list of clients and sends the message to each one.

def handle(self, client):
    while True:
        try:
            message = client.recv(1024)
            self.broadcast(message)
        except:
            index = self.clients.index(client)
            self.clients.remove(client)
            client.close()
            nickname = self.nicknames[index]
            self.nicknames.remove(nickname)
            self.broadcast(f'{nickname} left the chat!'.encode('ascii'))
            break

The handle method is used to handle incoming messages from each client. For each client, it starts an infinite loop where it tries to receive messages from the client and broadcast them. If receiving a message fails, which can happen if the client has disconnected, it removes the client from the clients list, closes the socket, removes the client’s nickname from the nicknames list, and broadcasts a message to the other clients that this client has left the chat.

def receive(self):
    while True:
        client, address = self.server.accept()
        print(f'Connected with {str(address)}')

        client.send('NICK'.encode('ascii'))
        nickname = client.recv(1024).decode('ascii')
        self.nicknames.append(nickname)
        self.clients.append(client)

        print(f'Nickname of the client is {nickname}!')
        self.broadcast(f'{nickname} joined the chat!'.encode('ascii'))
        client.send('Connected to the server!'.encode('ascii'))

        thread = threading.Thread(target=self.handle, args=(client,))
        thread.start()

The receive method is used to accept new connections. For each new connection, it sends the string ‘NICK’ to prompt the client to send its nickname. It then adds the new client and its nickname to the respective lists, broadcasts a message to the other clients that a new client has joined, and starts a new thread to handle this client’s messages.

if __name__ == "__main__":
    server = Server()
    server.receive()

Finally, if this script is being run directly (and not being imported as a module), it creates a Server object and starts the receive method to start accepting connections.

This sums up the server’s functionality. The main thread of the server is used to accept new connections, while a separate thread is created for each client to handle its messages. All messages are broadcast to all connected clients.

How the Client Works.

Absolutely! In the previous section, we’ve introduced a Client class that is designed to connect to a server and send/receive messages. Now, let’s break down the Client class and how it functions.

class Client:
    def __init__(self, host = '127.0.0.1', port = 55555):
        self.nickname = input("Enter your nickname: ")
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect((host, port))

In the __init__ method, the client is asked to input a nickname which is stored in self.nickname. A socket object is created and it attempts to connect to the server at the given host and port.

def receive(self):
    while True:
        try:
            message = self.client.recv(1024).decode('ascii')
            if message == 'NICK':
                self.client.send(self.nickname.encode('ascii'))
            else:
                print(message)
        except:
            print("Error!")
            self.client.close()
            break

The receive method is designed to continuously listen for incoming messages from the server. It runs an infinite loop that waits for messages and decodes them. If the received message is ‘NICK’, the client sends back its nickname to the server. Any other message is printed to the console. If there’s an exception (like the server disconnecting), it prints an error message, closes the client connection, and breaks the loop.

def write(self):
    while True:
        message = f'{self.nickname}: {input("")}'
        self.client.send(message.encode('ascii'))

The write method allows the client to write messages to the server. It runs an infinite loop that takes input from the user, prefixes it with the client’s nickname, encodes it, and sends it to the server.

def run(self):
    receive_thread = threading.Thread(target=self.receive)
    receive_thread.start()

    write_thread = threading.Thread(target=self.write)
    write_thread.start()

The run method is responsible for running the client’s processes. It creates and starts two threads: one for receiving messages from the server and one for writing messages to the server. The use of separate threads allows the client to write and receive messages simultaneously.

if __name__ == "__main__":
    client = Client()
    client.run()

Finally, if this script is being run directly (and not being imported as a module), it creates a Client object and calls the run method to start the client’s operations.

The client’s main functionality includes sending messages to the server and receiving messages from the server. It uses separate threads to perform these tasks concurrently, enabling real-time chat.

Using the key, in theory.

In our chat program, we’ve used symmetric encryption with the cryptography library, specifically the Fernet encryption method. Symmetric encryption uses the same key for both the encryption and decryption of the message.

Before the client and server can start exchanging encrypted messages, they must agree on a secret key. This secret key must be generated once and shared between the server and the client. It’s crucial to keep this key secure, as anyone with access to the key can decrypt the messages.

Here is how the key is generated:

from cryptography.fernet import Fernet

key = Fernet.generate_key()
with open("secret.key", "wb") as key_file:
    key_file.write(key)

This code generates a new key using Fernet’s generate_key function and writes it to a file named secret.key.

To use this key in encryption and decryption, it needs to be loaded from the file. Here is a simple function to load the key:

def load_key():
    return open("secret.key", "rb").read()

This function simply opens the secret.key file in binary mode, reads the contents, and returns them.

After the key is loaded, it can be used in the encrypt_message and decrypt_message functions:

def encrypt_message(message, key):
    encoded_message = message.encode()
    f = Fernet(key)
    encrypted_message = f.encrypt(encoded_message)
    return encrypted_message

def decrypt_message(encrypted_message, key):
    f = Fernet(key)
    decrypted_message = f.decrypt(encrypted_message)
    return decrypted_message.decode()

In encrypt_message, the plain-text message is first encoded into bytes, then a Fernet object is created with the provided key. This object is used to encrypt the encoded message. The resulting encrypted message is in bytes format and is returned by the function.

In decrypt_message, a Fernet object is again created with the provided key. This object is used to decrypt the encrypted message. The result is a byte string of the original message, which is then decoded back into a regular string and returned by the function.

To send an encrypted message from the client to the server, the client would first call encrypt_message with its message and the shared key, then send the resulting encrypted message. On the server side, when receiving a message, the server would call decrypt_message with the received message and the shared key to obtain the original message.

Likewise, to send an encrypted message from the server to the client, the server would first call encrypt_message with its message and the shared key, then send the resulting encrypted message. On the client side, when receiving a message, the client would call decrypt_message with the received message and the shared key to obtain the original message.

Remember that it is crucial to keep the shared key secure. If an attacker gains access to this key, they can easily decrypt all messages. In a real-world application, you would want to use more advanced key management solutions to securely exchange and store the key.

Using the Key in Practice on the Server.

Sure! Here’s a simple chat server that uses the Fernet symmetric encryption scheme to encrypt and decrypt messages. I will create a “secret.key” file in the same directory as the script, you should replace it with your actual key.

from cryptography.fernet import Fernet
import socket
import threading

class Server:
    def __init__(self, host = '127.0.0.1', port = 55555):
        self.host = host
        self.port = port
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.bind((self.host, self.port))
        self.server.listen()
        self.clients = []
        self.nicknames = []
        self.key = self.load_key()
        self.fernet = Fernet(self.key)

    def load_key(self):
        return open("secret.key", "rb").read()

    def broadcast(self, message):
        encrypted_message = self.fernet.encrypt(message.encode('ascii'))
        for client in self.clients:
            client.send(encrypted_message)

    def handle(self, client):
        while True:
            try:
                encrypted_message = client.recv(1024)
                decrypted_message = self.fernet.decrypt(encrypted_message).decode('ascii')
                self.broadcast(decrypted_message)
            except:
                index = self.clients.index(client)
                self.clients.remove(client)
                client.close()
                nickname = self.nicknames[index]
                self.nicknames.remove(nickname)
                self.broadcast(f'{nickname} left the chat!'.encode('ascii'))
                break

    def receive(self):
        while True:
            client, address = self.server.accept()
            print(f"Connected with {str(address)}")

            client.send('NICK'.encode('ascii'))
            nickname = client.recv(1024).decode('ascii')
            self.nicknames.append(nickname)
            self.clients.append(client)

            print(f'Nickname of the client is {nickname}!')
            self.broadcast(f'{nickname} joined the chat!'.encode('ascii'))
            client.send('Connected to the server!'.encode('ascii'))

            thread = threading.Thread(target=self.handle, args=(client,))
            thread.start()

if __name__ == "__main__":
    server = Server()
    server.receive()

This server program encrypts messages before broadcasting them to clients and decrypts messages as they come in from clients. It uses the Fernet symmetric encryption scheme, where the same key is used for both encryption and decryption.

Please note that the server and client must use the same encryption key for this to work. In this example, the key is loaded from a file named “secret.key”. You would want to implement a secure way of distributing the encryption key to both the server and the client in a real-world scenario.

Practical Client Implementation

Similar to the server, the client uses the key to create a Fernet object, which is then used to encrypt messages before they’re sent and to decrypt messages as they’re received. Let’s look at an example client script that uses symmetric encryption:

import socket
import threading
from cryptography.fernet import Fernet

class Client:
    def __init__(self, host = '127.0.0.1', port = 55555):
        self.nickname = input("Enter your nickname: ")
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.connect((host, port))
        self.key = self.load_key()
        self.fernet = Fernet(self.key)

    def load_key(self):
        return open("secret.key", "rb").read()

    def receive(self):
        while True:
            try:
                message = self.client.recv(1024)
                decrypted_message = self.fernet.decrypt(message).decode('ascii')
                if message == 'NICK':
                    self.client.send(self.nickname.encode('ascii'))
                else:
                    print(decrypted_message)
            except:
                print("An error occured!")
                self.client.close()
                break

    def write(self):
        while True:
            message = f'{self.nickname}: {input("")}'
            encrypted_message = self.fernet.encrypt(message.encode('ascii'))
            self.client.send(encrypted_message)

    def run(self):
        receive_thread = threading.Thread(target=self.receive)
        receive_thread.start()

        write_thread = threading.Thread(target=self.write)
        write_thread.start()

if __name__ == "__main__":
    client = Client()
    client.run()

In this script, the Client class first loads the key from the secret.key file in its constructor. The key is used to create a Fernet object, which is stored in self.fernet.

In the receive method, incoming messages (which are expected to be encrypted) are decrypted using the self.fernet object. Note that the decrypted message is decoded into a string before it’s printed to the console or compared to ‘NICK’.

In the write method, messages are encrypted before they’re sent to the server. Each message is first encoded into bytes, then encrypted using the self.fernet object.

A more secure method could be to use a key exchange algorithm like Diffie-Hellman, or to have a trusted third party distribute the keys.

Running the Server or the Client

We can create a Python script that asks the user whether they would like to launch a server or a client, and then launches the chosen one.

from server import Server
from client import Client

def launch():
    choice = input("Would you like to launch a (s)erver or a (c)lient? ")

    if choice.lower() == "s":
        server = Server()
        server.receive()
    elif choice.lower() == "c":
        client = Client()
        client.run()
    else:
        print("Invalid choice. Please enter 's' for server or 'c' for client.")

if __name__ == "__main__":
    launch()

To run this script, the user would enter ‘s’ to launch a server or ‘c’ to launch a client. Note that the server and client code should be in separate Python files named ‘server.py’ and ‘client.py’ in the same directory as this script.

This script assumes the existence of a Server class with a receive method in ‘server.py’ and a Client class with a run method in ‘client.py’. These classes should be the server and client classes from the previous examples.

Launching the Server or the Client

Sure, you can use Python’s command-line interface to run the server and client scripts.

Before running these commands, make sure you’re in the correct directory (the one that contains the server and client scripts). You can navigate to the correct directory with the cd command. For example:

cd path/to/your/directory

Replace path/to/your/directory with the path to the directory that contains your scripts.

Once you’re in the correct directory, you can run the server and client scripts with the python or python3 command, followed by the script name.

To launch the server, you would use:

python server.py

or

python3 server.py

Depending on your system setup, you might need to use python3 instead of python to specify that you want to use Python 3.

The server script should start running and listening for incoming connections.

To launch a client, you would open a new terminal or command prompt window, navigate to the correct directory as before, and then use:

python client.py

or

python3 client.py

The client script should start running and attempt to connect to the server. You should be able to start sending and receiving messages.

Remember, you need to replace server.py and client.py with the actual names of your server and client scripts if they are different. The .py extension indicates that these are Python scripts.

Leave a Reply

Your email address will not be published. Required fields are marked *