Skip navigation

Secure Communication with End-to-End Encryption

emCrypt includes all algorithms, functions and crypto primitives to implement various security schemes and protocols. emSSL (SSL and TLS) and emSSH are most advanced protocols, using emCrypt as its foundation.

This application demonstrates how to easily enable end-to-end encrypted communication between two clients which may be directly connected or communicate via a server. The source code is a reference for authenticated and encrypted peer-to-peer communication in less than 300 lines of code.

Example Application

The Secure Communication Example demonstrates the use authentication and end-to-end encryption using a password.

emcrypt-securecomm-server.png

Secure Communication Server

emcrypt-securecomm-client.png

Secure Communication Client

emcrypt-securecomm-wireshark.png

Encrypted Communication on the wire

When the first instance of the application is started, it runs as the "server", waiting for a connection. When a second instance is started, it runs as the "client" and connects to the server.
In both cases the password needs to be entered, and it has to be the same for server and client.

When the client connects to the server, the server challenges the client to make sure it uses the same password, to authenticate it. Once the client is authenticated, server and client can compute the session key used to encrypt further communication. Now data can be sent from client to server and vice-versa.

Note: The application is built to open a connection on localhost. Both instances have to run on the same host. This is only done to make it easily usable without any firewall configuration. The example can be reused to open connections to any host or even be tunneled via a server both clients connect to.

End-to-End Encryption with emCrypt

For the end-to-end encryption this example uses the password-based encryption scheme (PBES2) and a symmetric key algorithm to establish a secured half-duplex communication channel. The application can be extended to full-duplex communication by creating two encryption contexts.

Due to the symmetric key algorithm, both clients share the same key, based on a password known to both sides. AES is chosen as the symmetric key algorithm. It is used as a stream cipher (in OFB mode) to encrypt the whole communication stream rather than each message.

The Challenge Handshake Authentication Protocol (CHAP) is used to make sure the peer uses the same password without transmitting it. Additionally PBKDF2 (Password Based Key Derivation Function 2) is used to generate a session key from the password.

AES, PBKDF2, and SHA256 (used for CHAP), are available as part of emCrypt with ready-to-use APIs. With these functions, end-to-end encryption can easily be implemented in any application.

Source Listing

/*********************************************************************
*                   (c) SEGGER Microcontroller GmbH                  *
*                        The Embedded Experts                        *
*                           www.segger.com                           *
**********************************************************************

-------------------------- END-OF-HEADER -----------------------------

File        : APP_E2EE.c
Purpose     : Demonstrate how to setup a server/client with E2EE using emCrypt.
*/

/*********************************************************************
*
*       #include section
*
**********************************************************************
*/

#include "CRYPTO.h"
#include "SYS.h"
#include <stdio.h>
#include <stdlib.h>

/*********************************************************************
*
*       Defines
*
**********************************************************************
*/

#define COPYRIGHT_STRING      "emCrypt Communication Sample, (c) 2019 SEGGER Microcontroller GmbH."
#define DEFAULT_PASSWORD      "Secret"  // Default password
#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 SERVER_CONN_ACCEPT    (0x00)      // Value sent by server on success
#define SERVER_CONN_DENY      (0xFF)      // Value sent by server on fail

/*********************************************************************
*
*       Local types
*
**********************************************************************
*/

typedef struct {
  U8  abPassword[32];
  U32 PasswordLen;
  U8  abSalt[16];
  U32 SaltLen;
  struct {
    U8 aKey[CRYPTO_AES256_KEY_SIZE];
    U8 aIV[CRYPTO_AES256_KEY_SIZE];
  } Session;
  //
  struct {
    U8 aBlock[CRYPTO_AES_BLOCK_SIZE];
    unsigned Index;
  } Keystream;
  CRYPTO_AES_CONTEXT CipherContext;
} CONNECTION_STATE;

typedef struct {
  SYS_SOCKET_HANDLE hSock;
  CONNECTION_STATE* pState;
} THREAD_INFO;

/*********************************************************************
*
*       Static data
*
**********************************************************************
*/

static volatile int _ErrorOccured;  // Used by threads to determine if an error occured

/*********************************************************************
*
*       Static code
*
**********************************************************************
*/

/*********************************************************************
*
*       _DeriveKey()
*
*  Function description
*    Derive session keys.
*
*  Parameters
*    pState - Pointer to connection state.
*/
static void _DeriveKey(CONNECTION_STATE *pState) {
  CRYPTO_PBKDF2_HMAC_SHA256_Calc(pState->abSalt, pState->SaltLen,
                                 pState->abPassword, pState->PasswordLen,
                                 10000,
                                 (U8 *)&pState->Session, sizeof(pState->Session));
  //
  CRYPTO_AES_InitEncrypt(&pState->CipherContext, pState->Session.aKey, sizeof(pState->Session.aKey));
  CRYPTO_AES_Encrypt(&pState->CipherContext, pState->Keystream.aBlock, pState->Session.aIV);
  //
  pState->Keystream.Index = 0;
}

/*********************************************************************
*
*       _Encrypt()
*
*  Function description
*    Encrypt plain data.
*
*  Parameters
*    pState   - Pointer to connection state.
*    pSrc     - Source buffer.
*    pDest    - Destination buffer.
*    NumBytes - Length of buffers in bytes.
*/
static void _Encrypt(CONNECTION_STATE *pState, const void* pSrc, void* pDest, U32 NumBytes) {
  U8*  pCipher;
  U32  i;
  //
  // Exclusive-or key stream with plain data.
  //
  pCipher = (U8*)pDest;
  memcpy(pCipher, pSrc, NumBytes);
  for (i = 0; i < NumBytes; ++i) {
    pCipher[i] ^= pState->Keystream.aBlock[pState->Keystream.Index];
    if (++pState->Keystream.Index == CRYPTO_AES_BLOCK_SIZE) {
      CRYPTO_AES_Encrypt(&pState->CipherContext, pState->Keystream.aBlock, pState->Keystream.aBlock);
      pState->Keystream.Index = 0;
    }
  }
}

/*********************************************************************
*
*       _Decrypt()
*
*  Function description
*    Decrypt encrypted data.
*
*  Parameters
*    pState   - Pointer to connection state.
*    pSrc     - Source buffer.
*    pDest    - Destination buffer.
*    NumBytes - Length of buffers in bytes.
*/
static void _Decrypt(CONNECTION_STATE *pState, const void* pSrc, void* pDest, U32 NumBytes) {
  U8*  pCipher;
  U32  i;
  //
  // Exclusive-or key stream with plain data.
  //
  pCipher = (U8*)pDest;
  memcpy(pCipher, pSrc, NumBytes);
  for (i = 0; i < NumBytes; ++i) {
    pCipher[i] ^= pState->Keystream.aBlock[pState->Keystream.Index];
    if (++pState->Keystream.Index == CRYPTO_AES_BLOCK_SIZE) {
      CRYPTO_AES_Encrypt(&pState->CipherContext, pState->Keystream.aBlock, pState->Keystream.aBlock);
      pState->Keystream.Index = 0;
    }
  }
}

/*********************************************************************
*
*       _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];
  THREAD_INFO* pInfo;

  Result = 0;
  pInfo = (THREAD_INFO*)p;
  do {
    SYS_ConsoleGetString("", acIn, MAX_MSG_LEN);
    Len = strlen(acIn) + 1;  // Include terminating \0
    _Encrypt(pInfo->pState, acIn, abOut, Len);
    r = SYS_SOCKET_Send(pInfo->hSock, abOut, Len);
    if (r != Len) {  // Failed to send data? => Done
      printf("ERROR: Failed to send data to client\n");
      Result = -1;
      _ErrorOccured = 1;
      break;
    }
  } while (_ErrorOccured == 0);
  return (SYS_THREAD_PROC_EX_TYPE)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];
  char acOut[MAX_MSG_LEN + 1];
  THREAD_INFO* pInfo;

  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.
      continue;
    }
    r = SYS_SOCKET_Receive(pInfo->hSock, abIn, MAX_MSG_LEN);
    if (r <= 0) {
      printf("ERROR: Failed to receive data from client\n");
      Result = -1;
      _ErrorOccured = 1;
      break;
    }
    _Decrypt(pInfo->pState, abIn, acOut, r);
    printf(acOut);
    printf("\n");
  } while (_ErrorOccured == 0);
  return (SYS_THREAD_PROC_EX_TYPE)Result;
}

/*********************************************************************
*
*       _RunServer()
*
*  Function description
*    Runs server
*
*  Parameters
*    hSock    Opened TCP socket listening on SERVER_LISTENER_PORT
*
*/
static int _RunServer(SYS_SOCKET_HANDLE hSockListen) {
  U32      HashLen;
  U32      TmpLen;
  int      Result;
  int      r;
  char     acPass[32];
  const char* sPass;
  SYS_SOCKET_HANDLE hSockClient;
  U8       abTmp[256];  // Must be large enough to fit password + salt
  U8       abHash[32];
  U8       abClient[32];
  U8       Stat;
  CONNECTION_STATE* pState;
  THREAD_INFO* pThreadInfo;
  //
  // Start with title
  //
  Result      = 0;
  pState      = NULL;
  pThreadInfo = NULL;
  hSockClient = SYS_SOCKET_INVALID_HANDLE;
  printf("\n\n");
  printf(COPYRIGHT_STRING "\n");
  printf("Operating in server mode\n");
  printf("\n\n");
  //
  // Generate random 16 byte salt
  //
  pState = (CONNECTION_STATE*)malloc(sizeof(CONNECTION_STATE));
  if (pState == NULL) {
    printf("ERROR: Insufficient memory.\n");
    Result = -1;
    goto Done;
  }
  CRYPTO_Init();
  pState->SaltLen = sizeof(pState->abSalt);
  CRYPTO_RNG_Get(pState->abSalt, pState->SaltLen);
  //
  // Ask user for password to use
  //
  printf("Set the password (Default = \"%s\") ", DEFAULT_PASSWORD);
  SYS_ConsoleGetString("> ", acPass, sizeof(acPass));
  sPass = (acPass[0]) ? &acPass[0] : DEFAULT_PASSWORD;
  //
  // Append password to salt and generate hash
  // The resulting hash is the result of the challenge used by CHAP to authenticate a client.
  //
  pState->PasswordLen = strlen(sPass);
  memcpy(pState->abPassword, sPass, pState->PasswordLen + 1);
  HashLen = sizeof(abHash);
  memcpy(&abTmp[0], pState->abSalt, pState->SaltLen);
  memcpy(&abTmp[pState->SaltLen], pState->abPassword, pState->PasswordLen);
  TmpLen = pState->PasswordLen + pState->SaltLen;
  CRYPTO_SHA256_Calc(abHash, HashLen, abTmp, TmpLen);
  //
  // 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 occured while waiting for a client to connect.\n");
    Result = -1;
    goto Done;
  }
  printf("OK\n");
  //
  // After a client has connected, send challenge to client and wait for it to "solve" the challenge.
  //
  // <Direction>    <NumBytes>    <Explanation>
  // S -> C         16            Salt (Challenge)
  // C -> S         32            Hash (Result)
  // S -> C         1             Server status
  //
  SYS_SOCKET_SetBlocking(hSockClient);  // Make sure socket operations are blocking
  r = SYS_SOCKET_Send(hSockClient, pState->abSalt, pState->SaltLen);
  if (r != pState->SaltLen) {  // Failed to send challenge? => Done
    printf("ERROR: An error occured while sending data to the client.\n");
    Result = -1;
    goto Done;
  }
  r = SYS_SOCKET_Receive(hSockClient, abClient, HashLen);
  if (r != HashLen) {  // Failed to receive data? => Done
    printf("ERROR: An error occured while receiving data from the client.\n");
    Result = -1;
    goto Done;
  }
  r = memcmp(abClient, abHash, HashLen);                    // Compare client's response with expected hash value
  Stat = (r == 0) ? SERVER_CONN_ACCEPT : SERVER_CONN_DENY;
  SYS_SOCKET_Send(hSockClient, &Stat, 1);                   // Tell client if connection was accepted or denied
  if (r != 0) {                                             // Client's response is not correct? => Done
    printf("ERROR: Client failed to register (Incorrect password?).\n");
    Result = -1;
    goto Done;
  }
  //
  // After a successful authentication, E2EE communication between server and client can begin.
  // For that, a key is derived from the password and salt.
  //
  _DeriveKey(pState);
  pThreadInfo = (THREAD_INFO*)malloc(sizeof(THREAD_INFO));
  if (pThreadInfo == NULL) {
    printf("ERROR: Insufficient memory.\n");
    Result = -1;
    goto Done;
  }
  pThreadInfo->hSock  = hSockClient;
  pThreadInfo->pState = pState;
  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(_ErrorOccured == 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 (pState) {
    free(pState);
  }
  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;
  U32   TmpLen;
  U32   HashLen;
  U8    abTmp[256];           // Must be large enough to fit password + salt
  U8    abHash[32];
  U8    Stat;
  char  acPass[32];
  const char* sPass;
  CONNECTION_STATE* pState;
  THREAD_INFO* pThreadInfo;
  //
  // Start with title
  //
  Result      = 0;
  pState      = 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");
  //
  // Prompt user for password.
  // The password is needed for solving the server's challenge correctly
  //
  pState = (CONNECTION_STATE*)malloc(sizeof(CONNECTION_STATE));
  if (pState == NULL) {
    printf("ERROR: Insufficient memory.\n");
    Result = -1;
    goto Done;
  }
  printf("Please enter the password (Default: \"%s\") ", DEFAULT_PASSWORD);
  SYS_ConsoleGetString("> ", acPass, sizeof(acPass));
  sPass = (acPass[0]) ? &acPass[0] : DEFAULT_PASSWORD;
  pState->PasswordLen = strlen(sPass);
  memcpy(pState->abPassword, sPass, pState->PasswordLen + 1);
  //
  // Receive and solve challenge from server
  // <Direction>    <NumBytes>    <Explanation>
  // S -> C         16            Salt (Challenge)
  //
  pState->SaltLen = sizeof(pState->abSalt);
  r = SYS_SOCKET_Receive(hSock, pState->abSalt, pState->SaltLen);
  if (r != pState->SaltLen) {
    printf("ERROR: Failed to receive data from server.\n");
    Result = -1;
    goto Done;
  }
  memcpy(&abTmp[0], pState->abSalt, pState->SaltLen);
  memcpy(&abTmp[pState->SaltLen], pState->abPassword, pState->PasswordLen);
  TmpLen = pState->PasswordLen + pState->SaltLen;
  HashLen = sizeof(abHash);
  CRYPTO_Init();
  CRYPTO_SHA256_Calc(abHash, HashLen, abTmp, TmpLen);
  //
  // Register at server using result of challenge
  // <Direction>    <NumBytes>    <Explanation>
  // C -> S         32            Hash (Result)
  // S -> C         1             Server status
  //
  r = SYS_SOCKET_Send(hSock, abHash, HashLen);
  if (r != HashLen) {
    printf("ERROR: Failed to send data to server.\n");
    Result = -1;
    goto Done;
  }
  r = SYS_SOCKET_Receive(hSock, &Stat, 1);  // Get status of connection
  if (r != 1) {
    printf("ERROR: Failed to receive data to server.\n");
    Result = -1;
    goto Done;
  }
  if (Stat != SERVER_CONN_ACCEPT) {
    printf("ERROR: Server denied connection (Incorrect password?).\n");
    Result = -1;
    goto Done;
  }
  //
  // After a successful authentication, E2EE communication between server and client can begin.
  // For that, a key is derived from the password and salt.
  //
  _DeriveKey(pState);
  pThreadInfo = (THREAD_INFO*)malloc(sizeof(THREAD_INFO));
  if (pThreadInfo == NULL) {
    printf("ERROR: Insufficient memory.\n");
    Result = -1;
    goto Done;
  }
  pThreadInfo->hSock  = hSock;
  pThreadInfo->pState = pState;
  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(_ErrorOccured == 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 (pState) {
    free(pState);
  }
  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;
  //
  // 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 ****************************/

Further Use

The example demonstrates a simple use case of authentication and end-to-end encryption.

Full-Duplex Communication

The communication channel is half-duplex. All data has to be received before data can be transmitted. The application can be extended to full-duplex communication by adding a second keystream, which is based on another key, such as a second salt.

Key Agreement

In the example, the data used for authentication, the password and the salt, are used to generate the key for the encrypted communication, using PBKDF2. Other key agreement mechanisms, such as Diffie-Hellman Key Exchange (DH or ECDH) could be used to get an encryption key that is not associated with the authentication parameters.