Establishing secure ESP8266 and NodeJs communication by using Diffie-Hellman key exchange and Elliptic Curves

Introduction:
One of the issues of my later posts ( ESP8266 and AES128 for end-to-end encryption and ESP8266 – Logging data in a backend – AES and Crypto-JS) is that uses symmetric key AES128 to encrypt and decrypt data, and that the key that is used is pre-shared, meaning that it’s hardcoded on the code and is the same key to be used in all cryptographic operations.

While this might not be an issue for some use cases, in the real world, if the key is not properly protected, anybody who can gain access to it,  gains the capability to inject false data either on the ESP8266 or on the NodeJs Server, rendering in fact, the encryption effort useless.

The solution to not having a pre-shared key but since AES128 (and some other algorithms) require shared symmetric keys, we need to somehow generate a pre-shared key on demand that is not stored anywhere. But how to do that? This is a common actual problem on standard protocols such as SSL and HTTPS, and to solve this problem is where the Diffie-Hellman Key exchange/agreement protocol comes to help.

DH (Diffie-Hellman for short) works by creating at each peer that needs to communicate a set of two keys: one that is private, and one that is public. The peers exchange their public keys, and due to some mathematical properties they can calculate a common shared key using their own private key and the others public key. The key point here is that the shared key is generated without being transmitted between peers, which ensures that it is impossible to intercept it at transit. A possible attacker can see the public keys transmission, but without access to the private keys it can’t calculate the shared key.

We can just generate the shared key at boot up and keep using it until a reboot or restart, or generate a new key for each new transaction, generating in fact what is called ephemeral symmetric keys.
Since the code that will be shown bellow is just a proof of concept to show how it works, there isn’t the concept of session and so the NodeJs Server will just accept one key and peer at a time.

Anyway the DH key agreement protocol can be used at least in two different ways: by using the standard original key pairs based on the multiple groups of integers module N or the more recent and using shorter key lengths based on Elliptic Curve Cryptography, more specifically using the Curve25519 designed for DH Key exchanges for generating the necessary key pairs.

So let’s implement simple prototype that uses a single ESP8266 Wemos D1 based board that will send data to a NodeJs Server using AES128 based encryption, but this time, using as a key an ephemeral AES keys, not pre-shared keys.

NodeJS proof of concept:
From the NodeJS side we need to install a supporting module for Curve25519 DH, which is the Curve25519-N module. I’ve previously had trouble using this module, so just make sure that the used node version v13.13, where at least it compiles and works as expected.

The Curve25519 module provides the necessary functions to generate the private/public key pair and the shared key. The private/public key pair is generated from a random 32 byte secret used as a seed using the module functions. The sharedkey functions based on the given own private key and the peer’s public key can then calculate the shared key common to both peers.

A simple proof of concept is as following:

npm init
npm i curve25519-n --save

and the code testDH.js is:

// NodeJs simple DH key exchange using Ecliptic curves with the Curve25519
//
const curve = require('curve25519-n');

// Generate random 32 bytes secret
function randomSecret() {
   var result           = '';
   var characters       = 'ABCDEFGHIJKLMNOPQRSTUVXZabcdefghijklmnopqrstuvxz0123456789';
   var charactersLength = characters.length;
   for ( var i = 0; i < 32; i++ ) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
   }
   //console.log( result );
   return result;
}
// Generate the cryptographic material
var aliceServerSecret = Buffer.from( randomSecret() );

var alicePrivateKey = curve.makeSecretKey( aliceServerSecret );
var alicePublicKey  = curve.derivePublicKey( alicePrivateKey );

var bobServerSecret = Buffer.from( randomSecret() );

var bobPrivateKey = curve.makeSecretKey( bobServerSecret );
var bobPublicKey  = curve.derivePublicKey( bobPrivateKey );

var alice_shkey = curve.deriveSharedSecret(  alicePrivateKey , bobPublicKey );
var bob_shkey   = curve.deriveSharedSecret(  bobPrivateKey , alicePublicKey );

console.log("Alice public key: " ,  Buffer.from( alicePublicKey).toString('hex') );
console.log("Bob public key:   " ,  Buffer.from( bobPublicKey).toString('hex') );
console.log("");
console.log("------ Calculated shared keys: ");
console.log("Alice shared key: ", Buffer.from(alice_shkey).toString('hex') );
console.log("Bob shared key:   ",  Buffer.from(bob_shkey).toString('hex') );

Running this will held:

node testDH.js 

Alice public key:  64ec19b47ae105ca00b7e7e088fd2c809e93118fb961d33a118c95e2ee3a9d19
Bob public key:    98d9c8d93fceed09efb15d7629d449d66892ecb4bb4a16a486b1d656a2a1501d

------ Calculated shared keys: 
Alice shared key:  6692e8240a64b595698ef98440e89affbe5102082595631ebdf472897d432c2a
Bob shared key:    6692e8240a64b595698ef98440e89affbe5102082595631ebdf472897d432c2a

and lo and behold the shared keys are the same.

Now we just need to make Alice key the NodeJS key, and Bob’s public key the ESP8266/ESP32 key.

The ESP8266/ESP32 side:
On the ESP8266/ESP32 side we also have with the Arduino framework the Curve25519 ECDH functions for an Ecliptic Curve based Dilfie-Hellman key exchange.
As usual, using Platformio we need to add the Crypto library that does support ECDH Curve25519 based DH, and also AES128.

So on the ESP side, we generate again a set of public/private key pairs, and send the public key to the NodeJS server. As a response we receive the NodeJS server public key, and then we can calculate the shared key:

void    generateKeys() {
    Curve25519::dh1( m_publicKey, m_privateKey);
}

void    initSession() {
    // We contact the NodeJS server to send our Public key
    // and as a response we receive the Nodejs Public key
    generateKeys();                 // Generate a set of Curve25519 key pair for the DH key Agreement protocol

    // The Server end-point
    String url = "http://" + NODEServer_Address + ":" + NODEServer_Port + "/getSession";

    char    s_pubkey[65];
    Bytes2Str( s_pubkey, m_publicKey, KEY_SIZE );

    // Build the post body
    String postBody = "{\"pubkey\": \"" + String(s_pubkey) +"\"}";

    // Send the request
    http.begin( url );
    http.addHeader("content-type", "application/json");

    int httpCode = http.POST( postBody );
    if (httpCode > 0) {

        String payload = http.getString();
    
        if ( httpCode == 200 ) {            
            deserializeJson( jsonDoc, payload.c_str() );

            // Obtain the foreign public key
            const char *pubkey = jsonDoc["pubkey"];
            if ( pubkey != NULL) {
                Str2Bytes(m_fpublickey, (char *)pubkey, 64 );
                printHex( "Foreign Key: ", m_fpublickey , 32 );

                // Calculate now the shared key
                Curve25519::dh2( m_fpublickey, m_privateKey ); 
                printHex ( "Shared Key", m_fpublickey , 32 );     
                memcpy( m_shkey, m_fpublickey, 32 );
            }               
        }
        else {
            Serial.println("Error on HTTP request a session.");
        }
    }
    http.end();
}

The Crypto library for the Curve25519 offers two functions: Curve25519::dh1 for generating the keys pair where the public key is generated to be sent to the peer and Curve25519::dh2 function that given the private key and foreign public key, generates the shared key.

At the end, hopefully both sides end up with the same shared key, which they do, and from there we can use that key as the AES128 symmetric key to establish communications.

The resulting shared key has more bits than the necessary for the AES128 encryption/decryption, so we derive the AES128 symmetric key from the shared key. This can be done in several ways, but I just took the easier way and only used the necessary first 16 bytes of the pre-shared key to get the AES128 key. Other approaches are to take a SHA256 or SHA512 from the key to generate any missing bits if necessary.

We also can see that on the initSession() function we generate a new set of keys for each transmission so making all used keys ephemeral since they are only used once. The drawback is that for sending data we need two transactions, one for the key exchange and other for the data transmission itself.

Testing:
The testing code that shows this ECDH (Elliptic Curve DH key agreement working) is in this repository: https://github.com/fcgdam/AESCrypto_ECDH_KeyExchange.

As usual we use Platformio to flash the firmware on the ESP8266, and to run the NodeJs server, just run npm install and node server.js. Just make sure that on the ESP8266 the SSID, Password and node server IP address are correctly set.

Running we can see on the ESP side the AES128 key to be used, and compare it with the key that was generated on the NodeJS server side.

Foreign Key: :
C8 C9 74 6E BE E9 F3 63 33 46 39 A7 4C CC 88 AB 17 14 47 3F D8 10 E0 B9 4D 9C 5B BF 3A A3 30 02 
Shared Key:
4B 40 3A A1 E2 6E 56 3C B2 5B 15 3A A6 24 6F 77 D2 C5 D5 0D 96 17 73 90 09 3A B6 38 0F C4 70 40 
AES128 key to be used: :
4B 40 3A A1 E2 6E 56 3C B2 5B 15 3A A6 24 6F 77 

IV B64: wB2astutocBMfv+xsTvAKg==
------- Sending data:
 Data: wirA/v+JcsjnP9dAVml0W/20apkQqFnY4jYMrRnw9tM=

Foreign Key: :
C8 C9 74 6E BE E9 F3 63 33 46 39 A7 4C CC 88 AB 17 14 47 3F D8 10 E0 B9 4D 9C 5B BF 3A A3 30 02 
Shared Key:
E2 4A F6 17 F3 3F B5 79 3F 6F B4 B7 8A D9 5B 5C A9 6D 65 FF 88 F3 2C 9A 18 99 99 6B B0 0F C1 4A 
AES128 key to be used: :
E2 4A F6 17 F3 3F B5 79 3F 6F B4 B7 8A D9 5B 5C 

IV B64: VW1Lnm21M1UFE45E80eNfw==
------- Sending data:
 Data: V+FdoIzYORrKiA3DjyRn9CPdYREqaQWZf8fatKFFWY0=

The associated output on the server side. Note that the calculated shared key is the same, hence we can decrypt the messages without any problems.

POST /setdata 200 0.309 ms - 37
Foreign Public Key:  D9A799D46919A2B257E112678635D7061AB589B61C42714C7B7216315AAC961B
Shared key:  4b403aa1e26e563cb25b153aa6246f77d2c5d50d96177390093ab6380fc47040
AES128 key to be used:  4b403aa1e26e563cb25b153aa6246f77
POST /getSession 200 0.421 ms - 77
Data request:  {
  iv: 'wB2astutocBMfv+xsTvAKg==',
  data: 'wirA/v+JcsjnP9dAVml0W/20apkQqFnY4jYMrRnw9tM='
}
Decrypted message:  {"testdata": "346"}
POST /setdata 200 0.296 ms - 37
Foreign Public Key:  74B73F83D4E1FFD587B0A1E14C5546CEF3EEA50E517B2ED94E64BD585C278B2A
Shared key:  e24af617f33fb5793f6fb4b78ad95b5ca96d65ff88f32c9a1899996bb00fc14a
AES128 key to be used:  e24af617f33fb5793f6fb4b78ad95b5c
POST /getSession 200 0.422 ms - 77
Data request:  {
  iv: 'VW1Lnm21M1UFE45E80eNfw==',
  data: 'V+FdoIzYORrKiA3DjyRn9CPdYREqaQWZf8fatKFFWY0='
}
Decrypted message:  {"testdata": "347"}

Conclusion:
This example shows that there is no need to preset keys on the ESP8266 device to be able to encrypt data as long that both the device and the server agree on the process for the generating the necessary key(s). Of course the server must support different sets of keys for different devices, which is not the case of the provided example, it’s just proof of concept.
Also another key element is to know who is doing the key agreement (ensuring device identity) since the above code accepts anyone to do the key agreement, which is another issue in itself.

One thought on “Establishing secure ESP8266 and NodeJs communication by using Diffie-Hellman key exchange and Elliptic Curves

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.