ECDH and AES 256

When writing one client-server application in C ++, it was required to organize a secure connection between two remote nodes. I immediately noticed the algorithms used in TLS 1.3. Omitting the implementation of TLS certificates and taking only the part responsible for data encryption, I got to work. I had to look for information in many completely different sources (from the official documentation of OpenSSL, to answers on stackoverflow), and in some places even figure out how to put together pieces of code from these very different sources. So after the successful implementation of our plans, I decided to write this article in order to help those who will solve a similar problem. In this article, we will look at a simple implementation of the bundle Diffie-Hellman Key Agreement Algorithm on Elliptic Curves and symmetric encryption algorithm AES 256 using the OpenSSL library to organize a secure connection.

Generating ECDH Keys

The header file looks like this:

#ifndef SECURITY_HPP
#define SECURITY_HPP

#include <openssl/ecdh.h>
#include "byte_array.hpp"

namespace security {

/**
 * @brief AES256 key and initialization vector
 */
struct AES_t {
  uint8_t key[32] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0
  };
  uint8_t init_vector[16] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0
  };
  bool isEmpty() const {
      static const AES_t empty_aes;
      return !std::memcmp(this, &empty_aes, sizeof (AES_t));
  }
  void clear() {*this = AES_t();}
  AES_t() = default;
  AES_t(ByteArray data) {
    *this = *reinterpret_cast<AES_t*>(data.begin());
  }
};

// ECDH
/**
 * @brief Generate ECDH key pair
 * @return ECDH key pair
 */
EVP_PKEY* genKey();

/**
 * @brief Free key memory
 * @param key
 */
void freeKey(EVP_PKEY* key);

/**
 * @brief Extract public key from key pair
 * @param key_pair
 * @return ByteArray with public key
 */
ByteArray extractPublicKey(EVP_PKEY* key_pair);

/**
 * @brief Extract private key from key pair
 * @param key_pair
 * @return ByteArray with private key
 */
ByteArray extractPrivateKey(EVP_PKEY* key_pair);

/**
 * @brief Conver ByteArrays to key pair
 * @param priv_key_raw
 * @param pub_key_raw
 * @return ECDH key pair
 */
EVP_PKEY* getKeyPair(ByteArray priv_key_raw, ByteArray pub_key_raw);

/**
 * @brief Get AES256 key and initialization vector from ECDH keys
 * @param peer_key - public key from other side
 * @param key_pair - private key from this side
 * @return AES256 key and initialization vector
 */
AES_t getSecret(ByteArray peer_key, EVP_PKEY* key_pair);

// AES256
/**
 * @brief Encrypt message
 * @param plain_text
 * @param aes_struct
 * @return Cyphertext
 */
ByteArray encrypt(ByteArray plain_text, AES_t aes_struct);

/**
 * @brief Decrypt message
 * @param ciphertext
 * @param aes_struct
 * @return plain_text
 */
ByteArray decrypt(ByteArray ciphertext, AES_t aes_struct);

/**
* @brief Encode data with base64
* @param decoded
* @return
*/
ByteArray encodeBase64(ByteArray decoded);

/**
* @brief Decode base64 data
* @param encoded
* @return
*/
ByteArray decodeBase64(ByteArray encoded);

}

#endif // SECURITY_HPP

To work with all the OpenSSL functions described below, we will need to include the following header files in the implementation file:

// файл security.cpp
#include "security.hpp"

#include <openssl/conf.h>
#include <openssl/err.h>
// EVP
#include <openssl/evp.h>
// AES
#include <openssl/aes.h>
// ECDH
#include <openssl/ec.h>
#include <openssl/pem.h>

#include <stdexcept>

// Обработка ошибок
void handleErrors() {
  ERR_print_errors_fp(stderr);
  throw std::runtime_error("Security error");
}

// Нижеприведённый код здесь

First, each party must generate ECDH key pairs. To work with ECDH keys, we will use the high-level OpenSSL interface – EVP

EVP_PKEY* security::genKey() {
  EVP_PKEY* key_pair = nullptr;						// Ключевая пара
  EVP_PKEY_CTX* param_gen_ctx = nullptr; 	// Контекст генерации параметров
  EVP_PKEY_CTX* key_gen_ctx = nullptr;		// Контекст генерации ключа
  EVP_PKEY* params= nullptr;							// Параметры ключа

  // Выделяем память для контекста генерации параметров EC-ключа
  if(!(param_gen_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL))) handleErrors();
  // Инициализируем контекст генерации параметров EC-ключа
  if(!EVP_PKEY_paramgen_init(param_gen_ctx)) handleErrors();

  // Задаём элиптичекую кривую prime256v1
  if(!EVP_PKEY_CTX_set_ec_paramgen_curve_nid(param_gen_ctx, NID_X9_62_prime256v1))
    handleErrors();

  // Генерируем параметры
  if(!EVP_PKEY_paramgen(param_gen_ctx, &params)) handleErrors();

  // Выделяем память для контекста генерации EC-ключа
  if(!(key_gen_ctx = EVP_PKEY_CTX_new(params, nullptr))) handleErrors();
  // Инициализируем контекст генерации EC-ключа
  if(!EVP_PKEY_keygen_init(key_gen_ctx)) handleErrors();
  // Генерируем ключ
  if(!EVP_PKEY_keygen(key_gen_ctx, &key_pair)) handleErrors();

  // Высвобождаем память контекста генерации параметров
  EVP_PKEY_CTX_free(param_gen_ctx);
  // Высвобождаем память контекста генерации ключа
  EVP_PKEY_CTX_free(key_gen_ctx);
  // Возвращаем указатель ключевой пары
  return key_pair;
}

Extracting ECDH keys from EVP_PKEY * key pair to raw buffer

For transmission over the network or for storage in a file, it is useful to know how to extract the public and private key from the pointer to EVP_PKEY.

Since we are working with C ++ for storing raw data, you can use std::vector<uint8_t> or a class like the following:

// Файл byte_array.hpp
#ifndef BYTE_ARRAY_HPP
#define BYTE_ARRAY_HPP

#include <cstdint>
#include <cstring>
#include <utility>
#include <new>
#include <malloc.h>

class ByteArray {
  uint8_t* byte_array = nullptr;
  uint64_t _length = 0;
  public:
  typedef uint8_t* iterator;
  // Конструктор по умолчанию
  ByteArray() = default;
  
  // Коснтруктор с выделением памяти
  ByteArray(uint64_t length)
    : byte_array(new uint8_t[length]),
  		_length(length) {}
  
  // Конструктор копирования из сырого буфера
  ByteArray(void* buffer, uint64_t length)
    : byte_array(new uint8_t[length]),
      _length(length) {
        memcpy(byte_array, buffer, _length);
      }
  
  // Конструктор копирования
  ByteArray(ByteArray& other)
    : byte_array(new uint8_t[other._length]),
      _length(other._length) {
        memcpy(byte_array, other.byte_array, _length);
      }
  
  // Конструктор перемещения
  ByteArray(ByteArray&& other)
    : byte_array(other.byte_array),
      _length(other._length) {
        other.byte_array = nullptr;
      }
  
  // Деструктор
  ~ByteArray() {if(byte_array) delete[] byte_array;}
  
  // Изменить размер
  void resize(uint64_t new_length) {
  	_length = new_length;
  	byte_array = (uint8_t*)realloc(byte_array, _length);
  }
  
  // Добавить размер
  iterator addSize(uint64_t add) {
    byte_array = (uint8_t*)realloc(byte_array, _length + add);
    iterator it = byte_array + _length;
    _length += add;
    memset(it, 0, add);
    return it;
  }
  
  // Getter для размера
  inline uint64_t length() {return _length;}
  // Оператор взятие елемента
  inline uint8_t& operator[](uint64_t index) {return byte_array[index];}
  // Оператор присвоения
  inline ByteArray& operator=(ByteArray other) {
    this->~ByteArray();
    return *new(this) ByteArray(std::move(other));
  }
  
  // Итераторы для range-based for
  // for(auto byte : byte_array_object) {...}
  inline iterator begin() {return byte_array;}
  inline iterator end() {return byte_array + _length;}
};

#endif // BYTE_ARRAY_HPP

Public key extraction function:

ByteArray security::extractPublicKey(EVP_PKEY* key_pair) {
  EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(key_pair);
  EC_POINT* ec_point = const_cast<EC_POINT*>(EC_KEY_get0_public_key(ec_key));

  EVP_PKEY* public_key = EVP_PKEY_new();
  EC_KEY* public_ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

  EC_KEY_set_public_key(public_ec_key, ec_point);
  EVP_PKEY_set1_EC_KEY(public_key, public_ec_key);


  EC_KEY *temp_ec_key = EVP_PKEY_get0_EC_KEY(public_key);

  if(temp_ec_key == NULL) handleErrors();

  const EC_GROUP* group = EC_KEY_get0_group(temp_ec_key);
  point_conversion_form_t form = EC_GROUP_get_point_conversion_form(group);

  unsigned char* pub_key_buffer;
  size_t length = EC_KEY_key2buf(temp_ec_key, form, &pub_key_buffer, NULL);
  if(!length) handleErrors();
  ByteArray data(pub_key_buffer, length);

  OPENSSL_free(pub_key_buffer);
  EVP_PKEY_free(public_key);
  EC_KEY_free(ec_key);
  EC_KEY_free(public_ec_key);
  EC_POINT_free(ec_point);

  return data;
}

Private key extraction function:

ByteArray security::extractPrivateKey(EVP_PKEY* key_pair) {
  EC_KEY* ec_key = EVP_PKEY_get1_EC_KEY(key_pair);
  const BIGNUM* ec_priv = EC_KEY_get0_private_key(ec_key);
  int length = BN_bn2mpi(ec_priv, nullptr);
  ByteArray data(length);
  BN_bn2mpi(ec_priv, data.begin());
  return data;
}

To get an EVP key pair from two raw buffers, you can use the following function:

EVP_PKEY* security::getKeyPair(ByteArray priv_key_raw, ByteArray pub_key_raw) {
  EVP_PKEY* key_pair = EVP_PKEY_new();
  EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

  const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key);
  EC_POINT* ec_point = EC_POINT_new(ec_group);
  EC_POINT_oct2point(ec_group, ec_point, pub_key_raw.begin(), pub_key_raw.length(), nullptr);
  EC_KEY_set_public_key(ec_key, ec_point);
  EC_POINT_free(ec_point);

  BIGNUM* priv = BN_mpi2bn(priv_key_raw.begin(), priv_key_raw.length(), nullptr);
  EC_KEY_set_private_key(ec_key, priv);
  BN_free(priv);

  EVP_PKEY_set1_EC_KEY(key_pair, ec_key);
  EC_KEY_free(ec_key);
  return key_pair;
}

To store or transfer keys not in binary, but in text format, you can encode the buffer using base64 encoding, which is also supported in OpenSSL:

// Закодировать в base64
ByteArray security::encodeBase64(ByteArray decoded) {
  ByteArray encoded((4*((decoded.length()+2)/3)) + 1);
  EVP_EncodeBlock(encoded.begin(), decoded.begin(), decoded.length());
  return encoded;
}

// Декодировать из base64
ByteArray security::decodeBase64(ByteArray encoded) {
  ByteArray decoded((3*encoded.length()/4) + 1);
  size_t recived_data_size = EVP_DecodeBlock(decoded.begin(), encoded.begin(), encoded.length());
  if(recived_data_size < decoded.length())
    decoded.resize(recived_data_size);
  return decoded;
}

Retrieving a shared secret via the ECDH protocol

Each of the parties generated a key pair and successfully exchanged their public keys. Now each of the parties must receive a “shared secret” hash from which it will be used as the AES 256 key. First, let’s define the structure of the AES 256 key:

struct AES_t {
  // 32 байта для ключа
  uint8_t key[32] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
    0,0
  };
  // 16 байт для вектора инициализации
  uint8_t init_vector[16] = {
    0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0
  };
  
  // В общей сумме размер ключа 48 байт или 384 бита
  
  // Проверка на пустоту ключа
  bool isEmpty() const {
    static const AES_t empty_aes;
    return !std::memcmp(this, &empty_aes, sizeof (AES_t));
  }
  // Очистить ключ
  void clear() {*this = AES_t();}
  // Конструктор по умолчанию
  AES_t() = default;
  // Конструктор из ByteArray
  AES_t(ByteArray data) {
    *this = *reinterpret_cast<AES_t*>(data.begin());
  }
}

As we can see, the structure for the AES 256 key takes 48 bytes or 384 bits, and therefore, to obtain the AES 256 key from the shared secret, a 384 bit hash digest is suitable, that is, such hashing algorithms as sha384 (sha2) and sha3_384 (previously known as Keccak). You should not take the sha3 algorithm as a receiver of the sha2 algorithm, at the moment it is considered that both algorithms are safe enough to use, which is not to say about sha1. Both of these algorithms are supported by OpenSSL, but in this case we will still use the sha3_384 algorithm.

AES_t security::getSecret(ByteArray peer_key, EVP_PKEY* key_pair) {
  EC_KEY *temp_ec_key = nullptr;
  EVP_PKEY *peerkey = nullptr;

  // Извлекаем полученный с другой стороны публичный ключ
  // из сырого буффера в EVP_PKEY*
  temp_ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
  if(temp_ec_key == nullptr)
    handleErrors();
  if(EC_KEY_oct2key(temp_ec_key, peer_key.begin(), peer_key.length(), NULL) != 1)
    handleErrors();
  if(EC_KEY_check_key(temp_ec_key) != 1) handleErrors();
  peerkey = EVP_PKEY_new();
  if(peerkey == NULL)
    handleErrors();
  if(EVP_PKEY_assign_EC_KEY(peerkey, temp_ec_key)!= 1)
    handleErrors();

  // Получение общего секрета
  EVP_PKEY_CTX *derivation_ctx = EVP_PKEY_CTX_new(key_pair, NULL);
  EVP_PKEY_derive_init(derivation_ctx);
  EVP_PKEY_derive_set_peer(derivation_ctx, peerkey);
  size_t lenght;	// Размер общего секрета
  void* ptr;			// Указатель на буффер с общим секретом
  if(1 != EVP_PKEY_derive(derivation_ctx, NULL, &lenght)) handleErrors();
  if(NULL == (ptr = OPENSSL_malloc(lenght))) handleErrors();
  if(1 != (EVP_PKEY_derive(derivation_ctx, (unsigned char*)ptr, &lenght))) handleErrors();
  EVP_PKEY_CTX_free(derivation_ctx);
  EVP_PKEY_free(peerkey);

  // Хэшируем общий секрет и записываем в структуру AES_t
  AES_t aes_key;
  EVP_MD_CTX *mdctx;
  if((mdctx = EVP_MD_CTX_new()) == NULL)
    handleErrors();
  if(1 != EVP_DigestInit_ex(mdctx, EVP_sha384(), NULL))
    handleErrors();
  if(1 != EVP_DigestUpdate(mdctx, ptr, lenght))
    handleErrors();
  unsigned int length;
  if(1 != EVP_DigestFinal_ex(mdctx, (unsigned char*)&aes_key, &length))
    handleErrors();
  EVP_MD_CTX_free(mdctx);
  OPENSSL_free(ptr);
  return aes_key;
}

Encryption and decryption using AES 256

At the moment, both parties have an agreed AES 256 key and now you can proceed directly to data encryption:

ByteArray security::encrypt(ByteArray plain_text, AES_t aes_struct) {
  // Рассчитываем длинну шифротекста
  ByteArray ciphertext(plain_text.length() % AES_BLOCK_SIZE == 0
                       ? plain_text.length()
                       : (plain_text.length() / AES_BLOCK_SIZE + 1) * AES_BLOCK_SIZE);

  // Инициализация контекста шифра
  EVP_CIPHER_CTX *ctx;
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();
  if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, aes_struct.key, aes_struct.init_vector))
    handleErrors();

  // Шифрование исходного текста
  int f_length, s_length;
  if(1 != EVP_EncryptUpdate(ctx, ciphertext.begin(), &f_length, plain_text.begin(), plain_text.length()))
    handleErrors();

  // Иногда для записи шифротекста требутеся дополнительный AES блок
  if(uint64_t(f_length) == ciphertext.length())
    ciphertext.addSize(AES_BLOCK_SIZE);
  else if(uint64_t(f_length) > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

  // Запись последнего AES блока
  if(1 != EVP_EncryptFinal_ex(ctx, ciphertext.begin() + f_length, &s_length))
    handleErrors();
  
  // Уменьшение размера данных до размера записанного шифротекста
  if(uint64_t reuired_length = f_length + s_length; reuired_length < ciphertext.length())
    ciphertext.resize(f_length + s_length);
  else if(reuired_length > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

  // Высвобождения выделенной памяти для контекста шифра
  EVP_CIPHER_CTX_free(ctx);

  return ciphertext;
}

Data decryption looks like this:

ByteArray security::decrypt(ByteArray ciphertext, AES_t aes_struct) {
  ByteArray plain_text(ciphertext.length());

  // Инициализация контекста шифра
  EVP_CIPHER_CTX *ctx;
  if(!(ctx = EVP_CIPHER_CTX_new()))
    handleErrors();
  if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, aes_struct.key, aes_struct.init_vector))
    handleErrors();

  // Дешифровка шифротекста
  int f_length, s_length;
  if(1 != EVP_DecryptUpdate(ctx, plain_text.begin(), &f_length, ciphertext.begin(), ciphertext.length()))
    handleErrors();
  if(1 != EVP_DecryptFinal_ex(ctx, plain_text.begin() + f_length, &s_length))
    handleErrors();
  
  // Уменьшение размера буффера до размера полученных данных
  plain_text.resize(f_length + s_length);

  // Высвобождения выделенной памяти для контекста шифра
  EVP_CIPHER_CTX_free(ctx);

  return plain_text;
}

Usage example

Below is a simple example of using the above code:

#include "security.hpp"
#include <iostream>

int main(int argc, char* argv[]) {
  using namespace security;
  // Алиса генерирует ключ
  EVP_PKEY* alice_key_pair = genKey();
  // Алиса извлекает публичный ключ
  ByteArray alice_peer_key = extractPublicKey(alice_key_pair);
  
  // Боб генерирует ключ
  EVP_PKEY* bob_key_pair = genKey();
  // Боб извлекает публичный ключ
  ByteArray bob_peer_key = extractPublicKey(bob_key_pair);
  
  // Алиса и Боб обмениваются публичными ключами
  // через открытый канал передачи данных
  
  // Боб получает согласованный AES 256 ключ
  AES_t bob_aes_key = getSecret(alice_peer_key, bob_key_pair);
  
  // Алиса получает согласованный AES 256 ключ
  AES_t alice_aes_key = getSecret(bob_peer_key, alice_key_pair);
  
  // Алиса шифрует сообщение
  std::string alice_msg = "Hello, Bob";
  ByteArray alice_msg_buffer(alice_msg.data(), alice_msg.length() + 1);
  ByteArray alice_enc_msg = encrypt(alice_msg_buffer, alice_aes_key);
  // И передаёт зашифрованное сообщение по открытому каналу Бобу
  
  // Боб шифрует сообщение
  std::string bob_msg = "Hello, Alice";
  ByteArray bob_msg_buffer(bob_msg.data(), bob_msg.length() + 1);
  ByteArray bob_enc_msg = encrypt(bob_msg_buffer, bob_aes_key);
  // И передаёт зашифрованное сообщение по открытому каналу Алисе
  
  // Алиса получает и дешифровывет сообщение
  ByteArray alice_recived_msg = decrypt(bob_enc_msg, alice_aes_key);
  std::cout << "Bob: " << (char*)alice_recived_msg.begin() << 'n';
  
  // Боб получает и дешифровывет сообщение
  ByteArray bob_recived_msg = decrypt(alice_enc_msg, bob_aes_key);
  std::cout << "Alice: " << (char*)bob_recived_msg.begin() << 'n';

  return 0;
}

The source code is provided in this GitHub repository

Similar Posts

Leave a Reply