📜 ⬆️ ⬇️

libsodium: Public-key authenticated encryption or how I decrypted the message without the private key

This article is an example of how misunderstanding of the work of cryptographic primitives and overconfidence can lead to critical errors in the implementation of cryptographic protection. I hope my mistake will be a useful example for someone.

It all started with the fact that in one small project I needed to use encryption. Clients should send interception-protected messages to the Server. Since, in principle, client authentication was not required, and the data went only in one direction (from Clients to the Server), and besides, I did not want to bother with storing common encryption keys, I had the idea to use asymmetric cryptography. The idea looks simple: Clients are informed of the public key of the Server, with which they encrypt the message sent to the Server. The server (and only it) in turn can decrypt the received message using the private key known to it.

The manual implementation of cryptographic primitives is a thankless task and fraught with errors, so it was decided to use any open source library to implement the above idea. Since ZeroMQ libraries were already used in the project, its CZMQ wrapper, which in turn ensured the security of data transfer based on the libsodium library, the choice fell on it. Indeed, why grow dependency, if it already has everything.

About libsodium
As stated on the official website, libsodium is an open, modern, simple library for encryption, digital signatures, hashing, etc.
There is also an impressive list of projects and companies that use libsodium, among which, for example, Tox

So, a quick reading of the documentation showed that the library contains the implementation of asymmetric encryption on elliptic curves. Public-key authenticated encryption . In addition, it is possible to confirm the authenticity of the message by MAC . MAC encryption and generation is performed using the crypto_box_easy function, the reverse procedure (checking and decryption) is crypto_box_open_easy using crypto_box_open_easy .
')
From the documentation:
Original
Using public-key authenticated encryption, Alice's public key.
Using Bob's public key, it can be decrypting it.
Alice only needs the public key, the nonce and the ciphertext. Bob should never share his secret key, even with Alice.
Alice's public key. Alice should never be shared with Bob.

Using public-key encryption with authentication support, Bob can encrypt Alice's confidential message using her public key.

Using Bob’s public key, Alice can verify, before decryption, that the encrypted message was indeed created by Bob and is not forged.

Alice needs only Bob's public key, nonce and encrypted message. Bob must keep his private key secret even from Alice.

To send messages to Alice, Bob needs only Alice’s public key. Alice, in turn, must keep her private key secret even from Bob.

It seems that everything is simple and clear. The client encrypts the message with the Server's public key and signs the message with its private key. The server, having received the message, checks it using the client's public key and decrypts it with its private key. Apart from the Server, no one can decrypt the message, since it was encrypted with its public key (at least this is the main principle of asymmetric cryptography). However, the devil is in the details.

To test the concept, I copied the example from the official site, but accidentally made a mistake and got a strange result.

Test code
 #include <string.h> #include "sodium.h" #define MESSAGE "test" #define MESSAGE_LEN 4 #define CIPHERTEXT_LEN (crypto_box_MACBYTES + MESSAGE_LEN) static bool TestSodium() { unsigned char alice_publickey[crypto_box_PUBLICKEYBYTES]; unsigned char alice_secretkey[crypto_box_SECRETKEYBYTES]; crypto_box_keypair(alice_publickey, alice_secretkey); unsigned char bob_publickey[crypto_box_PUBLICKEYBYTES]; unsigned char bob_secretkey[crypto_box_SECRETKEYBYTES]; crypto_box_keypair(bob_publickey, bob_secretkey); unsigned char nonce[crypto_box_NONCEBYTES]; unsigned char ciphertext[CIPHERTEXT_LEN]; randombytes_buf(nonce, sizeof nonce); // message alice -> bob if (crypto_box_easy(ciphertext, (const unsigned char*)MESSAGE, MESSAGE_LEN, nonce, bob_publickey, alice_secretkey) != 0) { return false; } unsigned char decrypted[MESSAGE_LEN + 1]; decrypted[MESSAGE_LEN] = 0; //  //if (crypto_box_open_easy(decrypted, ciphertext, CIPHERTEXT_LEN, nonce, alice_publickey, bob_secretkey) != 0) //   "" if (crypto_box_open_easy(decrypted, ciphertext, CIPHERTEXT_LEN, nonce, bob_publickey, alice_secretkey) != 0) { return false; } if(strcmp((const char*)decrypted, MESSAGE) != 0) return false; return true; } 


In the test for Alice and Bob, first a randomly generated pair of keys ( crypto_box_keypair ), then again, nonce is randomly filled ( randombytes_buf ). After that, Alice encrypts her message for Bob using his public key and forms the MAC using her private key.

 // message alice -> bob if (crypto_box_easy(ciphertext, (const unsigned char*)MESSAGE, MESSAGE_LEN, nonce, bob_publickey, alice_secretkey) != 0) { return false; } 

However, in the decryption procedure, I made a mistake and passed the wrong parameters. Instead of decrypting the message for Bob with his private key, I tried to decrypt the message with Bob’s public key and Alice’s private key ( Copy-paste so that it ).

 //   "" if (crypto_box_open_easy(decrypted, ciphertext, CIPHERTEXT_LEN, nonce, bob_publickey, alice_secretkey) != 0) { return false; } 

What was my surprise when the message deciphered! It was very strange and put me in a state of cognitive dissonance. Before our eyes was found 0-day vulnerability and recognition of the world community . I could not figure out how to decrypt the message for Bob without using his private key. And besides, after all, the MAC check was successfully performed without using Alice's public key!

The first thing I thought to do was perform the decoding as in the original example, while everything went smoothly too - the message was decrypted and verified. Thus, the message could be decrypted (and verified!) With any key pair — Bob’s public key and Alice’s private key or vice versa — Bob’s private key and Alice’s public key.

My second thought was that I use the old version of the library. Updated to the latest version, but the test behavior has not changed.

Frankly, I had little time and desire to delve into the source code of libsodium. The answer was found on Stackoverflow. It turns out that libsodium means “Public-key authenticated encryption” a little differently from what it seemed to me.

After a detailed review, the encryption algorithm was as follows:

  1. Using the ECDH algorithm, a common key is generated for a symmetric cipher.
  2. The message is encrypted using the XSalsa20 symmetric cipher using the shared key obtained in the first step.
  3. An imitation MAC ( Poly1305 ) is generated using the same shared key.

This implies the following conclusions and properties of this algorithm:


I understand that these properties of the algorithm are normal and the fact is that I tried to use the wrong algorithm. However, I believe that the documentation on the official website is misleading users about the features of work and situations that are suitable for its use.

Since my goal was to use an algorithm that would not allow decrypting a message for the Server without knowing its private key, this behavior of this cryptographic primitive did not suit me and I refused to use it.

I can’t get rid of the idea: what if someone used this method to implement really critical systems, hoping for the reliability of a proven library, because I realized my mistake only because I made another and accidentally stumbled upon the “strange” behavior of the function .

I hope that was useful to someone. Good luck to all.

Source: https://habr.com/ru/post/311726/


All Articles