Cybersecurity has become a fairly popular issue last days. It is influenced by public incidents related to data leaks, hacker attacks, scandals related to Pegasus spyware, surveillance and information interception. A reckless approach to the issue of data security in the network and, above all, the lack of knowledge about good practices in the context of security, additionally increases this problem. It is therefore a topic worth taking a closer look at.
Curious and at the same time inspired by these topics, I decided to try my hand at designing and implementing an internet instant message communicator, the main assumption of which is to ensure the highest level of security and user privacy. The application that was created is only a concept and have mainly educational values. The time spent on implementing it, above all, is a great fun and learning for anyone who would like to deal with such a topic. At the same time, it is a great opportunity to introduce You to issues related to cryptography.
The following article is intended for software developers who would like to use their theoretical knowledge of cryptography in a practical project. Feel free to read and play with the source code.
Theory
Algorithms are an inherent part related to cybersecurity and cryptography. The knowledge of cryptographic techniques and the ability to apply them in practice will be useful to any software developer. So, improving your skills in this area is a good investment.
At the very beginning, however, it should be mentioned that no algorithm or technique provides 100% security. When choosing a protection method, we must take into account the ratio of the value of the information we protect to the costs that a potential attacker will have to incur to extract this information. In practice, it comes down to balancing the outlays that must be incurred to break the security in relation to the value of the protected information.
For example, information about what we ate for breakfast does not have to be encrypted with some terribly complicated algorithm, i.e. incurring the costs associated with its protection, because it is generally worthless, and no attacker will spend large resources on it to gain access to such information. Facebook password will be enough 🙂
On the other hand, the hypothetical file with passwords to our bank accounts (despite the fact that such a file should not even exist) is already quite valuable and should be subject to greater protection, because the attacker is also able to spend more resources on reaching the information contained therein.
In the context of our instant messenger, we will limit ourselves to the basics of symmetric and asymmetric cryptography. Correctly applied techniques have an impact on the final security of data exchange in the application. Proven, certified and tested algorithms have an advantage over self-made solutions, they guarantee a certain level of security and protection against certain types of attacks.
Symmetric cryptography
The most popular group of cryptographic methods are those based on symmetric cryptography. They are based on the assumption that the encryption and decryption operations are symmetrical, that is, they use the same common key. However, the encryption and decryption algorithm itself need not be the same in both operations.
The further classification of symmetric algorithms leads to the separation of block and stream algorithms. Block algorithms are only able to operate on a fixed-length data block, while stream algorithms accept any input data stream. There are also adaptive techniques that make it possible to use the block algorithm also to work with the data stream.
The main advantages of symmetric related to asymmetric cryptography algorithms are:
- relatively high speed of operation,
- simpler and less resource-consuming implementation.
Basic high-level AES encryption and decryption operations are shown in Figures 1 and 2. This particular implementation corresponds to the AES-CBC mode. In other operational modes, the use of the initialization vector may be slightly different. AES uses different algorithms for encryption and decryption operation, but for these both uses the same key, that’s why it is symmetric algorithm.
In contrast, a simple algorithm based on the XOR function only, uses the same function for both encryption and decryption operations. It is shown in Figure 3.
Asymmetric cryptography
Asymmetric cryptography algorithms are based on the assumption that we use two different keys for encryption and decryption. One of the keys is the public key and the other is the private key. Sharing the public key doesn’t affect the security of data used for private key operations in any way.
At this stage, I have not specifically marked which key we use for encryption and which for decryption, because asymmetric cryptography offers two other operations that we can perform with private and public keys. These are signing and signature verification operations.
Thus, we can distinguish two groups of operations:
- encryption and decryption (shown in Figure 4),
- signing and signature verification (shown in Figure 5).
For data encryption and decryption:
- public key is used to encrypt data (anyone who has access to the public key can encrypt the data),
- private key is used to decrypt the data (only the owner of the private key can decrypt the data).
For signing and signature verification:
- private key is used to sign data (only the owner of the private key can sign the data with his unique private key)
- public key is used to verify the signature (anyone who has access to the public key can verify that the data has been signed using the correct private key)
Design
Fortunately, educational projects have their own rules, so we can let our imagination run wild and assume that the information exchanged between the communicator’s users will be super top secret, and that decryption of messages will be practically impossible. Of course, you have to look at it with a grain of salt and have in mind that the concept is more important than the actual functionality.
Even the most sophisticated cryptographic algorithms, which can only be broken by brute-force and will take a million years on supercomputers, are useless if we do not properly secure access to the key. Thus, any cryptographic algorithm provides protection only to the extent that the key is secure.
Server
Two cloud services are required for the communicator to work. They are the “central” part of the system that allows you to establish connections between any communicator users. This assumption cannot be easily avoided and is simply due to the inability to connect directly between hosts in internal networks behind NAT, without public IP addresses. Of course, there are technical possibilities to deal with this problem as well, but it is not crucial from the point of view of the communicator design.
The high-level simplified system architecture is presented in Figure 6.
Message broker
The first service is a message broker that acts as an intermediary in the exchange of messages between the users of the messenger. As long as the messages are end-to-end encrypted, there is no problem with messages going through an intermediary as they are fully encrypted. Assuming that encrypted messages can always be intercepted, this is not a problem. Anyone can intercept an encrypted message, but only the intended recipient will be able to decrypt it.
The only public information that will be openly available on the broker side is the name of the message recipient. This is an acceptable simplification, resulting from the operation of the MQTT broker (subscription to a specific user-related topic), the use of which has greatly simplified the architecture and operation of the messenger.
Theoretically, this information can also be anonymized (which would increase the complexity) or even broadcast messages to all users (because only the right addressee will read it anyway) by completely removing this information, but it would have a critical impact on system performance.
As the messages are encrypted, we can even use the public MQTT broker, it does not significantly affect the security of the system. We do not have to use an encrypted TLS connection or even broker certificates, because the messenger application layer takes care of the security and authentication of messages. A simplified method of data exchange is shown in Figure 7.
Public keys provider
The second service necessary for the functioning of the communicator is a service that provides users’ public keys. The use of asymmetric cryptography with public keys determines the necessity to share the public keys of individual communicator users. Therefore, we must ensure that every messenger user has access to the public key sharing service. It has to be a centralized system with a database and API.
Of course, it is only needed when we do not have access to the recipients’ public keys offline and when we do not have access to the public key of the author of the message (for its verification). If we assume that each user can exchange information with everyone else at any time, the online service of sharing such public keys becomes essential.
To maintain security and ensure the credibility of the origin of the public keys, each message containing the server’s response with the keys is also signed with the private key located on the server. The server’s public key is available in the messenger client (hardcoded in the source code), so after retrieving the public keys, they can be verified. This provides protection against spoofing, of course, only as long as the password to the server on which the public key sharing service is running is not “1234” 🙂
It is worth noting here that there are ready-made key sharing systems available on the network (both commercial and free), and the application itself that was created during the work on the communicator is a topic for a completely different article. However, it is quite universal, and it can certainly find more use-cases than just a part of our instant messenger system.
A short description of the API of the key sharing system used in the communicator is presented in Table 1.
Table 1: Public keys provider API
Request URL | Description | Response code | Content body |
GET /users/<username> | Get user public keys | 200 | { “response”: { “public_key_message”: “<PUBLIC_KEY>”, “public_key_sign”: “<PUBLIC_KEY>”, } “signature”: “<RESPONSE_SIGNATURE>” } |
404 | User not found | ||
GET /users/<username>/<token> | Security token to activate and deactivate user | 200 | User activated/deactivated |
404 | User/token not found | ||
POST /users | Register new user | 201 | { “username”: “<USERNAME>”, “email”: “<EMAIL>”, “public_key_message”: “<PUBLIC_KEY>”, “public_key_sign”: “<PUBLIC_KEY>” } |
400 | Bad request |
Registration in the key sharing system requires only the e-mail address to which one-time activation and deactivation links will be sent. This is a necessary process to protect the system from bots that will flood the database with non-existent users. Inactivated user accounts are automatically removed from the database. The messenger itself doesn’t require username registration to make it work.
Description of the public keys provider software with source codes can be found here.
Encryption and sending
In order an encrypted message to be verified and decrypted only by the addressee, its preparation requires several steps on the sender’s side. This is shown in Figure 8.
- Generate a message-unique symmetric key.
- Encrypt the message using a symmetric key.
- Encryption of a symmetric key with an asymmetric private key.
- Signing the message using the private key.
- Combination of the encrypted key, encrypted message and signature, these 3 elements constitute the entire encrypted message that can be safely sent to the addressee even through a public channel.
The private and public key pair used to encrypt and decrypt symmetric keys is different from the key pair used to sign and verify the signature. Each messenger client therefore has a total of 4 keys. Two of them are public keys, placed on the key-sharing service, and two are private keys, available only to the user.
The message is sent as shown in Figure 9.
In order to minimize the network traffic, in particular the polling of the website that provides public keys, a local cache for the public keys was used. Once used, the public key is stored in local memory and when it is used again, it is retrieved directly from the memory instead of from the key server. The key is kept in memory only until the end of the messenger application.
Receiving and decryption
The message is received from the MQTT message broker by subscribing to a topic associated with a specific messaging client. After receiving the message, the client performs several steps as shown in Figure 10.
The process of decrypting the message itself at the recipient side also requires several steps. This is shown in Figure 11.
- Decryption of a symmetric key with a private key.
- Decryption of the message with a decrypted symmetric key.
- Verifying the signature of the decrypted message using the sender public key.
Session
In the context of communication and establishing an encrypted connection, it is often referred to as a session of a secure encrypted connection. Setting up and maintaining sessions could be a relative complex procedure, especially if we want to conduct several independent conversations with different users. From a functional point of view, sessions (at the messaging application layer level) can be a useful solution. Thanks to it, it is possible to optimize the exchange of keys and additionally send some metadata specific to the active session.
In our case, however, I decided not to implement such a solution. This significantly simplified the operation of the system and, paradoxically, increased its security, as each sent message is encrypted with a separate generated unique symmetric key. In this case, decoding the message string (without access to the private key used to encrypt the symmetric key itself) will require each message to be decoded independently. Breaking the key of one message does not affect the security of the others.
Message format
Messages are sent between messenger clients using the JSON format. It is a text format, so the binary data that we receive after encryption must be additionally encoded to be represented in text form. The Base-64 encoding was used for this.
The encrypted message exchanged between the communicator’s clients consists of 3 fields:
- cipherkey – encrypted symmetric key
- ciphertext – encrypted message
- signature – message signature
{
"cipherkey": "<encrypted_key>",
"ciphertext": "<encrypted_message>",
"signature": "<message_signature>"
}
The unencrypted message containing sensitive data from the security point of view consists of 4 fields:
- from – sender username
- to – recipient username
- timestamp – message sending timestamp
- message – message
"from": "<sender_username>",
"to": "<sender_username>",
"timestamp": <unix_timestamp>,
"message": "<text_message>"
}
Flow
The Figure 12 shows a detailed diagram showing an example of communication between two users of the messenger, and with a third user trying to listen in on the exchange of messages.
Development
I used the Go language to implement both the messenger client and the key-sharing service applications. Go language (sometimes called Go-Lang) is a general-purpose language, but with quite wide support for building server applications. Additionally, in the set of Go tools, we get multiplatform support and build to native code. In the context of security, the advantage of compiled programs over programs written in scripting languages (e.g., Python) is certainly their more difficult analysis and reverse-engineering.
The choice, however, was dictated mainly by personal preferences and the desire to consolidate the skills related to this fairly fresh and constantly developing technology from Google. The implementation of the entire software, including testing and tool investment, took about 2 weeks. This is quite a good result for an extensive PoC.
Environment
To create and deploy a fully functioning communicator system, we need a server on which we will run a public key sharing service. The server must be accessible to the public for every instant messenger user to have access to the key sharing service. In addition, the service that provides the keys uses an e-mail account to send activation links. This software is deployed to the server and run in the docker container.
The default messaging broker of our messenger is the test broker available at test.mosquitto.org. Nothing prevents you from using other message broker, it is not a key element of the system from the security point of view, but it ensures the exchange of information between system users. This particular choice was dictated only by good knowledge of tools to support the mosquitto broker.
Client application
The client software was created as a simple tool with a CLI interface for sending and receiving messages. Over time, an interactive mode was added that allows you to have conversations in traditional chat mode.
Application Quick Start Guide:
usage: nes <Command> [-h|--help] [-b|--broker "<value>"] [-p|--provider
"<value>"] [-P|--provider_key "<value>"] [-k|--private_message
"<value>"] [-K|--public_message "<value>"] [-j|--private_sign
"<value>"] [-J|--public_sign "<value>"] [-u|--user "<value>"]
[-c|--config "<value>"]
NES messenger
Commands:
register Register username at PubKey Service
listen Listen to messages
send Send message to recipient
config Configuration management
generate Generate private and public keys pair
chat Interactive chat with recipient
version Application version
Arguments:
-h --help Print help information
-b --broker MQTT broker server address. Default:
tcp://test.mosquitto.org:1883
-p --provider Public keys provider address. Default:
https://microshell.pl/pubkey
-P --provider_key Provider public key. Default:
PUBLIC_KEY_OF(https://microshell.pl/pubkey)
-k --private_message Private key file for message. Default:
~/.nes/<user>-rsa-message
-K --public_message Public key file for message. Default:
~/.nes/<user>-rsa-message.pub
-j --private_sign Private key file for signing. Default:
~/.nes/<user>-rsa-sign
-J --public_sign Public key file for signature verification. Default:
~/.nes/<user>-rsa-sign.pub
-u --user Local username. Default: <os_user>
-c --config Optional config file. Supported fields:
MQTT_BROKER_ADDRESS, PUBKEY_ADDRESS,
PUBKEY_PUBLIC_KEY_FILE, PRIVATE_KEY_MESSAGE_FILE,
PUBLIC_KEY_MESSAGE_FILE, PRIVATE_KEY_SIGN_FILE,
PUBLIC_KEY_SIGN_FILE, NES_USERNAME. Default:
~/.nes/config
You can find the application with a description, user manual and source codes here.
Demo
A video showing the simple data exchange is attached below. In order to enable visualization, the two communication sides are on the same host. Of course, nothing prevents you from having a conversation from two different computers, this is the purpose of the internet instant messenger application 🙂
Summary
The created communicator carries out its task. It enables the secure messages exchange between users. The multitude of technologies used and issues necessary to run the entire system make the project interesting and educational. The main challenge during the implementation was to combine the various parts of the system into a working whole. This required running smaller parts of the system, testing them, and integrating them into larger and larger modules. This reflects a common approach to developing virtually any software.
The application is certainly not perfect, it only implements the most important functionality, which is encrypted, secure messages exchange between users. A functional messenger should have much more functionalities, i.e. user statuses, contact lists, conversation histories, files exchange. The information exchange protocol is not complicated, so it can also be implemented on mobile devices as well as in embedded systems or broadly understood IoT. The development potential is therefore considerable. As the project was originally created as open source, there may be someone who will improve this application. I encourage you to try and experiment in this field!
Leave a comment