Reading Time: 11 minutes

TL;DR: We implement a cross-platform socket library (for GNU/Linux and Windows desktop platforms) that uses hybrid encryption scheme relying on Crypto++, a well-known C++ cryptographic library. We discuss its design principles and provide some insight about the en/decryption process. Additionally, we present an implementation example of both standard sockets and Secure_Socket, comparing the generated traffic. You can find the source code in the official code repository at GitHub.

Introduction

Sockets are usually the way to go when it comes to writing source code that performs communications between multiple processes, IPs and/or ports. Working with sockets is quite simple, and both server- and client-side applications can be developed quite easily (there are plenty of tutorials on the Internet in this regard, such as this one). While working with vanilla sockets may be suitable for testing or small personal projects, if a serious project is to be developed or if security is an important aspect to consider, plain sockets will not be good enough and something else is needed.

From a security perspective, one of the main reasons that plain sockets are not considered a good communication solution is the lack of encryption. That is, the data is sent over the network (regardless of its nature) with any encryption. Data without encryption is known as plaintext. Transmitting data “in clear” is a huge threat to the parties involved in the communication, since it jeopardizes the properties of the information security triad (confidentiality, integrity, and availability).

To deal with this problem, programmers usually want to add a cryptographic layer to their software to keep using sockets but in a “secure” way. There are many examples on the Internet of people asking how to protect their communications (see this post or this one on StackOverflow, to name a few), as well as multitude of tutorials/examples of how to do it (see this or this link, among many others). While reading tutorials and asking on forums helps, simply copying or implementing such a sensitive aspect of software without fully understanding it will end up causing problems or putting your system and your data at risk. In fact, some of the “solutions” proposed on popular sites like StackOverflow have been proved incorrect and dangerous, as recently published. An incorrect configuration in the applied cryptographic scheme can totally compromise the security of the scheme itself.

Faced with this situation, as the responsibility generally falls on the programmers, who are then forced to use and/or implement encryption algorithms and schemes that they do not fully understand themselves, we believe we should provide the community with a solution that is both easy and transparent for the programmers to use without reinventing the wheel. In this way, we came up with Secure_Socket, a C++ wrapper library for standard sockets that transparently encrypts and decrypts data using hybrid encryption scheme. That is, the socket itself establishes the connection and encrypts and decrypts the data appropriately when the application performs a send() or recv() operation. All of this is done transparently, without the need to write additional lines of code.

In this post we will briefly describe what hybrid encryption is, how we implement a secure version of sockets using the Crypto++ cryptographic library, and a usage example. Our implementation is called Secure_Socket and its source code (released under license GNU/GPLv3), along with documentation and a step-by-step guide on how to configure it, can be found here.

Cryptography Algorithms and Encryption Schemes

To ensure data confidentiality, there are several encryption schemes and algorithms to choose from. The two main categories are symmetric key and asymmetric key cryptography algorithms. We briefly describe both categories below.

Symmetric Key Scheme

In a symmetric key scheme (sometimes called private-key scheme), the encryption and decryption keys are the same. Therefore, the parties involved in the communication must know (or share) the keys in advance. There are a large number of symmetric key algorithms, and some of the most common are AES (Advanced Encryption Standard), DES or 3DES (Data Encryption Standard), RC4, RC5, or RC6.

These algorithms are of two types: block algorithms, in which the data is divided into blocks and is encrypted using a specific secret key (that is, the data is not encrypted until the block is complete); and stream algorithms, in which data is encrypted as it is received. These schemes have some advantages, such as requiring less computational power to perform encryption/decryption operations than in asymmetric counterparts (in other words, symmetric algorithms are faster under the same circumstances) and therefore being more efficient for large amounts of data.

Asymmetric Key Scheme

In an asymmetric key scheme (sometimes called public-key scheme), there are two keys: (1) a public key; and (2) a private key. The encryption key is public (that is, everybody has the means to know it) and it is used exclusively to encrypt messages addressed to the owner of that key. On the contrary, the decryption key is private and exclusive to the receipt of the message and it is used exclusively to decrypt the message. That’s the way asymmetric key schemes work: everyone can send messages to a certain party by encrypting them with their public key, but only that specific party will be able to decrypt those messages using their private key. The mathematical relation of the keys makes it possible, which has to be proven secure. As before, there are many asymmetric key algorithms. Some of the most common are RSA (Rivest-Shamir-Adleman), ECDSA (Elliptic Curve Digital Signature Algorithm), or DSA (Digital Signature Algorithm)

Asymmetric key schemes have some advantages over symmetric key schemes: they have greater security, since it is not necessary to transmit the private key to be able to communicate securely; and distributing the keys is no longer a problem, since the public key can be sent anywhere without limits (it is public!).

Hybrid Encryption

Hybrid encryption (also known as hybrid cryptosystem) is the combination of the two previous schemes. Systems that implement hybrid encryption take advantage of both encryption schemes. The Encyclopedia of Cryptography and Security (2011) states that “In a hybrid encryption scheme, a public-key encryption technique is used to encrypt a key K (KEM part) and a symmetric-key encryption technique is used to encrypt the actual plaintext m with the key K (DEM part).” According to Wikipedia, “[…] a hybrid cryptosystem is one which combines the convenience of a public-key cryptosystem with the efficiency of a symmetric-key cryptosystem.”

Hybrid encryption schemes are considered secure as long as the private keys are kept secure. As we mentioned earlier, our Secure_Socket library implements a hybrid encryption scheme. Specifically, we implement AES as the symmetric key algorithm and RSA as the asymmetric key algorithm. In other words, we use a symmetric algorithm AES to encrypt and decrypt the data and the asymmetric algorithm RSA to share the private key.

The Secure_Socket C++ Library

Since our library is a wrapper for the original socket library, we want it to be as similar as possible. Thus, when creating a new socket, the programmer must specify the port and IP address. In addition, they must specify the socket type (client or server), initialization vector (if any), RSA private key path, RSA public key path, and key length (32 bytes by default).

The socket type is necessary because the operations to be carried out vary, depending on whether we are on the client side or on the server side. As a hybrid encryption scheme, one side of the communication will generate the symmetric key (using AES) and send it to its counterpart using the asymmetric key (using RSA). In our case, the Server will contain the RSA public and private keys and will share the public key with each new connection attempt. On the other hand, each Client will generate an AES key, encrypt it with the server’s public RSA key and send it to the server. Once the server decrypts the AES key with the RSA private key, communication can continue securely. The same operations must be performed to share the corresponding initialization vector (IV), if any.

Specifically, on the server side, the library:

  1. Creates the socket.
  2. Attaches the socket to the specified port and IP address.
  3. Waits for and accepts incoming connections.
  4. Sends the RSA public key to the new client.
  5. Receives and decrypts the RSA-encrypted AES Key.
  6. Receives and decrypts the RSA-encrypted AES IV.
  7. Creates a Key object with received AES parameters.
  8. Communication can now continue with recv() and send() operations.

On the client side, the library:

  1. Creates the socket.
  2. Establishes the connection to the specified port and IP address.
  3. Generates an AES Key.
  4. Receives the RSA public key from the server.
  5. Encrypts the generated AES key with the received RSA key and sends it to the server.
  6. (Optional) Generates an AES IV, encrypts it with the received RSA key and send it to the server.
  7. Communication can now continue with recv() and send() operations.

Requirements, Installation, and Compilation

Our library works both in Windows and in Unix. All the documentation related to the system requirements and a detailed step-by-step guide on how to install and compile Secure_Socket, both in Windows and Unix environments, can be found in the official repository of the tool. Also, Microsoft Visual Studio 2017 version 15.9.38 setup and Crypto++ installation are explained step by step in the following YouTube video.

Usage example

Finally, here we show what the difference is when writing a program using the standard socket library or using Secure_Socket to establish a connection and communicate two endpoints. We will also use Wireshark to sniff the packets and inspect their contents. Note: The examples shown here are compiled and tested on a Unix-like OS. Nevertheless, the library behaves the same on Windows.

Standard Sockets

The standard way to communicate a server and a client using sockets (the code is written in C++) goes something like this:

Server code:

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <string>
#include <cstring>
#include <stdlib.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[]){

    std::string IP = "127.0.0.1";
    int port = 1337;

    /* Creation of socket */
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    int server_fd, socket_fd;
    int protocol = 0; // IPv4
    int opt = 1;

    server_fd = socket(AF_INET, SOCK_STREAM, protocol);

    if (server_fd == -1) {
        perror("[!] Socket creation failed! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }

    if ((setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) != 0) {
        perror("[!] Setting socket options (setsockopt) failed! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }

    /* Populating struct sockaddr_in */
    address.sin_family = AF_INET;
    inet_pton(AF_INET, IP.c_str(), &address.sin_addr);
    // To convert it back to string: inet_ntop(AF_INET, &(sa.sin_addr), str, INET_ADDRSTRLEN);
    address.sin_port = htons(port);

    /* Populating struct sockaddr_in */
    address.sin_family = AF_INET;
    inet_pton(AF_INET, IP.c_str(), &address.sin_addr);
    // To convert it back to string: inet_ntop(AF_INET, &(sa.sin_addr), str, INET_ADDRSTRLEN);
    address.sin_port = htons(port);

    /* Attaching socket to PORT port and ADDRESS IP */
    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) != 0) {
        perror("[!] Error binding socket! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }

    /* Waiting for incoming connections */
    if (listen(server_fd, SOMAXCONN) != 0) {
        perror("[!] Error while listening to incoming connections! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }

    /* Accepting incoming connections */
    if ((socket_fd = accept(server_fd, (struct sockaddr*)&address, (socklen_t *)&addrlen)) < 0) {
        perror("[!] Error while accepting incoming connections! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }

    char buffer[1000];
    recv(socket_fd, buffer, sizeof(buffer), 0);
    printf("Server received: %s\n", buffer);
    char* message = "Server salutes you!";
    send(socket_fd, message, strlen(message)+1, 0);
}

Client code:

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <string>
#include <cstring>
#include <stdlib.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[]){

    std::string IP = "127.0.0.1";
    int port = 1337;

    /* Creation of socket */
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    int server_fd, socket_fd;
    int protocol = 0; // IPv4
    int opt = 1;

    if ((socket_fd = socket(AF_INET, SOCK_STREAM, protocol)) == -1) {
        perror("[!] Socket creation failed! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }
    /* Populating struct sockaddr_in */
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
    inet_pton(AF_INET, IP.c_str(), &address.sin_addr);

    /* Establishing connection to the specified socket */
    if (connect(socket_fd, (struct sockaddr *)&address, sizeof(address)) != 0) {
        perror("[!] Error while connecting to the specified socket! ABORTING [!]\n");
        exit(EXIT_FAILURE);
    }

    char* message = "The client says hello!";
    send(socket_fd, message, strlen(message)+1, 0);
    char buffer[1000];
    recv(socket_fd, buffer, sizeof(buffer), 0);
    printf("Client received: %s\n", buffer);
}

Compile both codes. Then run the server first and then the client. You will see that the communication is successful and working as expected.

Execution of both server and client using standard Sockets

However, any adversary that sniffs the network (for example, through a MitM attack) can intercept and understand the communication since the standard sockets send the information without encryption. In the following images, the server port is 1337 and the client port is 50624.

Client data is transmitted without encryption
Server data is transmitted without encryption

Secure_Socket

The same attack cannot be performed when using the Secure_Socket library, as the data is automatically encrypted and can only be decrypted by the legitimate parties involved in the communication (i.e., the client and the server). Let’s now illustrate the same code skeletons (again written in C ++), but this time using our library. Note that the required RSA key files have been generated previously.

Server code:
This code can be found here. The only peculiarity is that Secure_Socket::send() expects an array of bytes as the first parameter, rather than an array of chars. Bytes can be represented as unsigned char in C/C++.

Comments are left for clarification purposes.

#include "../src/Secure_Socket.hpp"

int main(){
    int port = 12345;
    std::string IP = "127.0.0.1";

    /* Open a server-side socket to wait for incoming connections.
    On the server side the path of both public and private RSA key files must be specified
    The NULL parameter corresponds to the IV (it is ignored on the server side) */
    Secure_Socket* socket = new Secure_Socket(port, IP, SERVER_SECURE_SOCKET, NULL, "./private_unencrypted.pem", "./public.pem"); 

    /* Whenever the server receives an incoming communication from the clients, the hybrid 
    encryption is internally set up. Communication can now be carried on*/

    /* Secure socket sends and receives bytes (or unsigned char)). You can use
    vector or any other data structure capable of handling unsigned char. */
    std::vector<unsigned char> message;

    /* Strings must be converted to unsigned char* */
    std::string plaintext = "Hey, I'm the server and I'm sending you some info!";
    int message_len = plaintext.length()+1;
    message.resize(message_len);
    strcpy((char *)message.data(), plaintext.c_str());
    socket->secure_send(message.data(), message_len, 0);

    unsigned char buffer[100];
    int bytes_received = socket->secure_recv(buffer, 100, 0);
    printf("[+] Received from the client: %s\n[+] A total amount of %d bytes!\n", buffer, bytes_received);

    /* Or you can declare your string as unsigned char */
    unsigned char plaintext_2[] = "That's all for today! See you next time.";
    message_len = strlen((char*) plaintext_2)+1;
    socket->secure_send(plaintext_2, message_len, 0);


    free(socket);
}

Client code:
This code can be found here.

#include "../src/Secure_Socket.hpp"

int main(){
    int port = 12345;
    std::string IP = "127.0.0.1";

    /* Create the server-side Secure Socket. If no IV is specified, a random one will be generated */
    Secure_Socket* socket = new Secure_Socket(port, IP, CLIENT_SECURE_SOCKET); //init_vector, key_len and socket_type have default values

    /* Example of predefined IV */
    //unsigned char init_vector[] = "EXAMPLE_INITIALIZATION_VECTOR_LARGE_ENOUGH";
    //Secure_Socket* socket = new Secure_Socket(port, IP, CLIENT_SECURE_SOCKET, init_vector); //key_len and socket_type have default values

    unsigned char buffer[100];
    int bytes_received = socket->secure_recv(buffer, 100, 0);
    printf("[+] Received from the server: %s\n[+] A total amount of %d bytes!\n", buffer, bytes_received);

    unsigned char plaintext[] = "I'm the client. Sending you back a response!";
    int plaintext_len = strlen((char*)plaintext) + 1;
    socket->secure_send(plaintext, plaintext_len, 0);

    bytes_received = socket->secure_recv(buffer, 100, 0);
    printf("[+] Received from the server: %s\n[+] A total amount of %d bytes!\n", buffer, bytes_received);

    free(socket);
}

Again, compile both codes. Then run the server first and then the client. You will see that the communication is successful and working as expected.

Execution of both server and client using Secure_Socket

Note that although the communication can still be sniffed out by any adversary, this time it is not readable for them. Since Secure_Socket performs hybrid encryption internally, no third (and external) party will be able to understand the messages sent in the communication. In the following images, the server port is 12345 and the client port is 40830.

We can first see how the server sends its public RSA key to the client. This content is unencrypted and sniffable, but we don’t worry because… the public key is PUBLIC, by definition! (:

Server sending its public key

The client then sends its private AES key, encrypted with the server’s public RSA key, to the server. Note that only the server will be able to decrypt the symmetric key, as long as its private RSA key is kept secret.

Client data is now encrypted

From then on, all messages sent by the server or the client will be encrypted using the symmetric key (AES). Since no one else will know what that key is, only they will be able to decrypt the data, thus guaranteeing the security of the data sent in communication.

Server data is now encrypted

And that’s all, folks! In this post, we have reviewed the main categories of cryptography algorithms (symmetric key and asymmetric key schemes). Also, we have explained the hybrid encryption scheme, which combines both of the above schemes. We have also introduced Secure_Socket, our C++ wrapper library for standard C++ sockets that transparently embeds this cryptographic scheme, allowing programmers to communicate securely. Currently, our library uses AES as the symmetric algorithm and RSA as the asymmetric algorithm. You can find it in the official code repository. Secure_Socket is released under the GNU/GPLv3 license, so feel free to give it a try! And of course, please send us any issues and comments that help make it better!