Suppose you have 2 nodes, where one (the Gateway) is sending a wireless message with a command to another (the Garage Door node) to open the Garage Door.
If someone would use a simple SDR, he could record the sent command and later on resend it and open the Garage Door.
It wouldn't matter if the transaction is encrypted or not, because in the end, what was recorded is already the encrypted wireless command and that is all it takes to for the receiving Garage Door Node to diligently open the Garage Door.
To avoid this, several methods may be implemented. One of the most common is the "rolling code" method, like the one used by the normal garage doors keyfobs. But even this if quite easy to bypass. The method is "very elegantly", as Felix put it, described here: http://spencerwhyte.blogspot.com/2014/03/delay-attack-jam-intercept-and-replay.html (http://spencerwhyte.blogspot.com/2014/03/delay-attack-jam-intercept-and-replay.html).
One other method is to use "Authentication". The idea is very simple and quite easy to implement on the Moteino. Let's use the same example mentioned before:
Imagine that the Gateway wants to send a message to the Garage Door Node with the command "OPEN GARAGE DOOR".
1. First, the Gateway will ask the Garage Door Node for a PUBLIC KEY, an authentication token also known as "salt".
2. The Garage Door node receives this request for the PUBLIC KEY and creates one, on the fly, consisting of 16 random alphanumeric characters (like this, #d$g(/aW"#g;=)%f, for instance) and sends it back to the request originator, waiting up to 200ms for an authenticated command to come trough.
3. The Gateway receives the one time only PUBLIC KEY, concatenates it with the shared common PRIVATE KEY (the same key is available to all the network nodes, &E$ft&75)jb:<Di= for instance) and passes the resulting 32 characters string (#d$g(/aW"#g;=)%f&E$ft&75)jb:<Di=) trough the MD5 library to create a 32 bytes Hexadecimal digested hash (40769061cc0d0e6a0923b0a9f1a01d6 in this case).
Now the Gateway send this MD5 HEX digested hash (the authentication token), along with the required command to the Garage Door node.
4. The Garage Door node receives the MD5 HEX digested hash (the authentication token) created by the Gateway and using the same PUBLIC and PRIVATE KEY, produces its own MD5 HEX digested hash (the same authentication token) using the same library and compares it with the one received. They should match, because they were created using the same keys on both sides.
If they match, the command "OPEN THE GARAGE DOOR" is executed. If not, it will wait until the 200ms expires for a correct authentication to come over and after that, that PUBLIC KEY is gone. Forever.
No other node from outside this network and without the AES128 Encryption key can request an authentication token.
ABOUT the MD5 library: the MD5 library "cooks" the concatenation of the PUBLIC and PRIVATE KEYs in order to produce the 32 heaxadecimal digested hash code that will be used for the authentication of the transaction.
The size of the digested hash is always 32 bytes, no matter what the public and private keys sizes are. For instance, the following string: The quick brown fox jumps over the lazy dog will produce the following digested hash: 9e107d9d372bb6826bd81d3542a419d6. By changing a single letter (from dog to doc), the result will become: f1890d29460f7a47955dec4dcbf378e9, which is a dramatic change for such a small modification. Even a null string "" will produce: d41d8cd98f00b204e9800998ecf8427e. You can try a MD5 hash generator here: https://www.miraclesalad.com/webtools/md5.php (https://www.miraclesalad.com/webtools/md5.php).
(http://linhadafrente.net/Moteino/authentication.jpg)
Now imagine that the same command is used to open and close the Garage Door. You give the command to close the garage door and someone jammed its reception while recording it, hoping to open the door later on. So the genuine command didn't came trough and the door didn't close. So you repeat the "CLOSE GARAGE DOOR command" and start over the authentication process by sending a request for a new PUBLIC KEY, thus repeating the whole process again.
If someone tries to use the recorded transaction, the PUBLIC KEY is no longer valid and so the authentication will fail.
I'm posting two different codes, one for the Gateway Node and one for the Garage Door Node, along with the MD5 library for the proof of concept to be reproduced, if required.
In this code, the Gateway restarts the process to request for the garage door to open every 5 seconds.
[DISCLAIMER] I'm a copy/paste coder and you should use this concept at your own risk[/DISCLAIMER]
The code is ready to be loaded in both nodes (Moteinos) and trough the serial monitor you can see the transactions taking place. The onboard LED will blink on the Gateway the moment it request as PUBLIC KEY and on the Garage Door Node when the command is executed, so that you can see how fast all this authentication method is. It takes the Gateway about 60ms from the time it requests the PUBLIC KEY until the command with the authentication token is sent and it takes the Garage Door node about 47ms since it first receives a request for a PUBLIC KEY until the "OPEN GARRAGE DOOR" command is executed.
the MD5 Library can be found here: https://github.com/tzikis/ArduinoMD5
Gateway Moteino code:
//*****************************
// Homeseer MD5 Auth Moteino Gateway
// Ver. 1.1
// Node 20
//*****************************
#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 "EQ56&%/#$_)&/K?_"
//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 hash;
bool tokenReceived = 0;
char md5[33];
//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
char in_data3[17]; // Data 3 - Received Authentication Token (PUBLIC KEY)
}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 - A 3 is a request for a Authenthication Token (PUBLIC KEY); a 1 is designating output 1 (the garage door relay) as destination for the command
uint32_t out_data2; // Data 2 - Command to be sent: a 1 means OPEN the DOOR
char out_data3[33]; // Data 3 - MD5 digested hash used as Authenticator
}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 ON or OFF according to what in the SPY 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
{
Blink(50);
requestToken();
}
if (tokenReceived == 1)
{
Serial.println("OK");
outData.out_data0 = radio.SENDERID;
outData.out_data1 = 1;
outData.out_data2 = 1;
strcpy(outData.out_data3, md5);
sendData(2);//Send command with digested MD5 Hash
tokenReceived = 0; //Reset
}
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())
{
Serial.println("Data received with an [ACK] requested...");
radio.sendACK();
Serial.println("[ACK] sent!");
}
if (inData.in_data1 == 3) //If the data is a Authentication Token
{
tokenReceived = 1;
Serial.print("The new Auth Token received is: [");
Serial.print(inData.in_data3);
Serial.println("]");
//Now create a new 32 bytes MD5 HEX DIGESTED HASH
createDigestedHash(inData.in_data3);
}
}
}
void requestToken()
{
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 new Auth Token...");
sendData(1);//Request a authentication token
}
void createDigestedHash(char authToken[17])
{
char authSaltedToken[33] = "";
//Now add the Auth Token to our PRIVATE KEY and create a string from it
strcat(authSaltedToken, authToken);
strcat(authSaltedToken, PRIVATEKEY);
Serial.print("Concatenation of Salt + PRIVATEKEY: [");
Serial.print(authSaltedToken);
Serial.println("]");
//Now generate the MD5 hash from our string
unsigned char* hash = MD5::make_hash(authSaltedToken);
char *md5str = MD5::make_digest(hash, 16);
strcpy(md5, md5str);
free(hash);
free(md5str);
Serial.print("DIGESTED HASH: ");
Serial.println(md5);
}
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");
Serial.print("Elapsed Time since frist requesting an Auth Token and sending the command:");
Serial.println(millis() - timeNow);
}
}
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 Door Node code:
//********************************************
// Homeseer MD5 Auth Moteino Garage Door Node
// Ver. 1.1
// Node 21
//********************************************
#include <RFM69.h>
#include <RFM69_ATC.h>
#include <SPIFlash.h>
#include <MD5.h> //https://github.com/tzikis/ArduinoMD5
#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 "EQ56&%/#$_)&/K_?"
#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;
char authToken[17];
char md5[32];
//Inbound Data Struct
typedef struct
{
uint8_t in_data0; // Data 0 - Incoming Node ID
uint8_t in_data1; // Data 1 - A 3 is a incoming request fot a Authentication token (PUBLIC KEY); a 1 naming the destination of the command (DOOR RELAY)
uint32_t in_data2; // Data 2 - NIL (just a place holder) during a Authentication Token request; 1 for OPEN DOOR command
char in_data3[33]; // Data 3 - MD5 Digested hash Authenticator received when command received
}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 - a 3 means this data is the response for a Authentication Token request
uint32_t out_data2; // Data 2 - NIL (Just a place holder)
char out_data3[17]; // Data 3 - The Authentication Token sent (PUBLIC KEY)
}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 ON or OFF according to what in the SPY 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())
{
Serial.println("Data received with an [ACK] requested...");
radio.sendACK();
Serial.println("[ACK] sent!");
}
// If the Auth Token was requested
if (inData.in_data1 == 3)
{
Serial.println("Incoming data is a request for a Auth Token.");
timeNow = millis();
createNewAuthToken(); //Generate a new 16 char Auth Token
sendAuthToken(); //Send new Auth Token to the requesting node by radio
}
else if (inData.in_data1 == 1 && millis() - timeNow < TIMEOUT)
{
//Create a new 32 bytes MD5 HEX DIGESTED HASH to compare with the received one and store in in md5
Serial.print("Elapsed time for Command with Authentication received since the request of the Auth Token:");
Serial.println(millis() - timeNow);// Show how much time passed sinced the requst for a token until a command was received
createDigestedHash();
if (strcmp(inData.in_data3, md5) == 0)// See if the locally Digested Hash is equal to the remote Digested Hash received
{
Serial.println("Command received!");
Blink(50);
Serial.println();
}
}
else if (inData.in_data1 == 1 && millis() - timeNow >= TIMEOUT)
{
Serial.println("Auth Token Expired!");
}
}
}
void createNewAuthToken()
{
randomSeed(analogRead(A0));
for (int i = 0; i < (sizeof(authToken) - 1); i++)
{
authToken[i] = random(32, 126);
}
authToken[strlen(authToken)] = NULL;
Serial.print("The new Auth Token generated is: [");
Serial.print(authToken);
Serial.println("]");
}
void sendAuthToken()
{
//Sending the new Auth Token back to the requesting node
outData.out_data0 = radio.SENDERID; //The node ID of the Auth Token requesting node
outData.out_data1 = 3; // A 3 means that it is a Auth Token
outData.out_data2 = 0; // Just a place holder
strcpy(outData.out_data3, authToken); //The new Auth Token to be sent
Serial.print("Sending Auth Token to node [");
Serial.print(inData.in_data0);
Serial.println("]");
sendData(1); // Send the new Auth Token trough the radio
}
void createDigestedHash()
{
char authSaltedToken[33] = "";
//Now add the Auth Token to our PRIVATE KEY and create a string from it
strcat(authSaltedToken, authToken);
strcat(authSaltedToken, PRIVATEKEY);
Serial.print("Concatenation of Salt + PRIVATEKEY: [");
Serial.print(authSaltedToken);
Serial.println("]");
//Now generate the MD5 hash from our string
unsigned char* hash = MD5::make_hash(authSaltedToken);
char *md5str = MD5::make_digest(hash, 16);
strcpy(md5, md5str);
free(hash);
free(md5str);
Serial.print("DIGESTED HASH:");
Serial.println(md5);
}
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("Auth Token sent and ACK received");
}
else if (type == 2)
{
//Send something else
}
}
outData.out_data0 = 0;
outData.out_data1 = 0;
outData.out_data2 = 0;
strcpy(outData.out_data3, 0);
}
void Blink(int DELAY_MS)//The led blinking function
{
digitalWrite(LED_BUILTIN, HIGH);
delay(DELAY_MS);
digitalWrite(LED_BUILTIN, LOW);
}
Please feel free to suggest any improvements for the code.
A quick video showing the timing between an Authentication Token is requested and the command is executed: