Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions examples/message-encryption/Node-RED_decryption_node
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Import crypto module from Setup tab.

const IV_LEN = 12;
const TAG_LEN = 16;
const PSK = Buffer.from([
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
]);

function aesGcmDecrypt(hexMsg) {
const MIN_LEN = (IV_LEN + TAG_LEN + 4) * 2;
if (typeof hexMsg !== 'string' || hexMsg.length < MIN_LEN || hexMsg.length % 2 !== 0)
throw new Error(`Invalid length: ${hexMsg?.length}`);

const raw = Buffer.from(hexMsg, 'hex');
const iv = raw.slice(0, IV_LEN);
const tag = raw.slice(IV_LEN, IV_LEN + TAG_LEN);
const ct = raw.slice(IV_LEN + TAG_LEN);

const decipher = crypto.createDecipheriv('aes-128-gcm', PSK, iv);
decipher.setAuthTag(tag);
const pt = Buffer.concat([decipher.update(ct), decipher.final()]); // throws if tampered

// Extract counter (first 4 bytes)
const counter = pt.readUInt32BE(0);
const message = pt.slice(4).toString('utf8');
return { counter, message };
}

try {
const topic = msg.topic || 'default';
const stateKey = `last_counter_${topic.replace(/\//g, '_')}`;
const lastSeen = flow.get(stateKey) || 0;

const { counter, message } = aesGcmDecrypt(msg.payload);

if (counter <= lastSeen) {
node.warn(`REPLAY DETECTED on ${topic}: counter ${counter} <= last seen ${lastSeen}`);
msg.payload = null;
msg.decrypted_ok = false;
msg.decrypt_error = `Replay attack: counter ${counter} <= ${lastSeen}`;
return msg;
}

flow.set(stateKey, counter); // accept and advance window

msg.payload = message;
msg.counter = counter;
msg.decrypted_ok = true;
return msg;

} catch (err) {
node.error(`Decrypt failed: ${err.message}`, msg);
msg.payload = null;
msg.decrypted_ok = false;
msg.decrypt_error = err.message;
return msg;
}
39 changes: 39 additions & 0 deletions examples/message-encryption/Node-RED_encryption_node
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Import crypto module from Setup tab.

const IV_LEN = 12;
// Secret Key
const PSK = Buffer.from([
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
]);

function aesGcmEncrypt(plaintext) {
const txCounter = (flow.get('nr_tx_counter') || 0) + 1;
flow.set('nr_tx_counter', txCounter);

const msgBuf = Buffer.from(plaintext, 'utf8');
const ctrBuf = Buffer.alloc(4);
ctrBuf.writeUInt32BE(txCounter, 0);
const pt = Buffer.concat([ctrBuf, msgBuf]);

const iv = crypto.randomBytes(IV_LEN);
const cipher = crypto.createCipheriv('aes-128-gcm', PSK, iv);
const ct = Buffer.concat([cipher.update(pt), cipher.final()]);
const tag = cipher.getAuthTag();

return { hex: Buffer.concat([iv, tag, ct]).toString('hex'), counter: txCounter };
}

try {
const { hex, counter } = aesGcmEncrypt(msg.payload);
msg.payload = hex;
msg.topic = msg.topic || 'button_node/cmd';
msg.counter = counter;
msg.encrypted_ok = true;
return msg;
} catch (err) {
node.error(`Encrypt failed: ${err.message}`, msg);
msg.payload = null;
msg.encrypted_ok = false;
return msg;
}
23 changes: 23 additions & 0 deletions examples/message-encryption/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Application-Layer Encryption Proof of Concept

This is a Proof of Concept application-layer encryption implementation.

The files have to be manually added to the build folder and Node-RED in order for this PoC to work. Currently no further integration with IoTempower is done, as the TLS is preferred for security.

This setup uses a counter saved to EEPROM to mitigate message replay attacks. An example of both sending and receiving an encrypted message is provided. The example uses a simple button node for sending the encrypted message and a LED node for receiving the messages. These nodes have ample documentation available for new users to explore so no further explanation is provided here.

### Files to be added to the build folder:

* Move `aes_helper.h` to `build/src/aes_helper.h`

### Modifications for existing files:

* Add the following line: `#include <aes_helper.h>` to `build/src/setup.cpp`
* Move:
* `receive/setup.h` to `build/src/setup.h` of the LED node
* `send/setup.h` to `build/src/setup.h` of the BUTTON node

### Files to be added to Node-RED (As the content of a function node):

* `Node-RED_decryption_node`
* `Node-RED_encryption_node`
186 changes: 186 additions & 0 deletions examples/message-encryption/aes_helper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#pragma once
#include <Arduino.h>
#include <EEPROM.h>
#include <bearssl/bearssl_block.h>
#include <bearssl/bearssl_aead.h>

static const uint8_t AES_PSK[16] = {
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
};

#define AES_GCM_IV_LEN 12
#define AES_GCM_TAG_LEN 16
#define EEPROM_MAGIC 0xAE
#define EEPROM_ADDR_MAGIC 0
#define EEPROM_ADDR_COUNTER 1
#define EEPROM_ADDR_RX 5

static void eeprom_init() {
EEPROM.begin(16);
if (EEPROM.read(EEPROM_ADDR_MAGIC) != EEPROM_MAGIC) {
// First boot — initialise counters to 0
EEPROM.write(EEPROM_ADDR_MAGIC, EEPROM_MAGIC);
EEPROM.put(EEPROM_ADDR_COUNTER, (uint32_t)0);
EEPROM.put(EEPROM_ADDR_RX, (uint32_t)0);
EEPROM.commit();
ulog("EEPROM initialised with fresh counters");
}
}

static uint32_t eeprom_read_tx_counter() {
uint32_t val;
EEPROM.get(EEPROM_ADDR_COUNTER, val);
return val;
}

static void eeprom_write_tx_counter(uint32_t val) {
EEPROM.put(EEPROM_ADDR_COUNTER, val);
EEPROM.commit();
}

static uint32_t eeprom_read_rx_counter() {
uint32_t val;
EEPROM.get(EEPROM_ADDR_RX, val);
return val;
}

static void eeprom_write_rx_counter(uint32_t val) {
EEPROM.put(EEPROM_ADDR_RX, val);
EEPROM.commit();
}

static void aes_random_bytes(uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++) buf[i] = (uint8_t)(RANDOM_REG32 & 0xFF);
}

static String bytes_to_hex(const uint8_t* buf, size_t len) {
String out; out.reserve(len * 2);
const char* h = "0123456789abcdef";
for (size_t i = 0; i < len; i++) { out += h[(buf[i]>>4)&0xF]; out += h[buf[i]&0xF]; }
return out;
}

static bool hex_to_bytes(const String& hex, uint8_t* out, size_t expected_len) {
if (hex.length() != expected_len * 2) return false;
auto fh = [](char c) -> int {
if (c>='0'&&c<='9') return c-'0';
if (c>='a'&&c<='f') return c-'a'+10;
if (c>='A'&&c<='F') return c-'A'+10;
return -1;
};
for (size_t i = 0; i < expected_len; i++) {
int hi = fh(hex[i*2]), lo = fh(hex[i*2+1]);
if (hi<0||lo<0) return false;
out[i] = (uint8_t)((hi<<4)|lo);
}
return true;
}


// ENCRYPT


static String aes_publish_secure(const String& plaintext) {
// 1. Read and increment TX counter
uint32_t counter = eeprom_read_tx_counter() + 1;
eeprom_write_tx_counter(counter);

// 2. Build plaintext: 4-byte big-endian counter + message
size_t msglen = plaintext.length();
size_t ptlen = 4 + msglen;
uint8_t* pt = (uint8_t*)malloc(ptlen);
if (!pt) return "ERR_ALLOC";

pt[0] = (counter >> 24) & 0xFF;
pt[1] = (counter >> 16) & 0xFF;
pt[2] = (counter >> 8) & 0xFF;
pt[3] = counter & 0xFF;
memcpy(pt + 4, plaintext.c_str(), msglen);

// 3. Generate random IV
uint8_t iv[AES_GCM_IV_LEN];
aes_random_bytes(iv, AES_GCM_IV_LEN);

// 4. AES-128-GCM encrypt in-place
uint8_t tag[AES_GCM_TAG_LEN];
br_gcm_context gcm;
br_aes_small_ctr_keys aes_ctx;
br_aes_small_ctr_init(&aes_ctx, AES_PSK, 16);
br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcm, iv, AES_GCM_IV_LEN);
br_gcm_flip(&gcm);
br_gcm_run(&gcm, 1, pt, ptlen);
br_gcm_get_tag(&gcm, tag);

String result = bytes_to_hex(iv, AES_GCM_IV_LEN)
+ bytes_to_hex(tag, AES_GCM_TAG_LEN)
+ bytes_to_hex(pt, ptlen);
free(pt);

ulog("TX counter: %u", counter);
return result;
}


// DECRYPT


static String aes_receive_secure(const String& hex_msg) {
const size_t MIN_HEX = (AES_GCM_IV_LEN + AES_GCM_TAG_LEN + 4) * 2;
if (hex_msg.length() < MIN_HEX || hex_msg.length() % 2 != 0) {
ulog("AES-GCM RX: invalid length");
return "";
}

size_t ct_len = (hex_msg.length() / 2) - AES_GCM_IV_LEN - AES_GCM_TAG_LEN;

uint8_t iv[AES_GCM_IV_LEN], tag[AES_GCM_TAG_LEN];
uint8_t* ct = (uint8_t*)malloc(ct_len);
if (!ct) return "";

bool ok = hex_to_bytes(hex_msg.substring(0, AES_GCM_IV_LEN * 2), iv, AES_GCM_IV_LEN)
&& hex_to_bytes(hex_msg.substring(AES_GCM_IV_LEN * 2,
(AES_GCM_IV_LEN + AES_GCM_TAG_LEN) * 2), tag, AES_GCM_TAG_LEN)
&& hex_to_bytes(hex_msg.substring((AES_GCM_IV_LEN + AES_GCM_TAG_LEN) * 2), ct, ct_len);

if (!ok) { free(ct); ulog("AES-GCM RX: hex decode failed"); return ""; }

br_gcm_context gcm;
br_aes_small_ctr_keys aes_ctx;
br_aes_small_ctr_init(&aes_ctx, AES_PSK, 16);
br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul32);
br_gcm_reset(&gcm, iv, AES_GCM_IV_LEN);
br_gcm_flip(&gcm);
br_gcm_run(&gcm, 0, ct, ct_len);

if (!br_gcm_check_tag(&gcm, tag)) {
free(ct); ulog("AES-GCM RX: auth FAILED - rejected"); return "";
}

// Extract and validate counter (first 4 bytes of plaintext)
if (ct_len < 4) { free(ct); ulog("AES-GCM RX: too short for counter"); return ""; }

uint32_t rx_counter = ((uint32_t)ct[0] << 24) | ((uint32_t)ct[1] << 16)
| ((uint32_t)ct[2] << 8) | (uint32_t)ct[3];
uint32_t last_rx = eeprom_read_rx_counter();

if (rx_counter <= last_rx) {
free(ct);
ulog("AES-GCM RX: REPLAY DETECTED - counter %u <= last seen %u",
rx_counter, last_rx);
return "";
}

eeprom_write_rx_counter(rx_counter);

String result;
result.reserve(ct_len - 4);

for (size_t i = 4; i < ct_len; i++) {
result += (char)ct[i];
}
free(ct);
ulog("AES-GCM RX: accepted, counter=%u", rx_counter);
return result;
}
54 changes: 54 additions & 0 deletions examples/message-encryption/receive/setup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* setup.cpp
* This is the configuration code for a IoTempower node.
* Here, you define all the devices (and eventually their interactions)
* connected to the node specified with this directory.
* If you want to see more device configuration examples,
* check $IOTEMPOWER_ROOT/examples/running-node-test-setup.cpp
*
* Or check out the command reference for potential devices you can add.
*
* This whole comment block can be deleted
* */
void iotempower_init() {
eeprom_init();
}

// Simple LED node
rgb_single(rgb1, D4, D3, D2);

void iotempower_start() {

IN(rgb1).subdevice(1)->with_receive_cb([&](const Ustring& encrypted_payload) {
String decrypted = aes_receive_secure(encrypted_payload.as_cstr());

if (decrypted == "on") {
ulog("AES accepted: turning ON");
IN(rgb1).on();
return true;
}
else if (decrypted == "off") {
ulog("AES accepted: turning OFF");
IN(rgb1).off();
return true;
}

ulog("AES On/Off command rejected or invalid.");
return false;
});

IN(rgb1).subdevice(5)->with_receive_cb([&](const Ustring& encrypted_payload) {
String decrypted = aes_receive_secure(encrypted_payload.as_cstr());

if (decrypted != "") {
ulog("AES Color command accepted: %s", decrypted.c_str());

Ustring u_decrypted(decrypted.c_str());

IN(rgb1).set_colorstr(u_decrypted);
return true;
}

ulog("AES Color command rejected.");
return false;
});
}
Loading
Loading