So I spent some time studying this subject, and I went on and instead of just sending a
if (struct.magicToken ==
Averyspecialstringherethattellsmethisisreallyforthegarageandnotsomethingelselikefakedata") { all good, open my garage! }
I decided to implement an Authentication method, using a MD5 HMAC, or keyed-hash message authentication code.
Mind you that I'm a copy/paste programmer, so take this with a bit of salt (pun intended
).
The idea is simple:
I have two nodes, one is the Gateway and the other is a node that will control something (like the garage, for instance).
So in order to make sure that the garage node only receives a command from the gateway and from nothing else, the gateway has to be authenticated.
To do this, I assigned a common password (or key, if you wish), that is considered private. This key is known by all the nodes. It is a private key. (Not the encryption type of key, but a key, anyway).
Now imagine that the Gateway wants to send a "OPEN THE GARAGE DOOR" to the garage door node.
First it will request a public key from the garage node, one that can be captured by someone listening to this conversation. But this key is nothing more the the "salt" that will scramble the private key when it is sent over the radio and on its own it is useless.
So the Gateway requests a public key to the garage node.
The garage node will fabricate a random number, of 9 digits in this case, using the millis() as a seed to assure the randomness of this number, and sends it to the gateway.
The gateway receives this public key and adds it to the private key, another 9 digit fixed number, and creates a MD5 Digested Hash, a large hexadecimal number, that is always the same size, no matter the dimension of the numbers used to generate it. This MD5 Hash is created by a very simple library created by TZIKIS (
https://github.com/tzikis/ArduinoMD5, trough two commands: one to create the Hash, and the other to create the Digested Hash that will be in a HEX format.
Now the Gateway sends this Digested Hash back to the garage node along with the intended command of "OPEN THE GARAGE DOOR".
The node receives this command and the Digested Hash and does the same process on its side: adds the private key with the public key it has already created and runs this trough the library to create the same Digested Hash, which will be compared with the received one.
If they match, the gateway is authenticated and the command is executed. If not, the node will wait a total of 200ms since it created the public key to receive the correct digested hash. After that that public key is expired and a new one must be requested.
All this takes about 30 to 50 ms.
If someone would receive the public key, not knowing what the private key is, the authentication digested hash cannot be recreated. And if the authenticated digested hash sent by the gateway is captured, it is no longer valid, since the node will only use it once.
So, I think this is a great step in the direction of increasing the security of this data transactions, beyond the AES128 encryption that the radios are already capable of.
I read that MD5 is not very secure compared with SHA256, for instance, but its a first step. I'm starting to look a solution for SHA256, but I'm not sure if the time it takes to create the hash will make it feasible.
Anyway, I leave the codes I use on both nodes here for your appreciation and comment.
In the Gateway Node, the code sends a request to the Garage Node every 5 seconds, for this test.
Below is a screen capture of this transactions.
Gateway node
//*****************************
// Homeseer Moteino Gateway
// Ver. 2G 1.0
// Node 20
//*****************************
#include <RFM69.h>
#include <RFM69_ATC.h>
#include <SPIFlash.h>
#include <MD5.h>
#define THISNODEID 20 //unique for each node on same network
#define NETWORKID 100 //the same on all nodes that talk to each other
#define FREQUENCY RF69_433MHZ //Match with the correct radio frequency
#define ENCRYPTKEY "xxxxxxxxxxxxxxxx" //exactly the same 16 characters/bytes on all nodes!
#define ATC_RSSI -80
#define ACK_TIME 500 // max # of ms to wait for an ack
#define SERIAL_BAUD 115200//Make sure the script comport_open1.txt has the same baud rate
#define FLASH_SS 8 // and FLASH SS on D8
#define SPY false
#define PRIVATEKEY 189578193
//Define Hardware
SPIFlash flash(FLASH_SS, 0xEF30); //EF30 for windbond 4mbit flash
RFM69_ATC radio;
unsigned long last_millis = 0;
unsigned long timeNow = 0;
uint32_t authToken = 0;
uint32_t hash;
String authTokenSt;
char buf[10];
//Inbound Data Struct
typedef struct
{
uint8_t in_data0; // Data 0 - Incoming Node ID
uint8_t in_data1; // Data 1 - 3: Auth token incoming
uint32_t in_data2; // Data 2 - NIL
uint32_t in_data3; // Data 3 - Security Token
}in_Payload;
in_Payload inData;
//Outboud Data Struct
typedef struct
{
uint8_t out_data0; // Data 0 - Destination Node ID
uint8_t out_data1; // Data 1 - 3: Request a Auth Token
uint32_t out_data2; // Data 2 - NIL
char out_data3[33]; // Data 3 - Security Token
}out_Payload;
out_Payload outData;
void setup()
{
//Initialize the Serial port at the specified baud rate
Serial.begin(SERIAL_BAUD);
pinMode(LED_BUILTIN, OUTPUT);
//Initialize the radio
radio.initialize(FREQUENCY, THISNODEID, NETWORKID);//Initialize the radio
radio.setHighPower();//Set the radio to high power
radio.encrypt(ENCRYPTKEY);//Turn the radio Encryption ON
radio.spyMode(SPY);//Turn the radio Promiscuous mode according to what in the promiscuousMode variable
radio.enableAutoPower(ATC_RSSI);
//Initialize FLASH
flash.initialize();
Serial.println("Gateway radio receiving in 433Mhz Mhz...");
Serial.println();
}
void loop()
{
//Every 5 seconds send
if (millis() > last_millis + 5000) //Time to send a new open/close instructions
{
last_millis = millis();
//Struct components to request the Salt
outData.out_data0 = 21;
outData.out_data1 = 3;
outData.out_data2 = 0;
strcpy(outData.out_data3, "");
timeNow = millis(); //Record the time to calculate how much time took the transaction
Serial.println();
Serial.println("Requesting a Auth Token...");
sendData(1);//Request a authentication token
}
if (radio.receiveDone())//If some packet was received by the radio, wait for all its contents to come trough
{
inData = *(in_Payload*)radio.DATA; //assume radio.DATA actually contains our struct and not something else
//If an ACK (acknowledge) was requested by the data packet transmitter radio, send the ACK back to it and display "ACK sent" on the serial monitor
if (radio.ACKRequested())
{
radio.sendACK();
Serial.println("[ACK sent!]");
}
if (inData.in_data1 == 3) //If the data is a Authentication Token
{
authToken = inData.in_data3 + PRIVATEKEY;
authTokenSt = String (authToken);
authTokenSt.toCharArray(buf, 11);
//generate the MD5 hash for our string
unsigned char* hash = MD5::make_hash(buf);
char *md5str = MD5::make_digest(hash, 16);
free(hash);
Serial.print("Auth Token received:");
Serial.println(inData.in_data3);
Serial.print("Elapsed Time:");
Serial.println(millis()-timeNow);// Display how much time it took for the Token to be available
Serial.print("DIGESTED HASH:");
Serial.println(md5str);
outData.out_data0 = radio.SENDERID;
outData.out_data1 = 1;
outData.out_data2 = 1;
strcpy(outData.out_data3, md5str);
free(md5str);
sendData(2);
Blink(50);
}
}
}
void sendData(int type)
{
if (radio.sendWithRetry(outData.out_data0, (const void*)(&outData), sizeof(outData), 5, ACK_TIME))//Send the data to the destinationNode via radio and wait for the ACK to be received
{
if (type == 1)
{
Serial.println("Token request sent and ACK received");
}
else if (type == 2)
{
Serial.println("Command sent and ACK received");
}
}
outData.out_data0 = 0;
outData.out_data1 = 0;
outData.out_data2 = 0;
strcpy(outData.out_data3, "");
}
void Blink(int DELAY_MS)//The led blinking function
{
digitalWrite(LED_BUILTIN, HIGH);
delay(DELAY_MS);
digitalWrite(LED_BUILTIN, LOW);
}
Garage node:
//*****************************
// Homeseer Moteino Gateway
// Ver. 2G 1.0
// Node 21
//*****************************
#include <RFM69.h>
#include <RFM69_ATC.h>
#include <SPIFlash.h>
#include <MD5.h>
#define THISNODEID 21 //unique for each node on same network
#define NETWORKID 100 //the same on all nodes that talk to each other
#define FREQUENCY RF69_433MHZ //Match with the correct radio frequency
#define ENCRYPTKEY "xxxxxxxxxxxxxxxx" //exactly the same 16 characters/bytes on all nodes!
#define ATC_RSSI -80
#define ACK_TIME 500 // max # of ms to wait for an ack
#define SERIAL_BAUD 115200//Make sure the script comport_open1.txt has the same baud rate
#define FLASH_SS 8 // and FLASH SS on D8
#define SPY false
#define PRIVATEKEY 189578193
#define TIMEOUT 200 //Auth Token timeout time in ms
//Define Hardware
SPIFlash flash(FLASH_SS, 0xEF30); //EF30 for windbond 4mbit flash
RFM69_ATC radio;
unsigned long last_millis = 0;
unsigned long int timeNow = 0;
uint32_t authToken = 0;
bool authTokenSent = false;
String authTokenSt;
char buf[10];
//Inbound Data Struct
typedef struct
{
uint8_t in_data0; // Data 0 - Incoming Node ID
uint8_t in_data1; // Data 1 - 3: Auth token incoming
uint32_t in_data2; // Data 2 - Auth Token
char in_data3[33]; // Data 3 - Security Token
}in_Payload;
in_Payload inData;
//Outboud Data Struct
typedef struct
{
uint8_t out_data0; // Data 0 - Sender Node ID
uint8_t out_data1; // Data 1 - 3: Request a Auth Token
uint32_t out_data2; // Data 2 - NIL
uint32_t out_data3; // Data 3 - Security Token
}out_Payload;
out_Payload outData;
void setup()
{
//Initialize the Serial port at the specified baud rate
Serial.begin(SERIAL_BAUD);
pinMode(LED_BUILTIN, OUTPUT);
//Initialize the radio
radio.initialize(FREQUENCY, THISNODEID, NETWORKID);//Initialize the radio
radio.setHighPower();//Set the radio to high power
radio.encrypt(ENCRYPTKEY);//Turn the radio Encryption ON
radio.spyMode(SPY);//Turn the radio Promiscuous mode according to what in the promiscuousMode variable
radio.enableAutoPower(ATC_RSSI);
//Initialize FLASH
flash.initialize();
Serial.println("Garage Node receiving in 433Mhz Mhz...");
Serial.println();
}
void loop()
{
if (radio.receiveDone())//If some packet was received by the radio, wait for all its contents to come trough
{
inData = *(in_Payload*)radio.DATA; //assume radio.DATA actually contains our struct and not something else
//If an ACK (acknowledge) was requested by the data packet transmitter radio, send the ACK back to it and display "ACK sent" on the serial monitor
if (radio.ACKRequested())
{
radio.sendACK();
Serial.println("[ACK sent!]");
}
// If the Auth Token was requested
if (inData.in_data1 == 3)
{
randomSeed(millis());
authToken = random(999999999);
Serial.println();
Serial.println("New request for a Auth Token received.");
Serial.print("New Auth Token:");
Serial.println(authToken);
outData.out_data0 = radio.SENDERID;
outData.out_data1 = 3;
outData.out_data2 = 0;
outData.out_data3 = authToken;
timeNow = millis();
sendData();
}
else if (inData.in_data1 == 1 && millis() - timeNow < TIMEOUT)
{
authToken = authToken + PRIVATEKEY; //Add the Salt to the Auth Token
authTokenSt = String (authToken); //Make the Auth Token a String
authTokenSt.toCharArray(buf, 11); //Convert the String into an array of chars
//generate the MD5 hash for our string
unsigned char* hash = MD5::make_hash(buf);
char *md5str = MD5::make_digest(hash, 16);//Convert the hash into HEX
Serial.print("Generated DIGESTED HASH:");
Serial.println(inData.in_data3);
Serial.print("Received DIGESTED HASH:");
Serial.println(md5str);
free(hash);// Release the hash memory blocks
if (strcmp(inData.in_data3, md5str) == 0)// See if the locally Digested Hash is equal to the remote Digested Hash received
{
free(md5str);//Release the md5str memory block
Serial.println("Command received!");
Serial.print("Elapsed Time:");
Serial.println(millis() - timeNow);// Show how much time passed sinced the requst for a token until a command was received
Blink(50);
authToken = 0;//Reset the Auth Token
}
}
else if (inData.in_data0 == 1 && millis() - timeNow >= TIMEOUT)
{
Serial.println("Auth Token Expired!");
}
}
}
void sendData()
{
if (radio.sendWithRetry(outData.out_data0, (const void*)(&outData), sizeof(outData), 5, ACK_TIME))//Send the data to the destinationNode via radio and wait for the ACK to be received
{
Serial.println("Auth Token sent and ACK received");
}
outData.out_data0 = 0;
outData.out_data1 = 0;
outData.out_data2 = 0;
outData.out_data3 = 0;
}
void Blink(int DELAY_MS)//The led blinking function
{
digitalWrite(LED_BUILTIN, HIGH);
delay(DELAY_MS);
digitalWrite(LED_BUILTIN, LOW);
}