End-to-End encryption with ECDHE Key Agreement
Example applications
ECDHE Key Agreement enables the setup of end-to-end encrypted communication between two clients without user interaction, on a single, potentially insecure channel.
In contrast, password-based end-to-end encryption requires that both clients agree on one password prior to setting up the communication. The password is agreed on via a different channel such as face to face, or fixed on one side and printed in a user guide.
End-to-end encryption with ECDHE Key Agreement can be used for communication with embedded targets or a remote server. It can is also be used for communication between two users, such as for instant messaging.
ECDHE Key Agreement - Example Application
This application demonstrates how to use the ECDHE Key Agreement. All algorithms and cryptography primitives enabling this are provided by emCrypt.
The first instance runs as the "server" and waits for a connection. A second instance runs as the "client" and connects to the server. Both instances generate a random EC key pair and perform a handshake to agree on encryption keys.
The client sends its public key (Qc) to the server and receives the server's public key (Qs). From the reveiced public key (Q') and the own private key (d), both instances compute the master key (K = Q' * d).
Using AES-CMAC-PRF-128 and the master key, the session key data used to encrypt further communication is generated. Now, data can be sent from one instance to the other.
Source Listing
This is the source code for the example application.
The key agreement is shown in _HandshakeServer() and _HandshakeClient(), which make use of _GenKey() to generate a random key pair on both sides
and _GenKeyBlock() to derive the session keys for encryption after key exchange.
Encryption and decryption of packets is done in _Encrypt() and _Decrypt(). The remaining functions are common code for client-server communication.
/*********************************************************************
* (c) SEGGER Microcontroller GmbH *
* The Embedded Experts *
* www.segger.com *
**********************************************************************
-------------------------- END-OF-HEADER -----------------------------
Purpose : Example application to demonstrate E2E encrypted
communication with ECDHE session key agreement,
using emCrypt.
*/
/*********************************************************************
*
* #include section
*
**********************************************************************
*/
#include "CRYPTO.h"
#include "SYS.h"
#include <stdio.h>
#include <stdlib.h>
/*********************************************************************
*
* Defines
*
**********************************************************************
*/
#define COPYRIGHT_STRING "emCrypt E2E Encryption with ECDHE\n(c) 2021 SEGGER Microcontroller GmbH."
#define SERVER_LISTENER_PORT (19099) // TCP/IP port that server listens to
#define MAX_MSG_LEN (2048) // Maximum size (bytes) for a message. This includes a terminating \0.
#define MASTER_KEY_LEN 32 // Size of EC key with chosen curve (on Curve25519: 32 bytes)
#define IV_LEN 8 // Lenth of initialization vector/nonce for AES-CCM
#define TAG_LEN CRYPTO_AES128_KEY_SIZE // Length of authentication tag with AES-CCM
#define CHECK(X) do { if ((Status = (X)) < 0) goto Finally; } while (0)
/*********************************************************************
*
* Local types
*
**********************************************************************
*/
//
// KeyBlock[0]: Client Tx Stream Cipher / Server Rx Stream Cipher
// KeyBlock[1]: Client Tx Counter/IV / Server Rx Counter/IV
// KeyBlock[2]: Client Rx Stream Cipher / Server Tx Stream Cipher
// KeyBlock[3]: Client Rx Counter/IV / Server Tx Counter/IV
//
typedef struct {
U8 aMasterKey[MASTER_KEY_LEN];
U8 aKeyBlock[CRYPTO_AES128_KEY_SIZE * 4];
struct {
CRYPTO_AES_CONTEXT Context;
U8* pIV;
} Rx;
struct {
CRYPTO_AES_CONTEXT Context;
U8* pIV;
} Tx;
} SESSION_CONTEXT;
typedef struct {
SYS_SOCKET_HANDLE hSock;
SESSION_CONTEXT* pSession;
} THREAD_INFO;
/*********************************************************************
*
* Static data
*
**********************************************************************
*/
static SEGGER_MEM_CONTEXT _MemContext;
static volatile int _ErrorOccurred; // Used by threads to determine if an error occurred
/*********************************************************************
*
* Static code
*
**********************************************************************
*/
/*********************************************************************
*
* _HexDump()
*
* Function description
* Dump hex data.
*
* Parameters
* sText - Pointer to title.
* pData - Pointer to octet string.
* DataLen - Octet length of octet string.
*/
static void _HexDump(const char *sText, const U8 *pData, unsigned DataLen) {
unsigned i;
//
printf("%s:", sText);
for (i = 0; i < DataLen; ++i) {
if (i % 16u == 0) {
printf("\n ");
}
printf(" %02x", pData[i]);
}
printf("\n");
}
/*********************************************************************
*
* _GenKey()
*
* Function description
* Generate a random key on EC Curve25519.
*
* Parameters
* pd - Pointer to store private key data.
* pQ - Pointer to store public key data.
*/
static int _GenKey(CRYPTO_MPI* pd, CRYPTO_MPI* pQ) {
int Status;
CHECK(CRYPTO_MPI_RandomBits(pd, 255));
CHECK(CRYPTO_EC_Curve25519_Clamp(pd));
CHECK(CRYPTO_EC_X25519_BaseMul(pQ, pd, &_MemContext));
//
Status = 0;
Finally:
return Status;
}
/*********************************************************************
*
* _GenKeyBlock()
*
* Function description
* Generate a session key block
* from the private key and the received public key.
*
* Parameters
* pSession - Pointer to session data.
* pd - Pointer to private key.
* pQ - Pointer to public key.
*/
static int _GenKeyBlock(SESSION_CONTEXT* pSession, CRYPTO_MPI* pd, CRYPTO_MPI* pQ) {
int Status;
//
// Compute shared secret, K, which is the X coordinate of d*Q_S.
//
CHECK(CRYPTO_EC_X25519_Mul(pQ, pd, &_MemContext));
CRYPTO_MPI_StoreBytes(pQ, pSession->aMasterKey, sizeof(pSession->aMasterKey));
_HexDump("Master key", pSession->aMasterKey, sizeof(pSession->aMasterKey));
//
// Generate key blocks from master key, using AES-CMAC-PRF-128.
//
CRYPTO_CMAC_AES_PRF_128_Calc(pSession->aMasterKey, sizeof(pSession->aMasterKey), (const U8 *)"key block A", 11, &pSession->aKeyBlock[0*CRYPTO_AES128_KEY_SIZE]);
CRYPTO_CMAC_AES_PRF_128_Calc(pSession->aMasterKey, sizeof(pSession->aMasterKey), (const U8 *)"key block B", 11, &pSession->aKeyBlock[1*CRYPTO_AES128_KEY_SIZE]);
CRYPTO_CMAC_AES_PRF_128_Calc(pSession->aMasterKey, sizeof(pSession->aMasterKey), (const U8 *)"key block C", 11, &pSession->aKeyBlock[2*CRYPTO_AES128_KEY_SIZE]);
CRYPTO_CMAC_AES_PRF_128_Calc(pSession->aMasterKey, sizeof(pSession->aMasterKey), (const U8 *)"key block D", 11, &pSession->aKeyBlock[3*CRYPTO_AES128_KEY_SIZE]);
//
_HexDump("Key block", pSession->aKeyBlock, sizeof(pSession->aKeyBlock));
//
Status = 0;
Finally:
return Status;
}
/*********************************************************************
*
* _HandshakeServer()
*
* Function description
* Run server handshake.
*
* Parameters
* pSession - Pointer to session data.
* hSock - Socket handle of the connection.
*/
static int _HandshakeServer(SESSION_CONTEXT *pSession, SYS_SOCKET_HANDLE hSock) {
CRYPTO_MPI d;
CRYPTO_MPI Q_S;
CRYPTO_MPI Q_C;
U8 aKey[32];
int Status;
//
CRYPTO_MPI_Init(&Q_S, &_MemContext);
CRYPTO_MPI_Init(&Q_C, &_MemContext);
CRYPTO_MPI_Init(&d, &_MemContext);
//
// Generate server's ephemeral session key pair
//
CHECK(_GenKey(&d, &Q_S));
//
// Receive client's public key.
//
#ifdef _LOG_VERBOSE
printf("Receive client's public key\n");
#endif
SYS_SOCKET_Receive(hSock, (char *)aKey, sizeof(aKey));
CRYPTO_MPI_LoadBytesLE(&Q_C, aKey, sizeof(aKey));
//
// Send server's public key.
//
#ifdef _LOG_VERBOSE
printf("Send server's public key\n");
#endif
CRYPTO_MPI_StoreBytesLE(&Q_S, aKey, sizeof(aKey));
SYS_SOCKET_Send(hSock, (char *)aKey, sizeof(aKey));
//
// Generate key block
//
CHECK(_GenKeyBlock(pSession, &d, &Q_C));
//
// Initialize AEAD cipher streams.
//
CRYPTO_AES_InitEncrypt(&pSession->Rx.Context, &pSession->aKeyBlock[0*CRYPTO_AES128_KEY_SIZE], CRYPTO_AES128_KEY_SIZE);
pSession->Rx.pIV = &pSession->aKeyBlock[1*CRYPTO_AES128_KEY_SIZE];
CRYPTO_AES_InitEncrypt(&pSession->Tx.Context, &pSession->aKeyBlock[2*CRYPTO_AES128_KEY_SIZE], CRYPTO_AES128_KEY_SIZE);
pSession->Tx.pIV = &pSession->aKeyBlock[3*CRYPTO_AES128_KEY_SIZE];
//
CRYPTO_MPI_Kill(&Q_S);
CRYPTO_MPI_Kill(&Q_C);
CRYPTO_MPI_Kill(&d);
//
Finally:
return Status;
}
/*********************************************************************
*
* _HandshakeClient()
*
* Function description
* Run client handshake.
*
* Parameters
* pSession - Pointer to session data.
* hSock - Socket handle of the connection.
*/
static int _HandshakeClient(SESSION_CONTEXT *pSession, SYS_SOCKET_HANDLE hSock) {
CRYPTO_MPI d;
CRYPTO_MPI Q_S;
CRYPTO_MPI Q_C;
U8 aKey[32];
int Status;
//
CRYPTO_MPI_Init(&Q_S, &_MemContext);
CRYPTO_MPI_Init(&Q_C, &_MemContext);
CRYPTO_MPI_Init(&d, &_MemContext);
//
// Generate clients's ephemeral session key pair
//
CHECK(_GenKey(&d, &Q_C));
//
// Send client's public key.
//
#ifdef _LOG_VERBOSE
printf("Send client's public key\n");
#endif
CRYPTO_MPI_StoreBytesLE(&Q_C, aKey, sizeof(aKey));
SYS_SOCKET_Send(hSock, (char *)aKey, sizeof(aKey));
//
// Receive server's public key.
//
#ifdef _LOG_VERBOSE
printf("Receive server's public key\n");
#endif
SYS_SOCKET_Receive(hSock, (char *)aKey, sizeof(aKey));
CRYPTO_MPI_LoadBytesLE(&Q_S, aKey, sizeof(aKey));
//
// Generate key block
//
CHECK(_GenKeyBlock(pSession, &d, &Q_S));
//
// Initialize AEAD cipher streams.
//
CRYPTO_AES_InitEncrypt(&pSession->Tx.Context, &pSession->aKeyBlock[0*CRYPTO_AES128_KEY_SIZE], CRYPTO_AES128_KEY_SIZE);
pSession->Tx.pIV = &pSession->aKeyBlock[1*CRYPTO_AES128_KEY_SIZE];
CRYPTO_AES_InitEncrypt(&pSession->Rx.Context, &pSession->aKeyBlock[2*CRYPTO_AES128_KEY_SIZE], CRYPTO_AES128_KEY_SIZE);
pSession->Rx.pIV = &pSession->aKeyBlock[3*CRYPTO_AES128_KEY_SIZE];
//
CRYPTO_MPI_Kill(&Q_S);
CRYPTO_MPI_Kill(&Q_C);
CRYPTO_MPI_Kill(&d);
//
Finally:
return Status;
}
/*********************************************************************
*
* _Encrypt()
*
* Function description
* Encrypt and authenticate an input string to a packet.
*
* Parameters
* pSession - Pointer to connection state.
* pSrc - Source buffer.
* DestLen - Length of source buffer.
* pDest - Destination buffer.
* DestLen - Length of destination buffer. Has to be at least SrcLen + 4 + TAG_LEN.
*
* Additional information
* Packet = Input Length + Authentication Tag + Encrypted data
*/
static int _Encrypt(SESSION_CONTEXT *pSession, const char* pSrc, U32 SrcLen, U8* pDest, U32 DestLen) {
//
// Write input length to packet
//
CRYPTO_WRU32LE(pDest, SrcLen);
//
// Write authentication tag and encrypted data to packet.
//
CRYPTO_AES_CCM_Encrypt(&pSession->Tx.Context,
&pDest[4 + TAG_LEN],
&pDest[4],
TAG_LEN,
(const U8*)pSrc,
SrcLen,
NULL,
0,
pSession->Tx.pIV,
IV_LEN);
//
// Increment counter value
//
CRYPTO_IncCTRBE(pSession->Tx.pIV, IV_LEN, 1);
return SrcLen + 4 + TAG_LEN;
}
/*********************************************************************
*
* _Decrypt()
*
* Function description
* Decrypt encrypted data.
*
* Parameters
* pSession - Pointer to connection state.
* pSrc - Source buffer.
* DestLen - Length of source buffer.
* pDest - Destination buffer.
* DestLen - Length of destination buffer. Has to be at least SrcLen - TAG_LEN.
*
* Additional information
* For in-place decryption, pDest may be == pSrc + TAG_LEN.
*/
static int _Decrypt(SESSION_CONTEXT *pSession, const U8* pSrc, U32 SrcLen, U8* pDest, U32 DestLen) {
int Status;
//
// Decrypt data and authentication tag.
//
Status = CRYPTO_AES_CCM_Decrypt(&pSession->Rx.Context,
pDest,
pSrc,
TAG_LEN,
pSrc + TAG_LEN,
SrcLen,
NULL,
0,
pSession->Rx.pIV,
IV_LEN);
//
// Increment counter value
//
CRYPTO_IncCTRBE(pSession->Rx.pIV, IV_LEN, 1);
return Status;
}
/*********************************************************************
*
* _SendThread()
*
* Function description
* Thread that is responsible for handling data to send from the server to the client.
*/
static SYS_THREAD_PROC_EX_TYPE _SendThread(void* p) {
int Result;
int r;
U32 Len;
char acIn [MAX_MSG_LEN];
U8 abOut[MAX_MSG_LEN + 4 + TAG_LEN];
THREAD_INFO* pInfo;
Result = 0;
pInfo = (THREAD_INFO*)p;
do {
SYS_ConsoleGetString("> ", acIn, MAX_MSG_LEN);
Len = strlen(acIn) + 1; // Include terminating \0
Len = _Encrypt(pInfo->pSession, acIn, Len, abOut, sizeof(abOut));
#ifdef _LOG_VERBOSE
_HexDump("Sending packet", abOut, Len);
#endif
r = SYS_SOCKET_Send(pInfo->hSock, abOut, Len);
if (r != Len) { // Failed to send data? => Done
printf("ERROR: Failed to send data to peer\n");
Result = -1;
_ErrorOccurred = 1;
break;
}
} while (_ErrorOccurred == 0);
return Result;
}
/*********************************************************************
*
* _RecvThread()
*
* Function description
* Thread that is responsible for handling data to receive by the server from the client.
*/
static SYS_THREAD_PROC_EX_TYPE _RecvThread(void* p) {
int Result;
int r;
U8 abIn [MAX_MSG_LEN + 4 + TAG_LEN + 1];
THREAD_INFO* pInfo;
U32 DataLen;
Result = 0;
pInfo = (THREAD_INFO*)p;
do {
r = SYS_SOCKET_IsReadable(pInfo->hSock, 100); // Poll for data every 100 ms
if (r == 0) { // Nothing to read yet? => Try again.
SYS_Sleep(10);
continue;
}
//
// Receive data length
//
r = SYS_SOCKET_Receive(pInfo->hSock, &abIn[0], 4);
if (r != 4) {
printf("ERROR: Failed to receive data length\n");
Result = -1;
_ErrorOccurred = 1;
break;
}
DataLen = CRYPTO_RDU32LE(&abIn[0]);
if ((4 + TAG_LEN + DataLen) > sizeof(abIn)) {
printf("Bad packet length\n");
Result = -1;
_ErrorOccurred = 1;
break;
}
//
// Receive tag and data
//
r = SYS_SOCKET_Receive(pInfo->hSock, &abIn[4], TAG_LEN + DataLen);
if (r != (TAG_LEN + DataLen)) {
printf("ERROR: Failed to receive data from client\n");
Result = -1;
_ErrorOccurred = 1;
break;
}
//
#ifdef _LOG_VERBOSE
_HexDump("Received packet", abIn, 4 + TAG_LEN + DataLen);
#endif
//
// Decrypt data
//
r = _Decrypt(pInfo->pSession, &abIn[4], DataLen, &abIn[4 + TAG_LEN], sizeof(abIn) - (4 + TAG_LEN));
if (r == 0) {
abIn[4+TAG_LEN+DataLen] = 0;
printf("\n< %s", &abIn[4+TAG_LEN]);
} else {
printf("Authentication tag error\n");
}
printf("\n> ");
} while (_ErrorOccurred == 0);
return Result;
}
/*********************************************************************
*
* _RunServer()
*
* Function description
* Runs server
*
* Parameters
* hSock Opened TCP socket listening on SERVER_LISTENER_PORT
*
*/
static int _RunServer(SYS_SOCKET_HANDLE hSockListen) {
int Result;
int r;
SYS_SOCKET_HANDLE hSockClient;
SESSION_CONTEXT* pSession;
THREAD_INFO* pThreadInfo;
//
// Start with title
//
Result = 0;
pSession = NULL;
pThreadInfo = NULL;
hSockClient = SYS_SOCKET_INVALID_HANDLE;
printf("\n\n");
printf(COPYRIGHT_STRING "\n");
printf("Operating in server mode\n");
printf("\n\n");
//
// Allocate session context
//
pSession = (SESSION_CONTEXT*)malloc(sizeof(SESSION_CONTEXT));
if (pSession == NULL) {
printf("ERROR: Insufficient memory.\n");
Result = -1;
goto Done;
}
memset(pSession, 0, sizeof(SESSION_CONTEXT));
//
// Wait for localhost client to connect
//
printf("Waiting for client to connect (Port = %d)...", SERVER_LISTENER_PORT);
hSockClient = SYS_SOCKET_Accept(hSockListen);
if (hSockListen == SYS_SOCKET_INVALID_HANDLE) { // Failed to open socket? => Done
printf("ERROR: An error occurred while waiting for a client to connect.\n");
Result = -1;
goto Done;
}
printf("OK\n\n");
SYS_SOCKET_SetBlocking(hSockClient); // Make sure socket operations are blocking
//
// After a client has connected, perform the handshake
// to setup the encrypted communication.
//
r = _HandshakeServer(pSession, hSockClient);
if (r < 0) {
printf("ERROR: Cannot negotiate a secure connection.\n");
Result = -1;
goto Done;
}
//
// After a successful handshake, E2EE communication between server and client can begin.
//
pThreadInfo = (THREAD_INFO*)malloc(sizeof(THREAD_INFO));
if (pThreadInfo == NULL) {
printf("ERROR: Insufficient memory.\n");
Result = -1;
goto Done;
}
pThreadInfo->hSock = hSockClient;
pThreadInfo->pSession = pSession;
SYS_SOCKET_SetNonBlocking(hSockClient);
printf("\n========== Secure connection established. ==========\n========== Type anything and press <Enter> to send. ==========\n\n");
SYS_CreateThreadEx(_SendThread, pThreadInfo, NULL, "Sender thread", 0);
SYS_CreateThreadEx(_RecvThread, pThreadInfo, NULL, "Receiver thread", 0);
//
// From here on, the other threads handle further communication
//
while(_ErrorOccurred == 0);
SYS_Sleep(100); // If one thread reported an error, the other might still access some resources. Give the other thread enought time to terminate.
Done:
//
// Clean-up
//
if (hSockClient != SYS_SOCKET_INVALID_HANDLE) {
SYS_SOCKET_Close(hSockClient);
}
if (hSockListen != SYS_SOCKET_INVALID_HANDLE) {
SYS_SOCKET_Close(hSockListen);
}
if (pSession) {
free(pSession);
}
if (pThreadInfo) {
free(pThreadInfo);
}
return Result;
}
/*********************************************************************
*
* _RunClient()
*
* Function description
* Runs client
*
* Parameters
* hSock Opened TCP socket
*/
static int _RunClient(SYS_SOCKET_HANDLE hSock) {
int Result;
int r;
SESSION_CONTEXT* pSession;
THREAD_INFO* pThreadInfo;
//
// Start with title
//
Result = 0;
pSession = NULL;
pThreadInfo = NULL;
printf("\n\n");
printf(COPYRIGHT_STRING "\n");
printf("Operating in client mode\n");
printf("\n\n");
//
// Connect to server
//
printf("Connecting to server...");
SYS_SOCKET_SetBlocking(hSock);
r = SYS_SOCKET_Connect(hSock, SYS_SOCKET_IP_ADDR_LOCALHOST, SERVER_LISTENER_PORT);
if (r != 0) { // Failed to connect? => Done
printf("ERROR: Failed to connect to server.\n");
Result = -1;
goto Done;
}
printf("OK\n\n");
//
// Allocate session context
//
pSession = (SESSION_CONTEXT*)malloc(sizeof(SESSION_CONTEXT));
if (pSession == NULL) {
printf("ERROR: Insufficient memory.\n");
Result = -1;
goto Done;
}
memset(pSession, 0, sizeof(SESSION_CONTEXT));
//
// After a client has connected, perform the handshake
// to setup the encrypted communication.
//
r = _HandshakeClient(pSession, hSock);
if (r < 0) {
printf("ERROR: Cannot negotiate a secure connection.\n");
Result = -1;
goto Done;
}
//
// After a successful handshake, E2EE communication between server and client can begin.
//
pThreadInfo = (THREAD_INFO*)malloc(sizeof(THREAD_INFO));
if (pThreadInfo == NULL) {
printf("ERROR: Insufficient memory.\n");
Result = -1;
goto Done;
}
pThreadInfo->hSock = hSock;
pThreadInfo->pSession = pSession;
SYS_SOCKET_SetNonBlocking(hSock);
printf("\n========== Secure connection established. ==========\n========== Type anything and press <Enter> to send. ==========\n\n");
SYS_CreateThreadEx(_SendThread, pThreadInfo, NULL, "Sender thread", 0);
SYS_CreateThreadEx(_RecvThread, pThreadInfo, NULL, "Receiver thread", 0);
//
// From here on, the other threads handle further communication
//
while(_ErrorOccurred == 0);
SYS_Sleep(100); // If one thread reported an error, the other might still access some resources. Give the other thread enought time to terminate.
Done:
//
// Clean-up
//
if (hSock != SYS_SOCKET_INVALID_HANDLE) {
SYS_SOCKET_Close(hSock);
}
if (pSession) {
free(pSession);
}
if (pThreadInfo) {
free(pThreadInfo);
}
return Result;
}
/*********************************************************************
*
* Public code
*
**********************************************************************
*/
/*********************************************************************
*
* main()
*
* Function description
* Main entry point for application.
*/
int main(void) {
int Result;
int r;
SYS_SOCKET_HANDLE hSock;
setvbuf(stdout, (char*)NULL, _IONBF, 0);
CRYPTO_Init();
SEGGER_MEM_SYSTEM_HEAP_Init(&_MemContext);
//
// Determine if this is the server or client side
//
Result = 0;
hSock = SYS_SOCKET_OpenTCP();
if (hSock == SYS_SOCKET_INVALID_HANDLE) { // Failed to open socket? => Done
printf("ERROR: Failed to open TCP socket.\n");
Result = -1;
goto Done;
}
r = SYS_SOCKET_ListenAtTCPAddr(hSock, SYS_SOCKET_IP_ADDR_LOCALHOST, SERVER_LISTENER_PORT, 1);
if (r == 0) { // We are able to listen at server listener port? => Run server
Result = _RunServer(hSock); // Blocking until server stops
} else { // We cannot listen at server listener port? => Server must already be running so run client.
Result = _RunClient(hSock); // Blocking until client stops
}
Done:
return Result;
}
/*************************** End of file ****************************/