Aller au contenu

Dépôt de Code

Accédez au code source complet pour la sécurisation LoRa : génération de clés, provisionnement ATECC608B et communication chiffrée.

Scan_SE.ino
Étape 0 : Vérification Hardware (ID Unique)
// ====== ETAPE 1 : ID UNIQUE ATECC608B-TNGTLS ======
#include <Wire.h>
#include <SparkFun_ATECCX08a_Arduino_Library.h>

#define SDA_PIN   32     // M5StickC Plus 2 / Core2 Port A
#define SCL_PIN   33
#define ECC_ADDR  0x35   // Adresse 7 bits par défaut du TNGTLS

ATECCX08A atecc;

void printHex(uint8_t b){ if(b<0x10) Serial.print('0'); Serial.print(b, HEX); }

void setup() {
  Serial.begin(115200);
  delay(500);

  Wire.begin(SDA_PIN, SCL_PIN);
  Wire.setClock(100000); // 100 kHz pour être tolérant

  Serial.println("\n=== ETAPE 1 : Lecture ID unique (serial) ===");

  if (!atecc.begin(ECC_ADDR, Wire, Serial)) {
    Serial.println("❌ ATECC non détecté (adresse 0x35). Vérifie Port A et l'alim.");
    while (1) delay(1000);
  }
  Serial.println("✅ Secure Element réveillé.");

  // Lecture de la zone config pour remplir revisionNumber[] et serialNumber[]
  if (!atecc.readConfigZone(false)) {
    Serial.println("❌ Echec readConfigZone().");
    while (1) delay(1000);
  }

  Serial.print("Revision: ");
  for (int i=0;i<4;i++) printHex(atecc.revisionNumber[i]);
  Serial.println();

  Serial.print("Serial  : ");
  for (int i=0;i<9;i++)  printHex(atecc.serialNumber[i]);
  Serial.println();

  Serial.println("\nCritère de réussite : vous voyez 9 octets hex pour 'Serial'.");
}

void loop() {}
generation_cle.cpp
Étape 1 : Génération de Clé (À venir)
// -----------------------------------------------------
// CODE DE GÉNÉRATION DE CLÉ (PLACEHOLDER)
// -----------------------------------------------------
// Provisionnement du slot 8 du secure element ATECC608B
// Objectif : écrire un blob PKCS#8 (clé privée DER) précédé de sa longueur sur 2 octets
// dans la zone DATA du slot 8 (13 blocs de 32 octets, soit 416 octets).

#include 
#include 
#include 

// Brochage I2C du secure element
static const uint8_t I2C_SDA  = 32;
static const uint8_t I2C_SCL  = 33;
static const uint8_t ECC_ADDR = 0x35;

ATECCX08A atecc;

// Paste from m5go_pub_key_der.h (generated by xxd -i)
// Ce tableau contient : [2 octets de longueur DER] + [blob DER de la clé privée]
const uint8_t m5go_pub_withlen_bin[] = {
  0x00, 0x8a, 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07,
  0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48,
  0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x6d, 0x30, 0x6b, 0x02, 0x01, 0x01,
  0x04, 0x20, 0x58, 0x55, 0xb5, 0x34, 0xff, 0xb5, 0xd9, 0xcc, 0x58, 0x9d,
  0xb0, 0x14, 0x17, 0xd1, 0x4f, 0xed, 0x16, 0xef, 0x24, 0xe2, 0x67, 0x37,
  0x02, 0x19, 0x69, 0xc7, 0x41, 0x86, 0x9a, 0x88, 0x8a, 0xbb, 0xa1, 0x44,
  0x03, 0x42, 0x00, 0x04, 0x14, 0x57, 0x96, 0x8c, 0x3f, 0xd7, 0xee, 0x11,
  0x0e, 0x1e, 0x45, 0x4a, 0x5c, 0xdb, 0xbe, 0xf4, 0x1b, 0x7a, 0x8d, 0xf6,
  0xba, 0x00, 0xc3, 0x5d, 0x8f, 0x91, 0x7d, 0xf5, 0xf5, 0x90, 0x0b, 0xfd,
  0xe4, 0x3b, 0x01, 0xe9, 0xf2, 0xe1, 0x85, 0x27, 0x3c, 0x16, 0x0d, 0xab,
  0xaf, 0x7b, 0x32, 0x49, 0xe9, 0x8f, 0x6d, 0x7e, 0xc1, 0xb7, 0x95, 0xc8,
  0x8b, 0x23, 0xc6, 0xc3, 0xbf, 0x28, 0xc7, 0x0f
};
const size_t m5go_pub_withlen_bin_len = sizeof(m5go_pub_withlen_bin);

// Helpers for slot 8
// Calcule l'adresse ATECC (zone DATA) associée au slot 8, bloc "block"
static uint16_t slot8AddressForBlock(uint8_t block)
{
  // Format d'adresse : bits [6:3] = slot (8), bits [8..] = block, bits [2:0] = word offset (0 pour 32 octets)
  return ((uint16_t)8 << 3) | ((uint16_t)block << 8);
}

// Écrit 32 octets dans le slot 8, bloc "block"
bool writeDataSlot8Block(ATECCX08A &atecc, uint8_t block, const uint8_t *data32)
{
  if (block >= 13) return false;  // slot 8 = 13 blocs de 32 octets
  uint16_t addr = slot8AddressForBlock(block);
  if (!atecc.write(ZONE_DATA, addr, (uint8_t *)data32, 32))
  {
    Serial.print("write block "); Serial.print(block); Serial.println(" failed");
    return false;
  }
  return true;
}

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.setRotation(1);
  M5.Display.clear();
  M5.Display.setCursor(10, 20);
  M5.Display.println("Provision slot 8 (PUB)");

  Serial.begin(115200);
  delay(500);
  Serial.println("\n--- ATECC Provision (Publisher, v2) ---");

  // Initialisation I2C vers le secure element
  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000);

  // Initialisation de la librairie SparkFun
  if (!atecc.begin(ECC_ADDR, Wire, Serial)) {
    Serial.println("ERROR: atecc.begin() failed");
    return;
  }

  // Lecture de la zone de configuration pour obtenir l'état des locks
  if (!atecc.readConfigZone(false)) {
    Serial.println("ERROR: readConfigZone() failed");
    return;
  }

  // Affichage d'information sur les locks (pour debug)
  Serial.print("ConfigLock="); Serial.println(atecc.configLockStatus);
  Serial.print("DataOTPLock="); Serial.println(atecc.dataOTPLockStatus);
  Serial.print("Slot0Lock="); Serial.println(atecc.slot0LockStatus);

  // Taille du blob "2 octets de longueur + DER"
  Serial.print("Key blob length (with prefix) = ");
  Serial.println(m5go_pub_withlen_bin_len);

  // Boucle d'écriture par blocs de 32 octets dans le slot 8
  size_t offset = 0;
  uint8_t block = 0;
  while (offset < m5go_pub_withlen_bin_len && block < 13) {
    uint8_t buf32[32];
    memset(buf32, 0, sizeof(buf32));  // remplissage par 0 pour les zones partielles

    size_t remaining = m5go_pub_withlen_bin_len - offset;
    size_t toCopy = (remaining > 32) ? 32 : remaining;

    memcpy(buf32, &m5go_pub_withlen_bin[offset], toCopy);

    Serial.print("Writing slot8 block ");
    Serial.print(block);
    Serial.print(", bytes ");
    Serial.println(toCopy);

    if (!writeDataSlot8Block(atecc, block, buf32)) {
      Serial.println("ERROR writing block, aborting");
      return;
    }

    offset += toCopy;
    block++;
  }

  // Fin du provisioning : tous les octets ont été écrits
  Serial.println("Provisioning complete.");
  M5.Display.setCursor(10, 50);
  M5.Display.println("Done. Power-cycle.");
}

void loop() {
  // Pas de logique en boucle, on laisse l'utilisateur re-booter après provisioning
  M5.update();
}

// -----------------------------------------------------
LoRa_ATECC_ProvisionKey.ino
Étape 2 : Provisionnement Slot 8
// LoRa_ATECC_ProvisionKey.ino
#include <M5Unified.h>
#include <Wire.h>
#include <SparkFun_ATECCX08a_Arduino_Library.h>

static const uint8_t I2C_SDA  = 32;
static const uint8_t I2C_SCL  = 33;
static const uint8_t ECC_ADDR = 0x35;

ATECCX08A atecc;

// 32-byte shared LoRa key (example). Use the SAME bytes on both good M5s.
const uint8_t LORA_KEY[32] = {
  0x7A, 0xF4, 0x39, 0x81, 0xC2, 0x55, 0x9E, 0x01,
  0x24, 0xB8, 0x93, 0x6D, 0x11, 0xA7, 0xCB, 0xEF,
  0x52, 0x3C, 0x68, 0x9D, 0xFA, 0x0B, 0x7E, 0x19,
  0x40, 0x22, 0xDD, 0x56, 0x8A, 0xBC, 0x33, 0x77
};

// ---- Slot 8 helpers ----
static uint16_t slot8AddressForBlock(uint8_t block)
{
  // Slot 8, 32-byte block addressing. 
  return ((uint16_t)8 << 3) | ((uint16_t)block << 8);
}

bool writeDataSlot8Block(ATECCX08A &atecc, uint8_t block, const uint8_t *data32)
{
  if (block >= 13) return false;
  uint16_t addr = slot8AddressForBlock(block);
  if (!atecc.write(ZONE_DATA, addr, (uint8_t *)data32, 32))
  {
    Serial.print("write block "); Serial.print(block); Serial.println(" failed");
    return false;
  }
  return true;
}

void printHex(const uint8_t *buf, size_t len)
{
  for (size_t i = 0; i < len; i++) {
    if (i && (i % 16 == 0)) Serial.println();
    if (buf[i] < 0x10) Serial.print('0');
    Serial.print(buf[i], HEX);
    Serial.print(' ');
  }
  Serial.println();
}

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.setRotation(1);
  M5.Display.clear();
  M5.Display.setCursor(10, 20);
  M5.Display.println("Provision LoRa key (slot 8)");

  Serial.begin(115200);
  delay(500);
  Serial.println("\n--- ATECC Provision LoRa key (slot 8) ---");

  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000);

  if (!atecc.begin(ECC_ADDR, Wire, Serial)) {
    Serial.println("ERROR: atecc.begin() failed");
    M5.Display.setCursor(10, 50);
    M5.Display.println("ATECC begin failed");
    return;
  }

  if (!atecc.readConfigZone(false)) {
    Serial.println("ERROR: read onfigZone() failed");
    M5.Display.setCursor(10, 50);
    M5.Display.println("readConfig failed");
    return;
  }

  Serial.print("ConfigLock=");   Serial.println(atecc.configLockStatus);
  Serial.print("DataOTPLock=");  Serial.println(atecc.dataOTPLockStatus);
  Serial.print("Slot0LockBit="); Serial.println(atecc.slot0LockStatus);

  Serial.println("Writing 32-byte LoRa key into slot 8 block 0...");
  Serial.print("Key = ");
  printHex(LORA_KEY, 32);

  if (!writeDataSlot8Block(atecc, 0, LORA_KEY)) {
    Serial.println("ERROR writing key to slot 8 block 0");
    M5.Display.setCursor(10, 50);
    M5.Display.println("Write failed");
    return;
  }

  Serial.println("Provisioning complete. Slot 8 block 0 now holds LoRa key.");
  M5.Display.setCursor(10, 50);
  M5.Display.println("Done. Power-cycle device.");
}

void loop() {
  M5.update();
}
M5_LoRa_Secure_Comm.ino
Étape 3 : Communication Complète (AES + UI + ACK)
/******************************************************
 * M5Stack LoRa – UI complète + ACK FIABLE + AES/ATECC
 * Nouvelle version corrigée (TS supprimé, parser stable)
 *******************************************************/

#include <M5Unified.h>
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <SparkFun_ATECCX08a_Arduino_Library.h>
#include "mbedtls/aes.h"

// ----------------------------------------------------
//                      CONFIG
// ----------------------------------------------------

#ifndef ATECCX08A_ZONE_DATA
#define ATECCX08A_ZONE_DATA 0x02
#endif

#define LORA_CS   5
#define LORA_RST  13
#define LORA_IRQ  34
#define LORA_FREQ 868E6

#define COLOR_SECURE   COLOR_OK     // vert
#define COLOR_UNSECURE COLOR_WARN   // rouge

// Modifier sur chaque M5 : "001" ou "002"
String NODE_ID = "002";

static const uint8_t I2C_SDA  = 32;
static const uint8_t I2C_SCL  = 33;
static const uint8_t ECC_ADDR = 0x35;

ATECCX08A atecc;
uint8_t g_loraKey[32];
bool g_haveKey = false;
bool secureMode = true;

// ----------------------------------------------------
//          VARIABLES LOra / ACK / MESSAGE
// ----------------------------------------------------
String lastTX = "-";
String lastRX = "Waiting...";
String lastPlaintext = "";
int    lastRSSI = 0;
String lastPacketTX = "";
String lastPacketRX = "";
String lastEncryptedTX = "";
String lastEncryptedRX = "";


// Stop-and-wait ACK
static int      g_nextMsgId        = 1;
static bool     g_waitingAck       = false;
static int      g_waitAckId        = -1;
static String   g_waitAckDst       = "";
static unsigned long g_ackDeadline = 0;
static uint8_t  g_retryCount       = 0;
static String   g_waitAckPacket    = "";

static const uint8_t  LORA_MAX_RETRIES   = 3;
static const unsigned long LORA_ACK_TIMEOUT = 3000; // 3 sec

// duplicate detection
uint16_t g_lastSeenFrom001 = 0;
uint16_t g_lastSeenFrom002 = 0;

// Max plaintext size (for AES)
static const size_t LORA_MAX_PLAINTEXT = 80;

// ----------------------------------------------------
//                      UI CONSTANTS
// ----------------------------------------------------
static const uint16_t COLOR_BG      = 0x0000;
static const uint16_t COLOR_CARD    = 0x0005;
static const uint16_t COLOR_ACCENT  = 0x07FF;
static const uint16_t COLOR_TEXT    = 0xFFFF;
static const uint16_t COLOR_OK      = 0x07E0;
static const uint16_t COLOR_WARN    = 0xF800;

static const int UI_MARGIN       = 6;
static const int UI_CARD_RADIUS  = 8;
static const int UI_CARD_SPACING = 6;
static const int UI_HEADER_H     = 32;
static const int UI_BOTTOM_BAR_H = 14;

// throttle : limiter les refresh UI
unsigned long nextUI = 0;

// ----------------------------------------------------
//                  UI LAYOUT
// ----------------------------------------------------
static void uiGetLayout(int &yStatus, int &hStatus,
                        int &yTraffic, int &hTraffic,
                        int &yDecrypt, int &hDecrypt)
{
  auto &d = M5.Display;
  int screenH = d.height();

  hStatus  = 48;
  hTraffic = 72;
  yStatus  = 4 + UI_HEADER_H + UI_CARD_SPACING;
  yTraffic = yStatus + hStatus + UI_CARD_SPACING;
  yDecrypt = yTraffic + hTraffic + UI_CARD_SPACING;
  hDecrypt = screenH - yDecrypt - UI_BOTTOM_BAR_H - 4;

  if (hDecrypt < 32) hDecrypt = 32;
}

// ----------------------------------------------------
//                HEADER (titre + NodeID)
// ----------------------------------------------------
void uiDrawHeader() {
  auto& d = M5.Display;
  d.fillScreen(COLOR_BG);

  int w = d.width() - 2 * UI_MARGIN;
  d.fillRoundRect(UI_MARGIN, 4, w, UI_HEADER_H, UI_CARD_RADIUS, COLOR_CARD);

  d.setTextColor(COLOR_ACCENT, COLOR_CARD);
  d.setTextSize(2);
  d.setCursor(UI_MARGIN + 8, 8);
  d.print("LoRa Secure Node");

  d.setTextSize(1);
  d.setCursor(UI_MARGIN + 8, 24);
  d.printf("Node ID: %s", NODE_ID.c_str());
}

// ----------------------------------------------------
//              STATIC CARDS (fond des 3 cartes)
// ----------------------------------------------------
void uiDrawStaticCards() {
  auto& d = M5.Display;
  int w = d.width() - 2 * UI_MARGIN;

  int yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt;
  uiGetLayout(yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt);

  d.fillRoundRect(UI_MARGIN, yStatus,  w, hStatus,  UI_CARD_RADIUS, COLOR_CARD);
  d.fillRoundRect(UI_MARGIN, yTraffic, w, hTraffic, UI_CARD_RADIUS, COLOR_CARD);
  d.fillRoundRect(UI_MARGIN, yDecrypt, w, hDecrypt, UI_CARD_RADIUS, COLOR_CARD);

  d.setTextColor(COLOR_TEXT, COLOR_CARD);
  d.setTextSize(1);

  d.setCursor(UI_MARGIN + 6, yStatus + 4);
  d.print("Node / Security");

  d.setCursor(UI_MARGIN + 6, yTraffic + 4);
  d.print("Last TX / RX");

  d.setCursor(UI_MARGIN + 6, yDecrypt + 4);
  d.print("Decryption / RSSI");
}

// ----------------------------------------------------
//             UI UPDATE: STATUS CARD
// ----------------------------------------------------
void uiUpdateStatusCard() {
  auto& d = M5.Display;
  int screenW = d.width();

  int yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt;
  uiGetLayout(yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt);

  int xCard = UI_MARGIN;
  int wCard = screenW - 2 * UI_MARGIN;
  int contentY = yStatus + 18;
  int contentH = hStatus - 22;
  
  // nettoyage complet de la zone texte
  d.fillRect(xCard + 2, contentY, wCard - 4, contentH, COLOR_CARD);

  d.setTextSize(1);
  d.setCursor(xCard + 8, contentY);
  d.setTextColor(COLOR_TEXT, COLOR_CARD);
  d.printf("Node: %s", NODE_ID.c_str());

  d.setCursor(xCard + 8, contentY + 12);
  if (g_haveKey) {
    d.setTextColor(COLOR_OK, COLOR_CARD);
    d.print("Key: OK  ");
  } else {
    d.setTextColor(COLOR_WARN, COLOR_CARD);
    d.print("Key: MISSING  ");
  }

  d.setTextColor(COLOR_TEXT, COLOR_CARD);
  d.print("Secure: ");

  if (!g_haveKey) {
    d.setTextColor(COLOR_WARN, COLOR_CARD);
    d.print("OFF (no key)");
  } else if (secureMode) {
    d.setTextColor(COLOR_OK, COLOR_CARD);
    d.print("ON");
  } else {
    d.setTextColor(COLOR_WARN, COLOR_CARD);
    d.print("OFF");
  }
}

// ----------------------------------------------------
//               UI UPDATE: TX/RX CARD
// ----------------------------------------------------
void uiUpdateTrafficCard()
{
    auto& d = M5.Display;
    int screenW = d.width();

    int yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt;
    uiGetLayout(yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt);

    int xCard = UI_MARGIN;
    int wCard = screenW - 2 * UI_MARGIN;
    int contentY = yTraffic + 18;

    // Effacer zone
    d.fillRect(xCard + 2, contentY, wCard - 4, hTraffic - 22, COLOR_CARD);

    d.setTextSize(1);
    d.setCursor(xCard + 8, contentY);

    // ---- TX ----
    d.setCursor(xCard + 8, contentY);
    d.setTextColor( secureMode ? COLOR_SECURE : COLOR_UNSECURE , COLOR_CARD );
    d.print("TX: ");

    d.setTextColor(COLOR_TEXT, COLOR_CARD);
    d.println(lastPacketTX);

    // ---- ENC TX ----
    d.setCursor(xCard + 8, contentY + 14);
    d.setTextColor( secureMode ? COLOR_SECURE : COLOR_UNSECURE , COLOR_CARD );
    d.print("ENC: ");

    d.setTextColor(COLOR_TEXT, COLOR_CARD);
    String shortEncTX = lastEncryptedTX;
    if (shortEncTX.length() > 32)
        shortEncTX = shortEncTX.substring(0, 32) + "...";
    d.println(shortEncTX);

    // ---- RX ----
    d.setCursor(xCard + 8, contentY + 32);
    d.setTextColor( secureMode ? COLOR_SECURE : COLOR_UNSECURE , COLOR_CARD );
    d.print("RX: ");

    d.setTextColor(COLOR_TEXT, COLOR_CARD);
    d.println(lastPacketRX);

    // ---- ENC RX ----
    d.setCursor(xCard + 8, contentY + 46);
    d.setTextColor( secureMode ? COLOR_SECURE : COLOR_UNSECURE , COLOR_CARD );
    d.print("ENC: ");

    d.setTextColor(COLOR_TEXT, COLOR_CARD);
    String shortEncRX = lastEncryptedRX;
    if (shortEncRX.length() > 32)
        shortEncRX = shortEncRX.substring(0, 32) + "...";
    d.println(shortEncRX);
}


// ----------------------------------------------------
//            UI UPDATE: DECRYPT + RSSI CARD
// ----------------------------------------------------
void uiUpdateDecryptCard() {
  auto& d = M5.Display;

  int yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt;
  uiGetLayout(yStatus, hStatus, yTraffic, hTraffic, yDecrypt, hDecrypt);

  int xCard = UI_MARGIN;
  int wCard = d.width() - 2 * UI_MARGIN;

  int contentY = yDecrypt + 18;
  int contentH = hDecrypt - 22;

  d.fillRect(xCard + 2, contentY, wCard - 4, contentH, COLOR_CARD);

  d.setTextSize(1);
  int textX = xCard + 8;

  d.setCursor(textX, contentY);
  d.setTextColor(COLOR_ACCENT, COLOR_CARD);
  d.printf("RSSI: %d", lastRSSI);

  // ---------------------------------------------
  //  TRONQUER lastPlaintext (max 24 caractères)
  // ---------------------------------------------
  String shortPlain = lastPlaintext;
  if (shortPlain.length() > 24) {
      shortPlain = shortPlain.substring(0, 24) + "...";
  }

  d.setCursor(textX, contentY + 12);
  d.setTextColor(COLOR_TEXT, COLOR_CARD);
  d.print("Dec: " + shortPlain);

  // bottom bar (A/B/C)
  int bottomY = d.height() - UI_BOTTOM_BAR_H;
  d.fillRect(0, bottomY, d.width(), UI_BOTTOM_BAR_H, COLOR_BG);
  d.setCursor(8, bottomY + 2);
  d.print("A:Send   B:-   C:Secure");
}

// ----------------------------------------------------
//             UI REFRESH (throttled)
// ----------------------------------------------------
void uiRefresh() {
  if (millis() < nextUI) return;
  nextUI = millis() + 120; // max 8 fps

  uiUpdateStatusCard();
  uiUpdateTrafficCard();
  uiUpdateDecryptCard();
}

void selfTestCrypto()
{
  if (!g_haveKey) {
    Serial.println("SelfTest: NO KEY, abort");
    return;
  }

  String plain = "HELLO";
  String ivB64, ctB64;

  Serial.println("SelfTest: starting with plain = " + plain);

  if (!loraEncrypt(plain, g_loraKey, ivB64, ctB64)) {
    Serial.println("SelfTest: Encrypt FAILED");
    return;
  }

  Serial.println("SelfTest: IV = " + ivB64);
  Serial.println("SelfTest: CT = " + ctB64);

  String decrypted;
  if (!loraDecrypt(ivB64, ctB64, g_loraKey, decrypted)) {
    Serial.println("SelfTest: Decrypt FAILED");
    return;
  }

  Serial.println("SelfTest: decrypted = " + decrypted);

  if (decrypted == plain) {
    Serial.println("SelfTest:  OK (HELLO -> HELLO)");
  } else {
    Serial.println("SelfTest:  MISMATCH");
  }
  Serial.println("------------------------------");
}


// ----------------------------------------------------
//                      SETUP
// ----------------------------------------------------
void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Display.setRotation(1);

  Serial.begin(115200);
  randomSeed((uint32_t)esp_random());
  delay(300);

  // -------- AJOUT : désactivation IRQ LoRa pendant secure-element --------
  pinMode(LORA_IRQ, INPUT);
  gpio_intr_disable(GPIO_NUM_34); // désactive l’IRQ proprement


  Serial.println();
  Serial.println("==== BOOT ====");

  // -------------------------------------------------
  //   1) ATECC FIRST – avant UI, avant LoRa
  // -------------------------------------------------
  Wire.begin(I2C_SDA, I2C_SCL);
  Wire.setClock(100000);

  g_haveKey = false;

  if (atecc.begin(ECC_ADDR, Wire, Serial)) {
      if (atecc.readConfigZone(false)) {
          if (readDataSlot8Block(atecc, 0, g_loraKey)) {
              g_haveKey = true;
          }
      }
  }

  // -------- AJOUT : double lecture pour fiabiliser I2C --------
  if (g_haveKey) {
      uint8_t tmpKey[32];
      if (readDataSlot8Block(atecc, 0, tmpKey)) {
          memcpy(g_loraKey, tmpKey, 32);
      }
  }

  Serial.print("Node ");
  Serial.print(NODE_ID);
  Serial.print(" — Slot 8 Key = ");
  if (g_haveKey) {
      for (int i = 0; i < 32; i++) Serial.printf("%02X", g_loraKey[i]);
  } else {
      Serial.print("NO KEY");
  }
  Serial.println();
  Serial.println("--------------------------------------");

  // -------------------------------------------------
  //   2) Self-test crypto — maintenant ça marchera
  // -------------------------------------------------
  selfTestCrypto();
  Serial.println("--------------------------------------");

  // -------------------------------------------------
  //   3) UI / LoRa seulement après le test
  // -------------------------------------------------
  uiDrawHeader();
  uiDrawStaticCards();
  uiRefresh();

  LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ);
  if (!LoRa.begin(LORA_FREQ)) {
    Serial.println("LoRa init failed!");
  }

  // -------- AJOUT : réactivation IRQ LoRa --------
  attachInterrupt(LORA_IRQ, NULL, RISING);

  if (!g_haveKey) secureMode = false;

  lastPlaintext = g_haveKey ? "ATECC key OK" : "NO KEY";
  uiRefresh();
}



// ======================================================================
//                    PARTIE 2/3 — Crypto + Parsing + ACK
// ======================================================================

// ----------------------------------------------------
//               BASE64 ENCODE / DECODE
// ----------------------------------------------------
static const char b64_table[] =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static String base64Encode(const uint8_t *data, size_t len) {
  String out;
  out.reserve(((len + 2) / 3) * 4);

  size_t i = 0;
  while (i < len) {
    size_t remain = len - i;

    uint32_t octet_a = data[i++];
    uint32_t octet_b = (remain > 1) ? data[i++] : 0;
    uint32_t octet_c = (remain > 2) ? data[i++] : 0;

    uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c;

    out += b64_table[(triple >> 18) & 0x3F];
    out += b64_table[(triple >> 12) & 0x3F];
    out += (remain > 1) ? b64_table[(triple >> 6) & 0x3F] : '=';
    out += (remain > 2) ? b64_table[triple & 0x3F]       : '=';
  }

  return out;
}


static int8_t b64DecodeChar(char c)
{
  if (c >= 'A' && c <= 'Z') return c - 'A';
  if (c >= 'a' && c <= 'z') return c - 'a' + 26;
  if (c >= '0' && c <= '9') return c - '0' + 52;
  if (c == '+') return 62;
  if (c == '/') return 63;
  if (c == '=') return -2;
  return -1;
}

static bool base64Decode(const String &in, uint8_t *out,
                         size_t &outLen, size_t maxOutLen)
{
  outLen = 0;
  int val = 0, valb = -8;

  for (int i = 0; i < in.length(); i++) {
    int8_t c = b64DecodeChar(in[i]);
    if (c == -1) continue;
    if (c == -2) break;

    val = (val << 6) + c;
    valb += 6;

    if (valb >= 0) {
      if (outLen >= maxOutLen) return false;
      out[outLen++] = (uint8_t)((val >> valb) & 0xFF);
      valb -= 8;
    }
  }
  return true;
}

// ----------------------------------------------------
//                      AES-CTR
// ----------------------------------------------------
static bool aesCtrCrypt(const uint8_t *key, size_t keyBits,
                        const uint8_t iv[16],
                        const uint8_t *input, size_t len,
                        uint8_t *output)
{
  mbedtls_aes_context ctx;
  mbedtls_aes_init(&ctx);

  if (mbedtls_aes_setkey_enc(&ctx, key, keyBits) != 0) {
    mbedtls_aes_free(&ctx);
    return false;
  }

  size_t nc_off = 0;
  unsigned char nonce_counter[16];
  unsigned char stream_block[16];
  memcpy(nonce_counter, iv, 16);
  memset(stream_block, 0, 16);

  int res = mbedtls_aes_crypt_ctr(&ctx, len, &nc_off,
                                  nonce_counter, stream_block,
                                  input, output);

  mbedtls_aes_free(&ctx);
  return (res == 0);
}

// ----------------- Encrypt + Base64 -----------------
static bool loraEncrypt(const String &plain,
                        const uint8_t key[32],
                        String &outIVb64,
                        String &outCTb64)
{
  size_t len = plain.length();
  if (len == 0) return true;
  if (len > LORA_MAX_PLAINTEXT) len = LORA_MAX_PLAINTEXT;

  uint8_t bufIn[LORA_MAX_PLAINTEXT];
  for (size_t i = 0; i < len; i++) bufIn[i] = plain[i];

  uint8_t iv[16];
  for (int i = 0; i < 16; i++) iv[i] = random(0, 256);

  uint8_t cipher[LORA_MAX_PLAINTEXT];

  if (!aesCtrCrypt(key, 256, iv, bufIn, len, cipher))
    return false;

  outIVb64 = base64Encode(iv, 16);
  outCTb64 = base64Encode(cipher, len);
  return true;
}

// ----------------- Decrypt -----------------
static bool loraDecrypt(const String &ivB64,
                        const String &ctB64,
                        const uint8_t key[32],
                        String &outPlain)
{
  uint8_t iv[16]; size_t ivLen = 0;
  uint8_t cipher[LORA_MAX_PLAINTEXT]; size_t cipherLen = 0;

  if (!base64Decode(ivB64, iv, ivLen, sizeof(iv))) return false;
  if (ivLen != 16) return false;
  if (!base64Decode(ctB64, cipher, cipherLen, sizeof(cipher))) return false;

  if (cipherLen == 0) { outPlain = ""; return true; }

  uint8_t plain[LORA_MAX_PLAINTEXT];

  if (!aesCtrCrypt(key, 256, iv, cipher, cipherLen, plain))
    return false;

  outPlain = "";
  for (size_t i = 0; i < cipherLen; i++)
    outPlain += (char)plain[i];

  return true;
}

// ----------------------------------------------------
//                  ATECC Slot 8 Key Load
// ----------------------------------------------------
bool readDataSlot8Block(ATECCX08A &atecc, uint8_t block, uint8_t *out32)
{
  if (block >= 13) return false;

  uint16_t addr = ((uint16_t)8 << 3) | ((uint16_t)block << 8);

  if (!atecc.read(ATECCX08A_ZONE_DATA, addr, 32, false)) {
    Serial.println("ATECC read error");
    return false;
  }

  for (int i = 0; i < 32; i++)
    out32[i] = atecc.inputBuffer[1 + i];

  return true;
}

// ======================================================================
//                    PARSER FIXE (ordre strict)
// ======================================================================
//
// Format A imposé :
//   SRC=xxx;DST=xxx;ID=n;TYPE=xxx;MSG=xxxxx
//
// -> Parsing **100 % fiable**
// -> Pas de TS
// -> Pas de champs optionnels
// ======================================================================

bool parsePacket(const String &p,
                 String &src, String &dst,
                 int &id, String &type, String &msg)
{
  // champs à rechercher
  int pSRC  = p.indexOf("SRC=");
  int pDST  = p.indexOf(";DST=");
  int pID   = p.indexOf(";ID=");
  int pTYPE = p.indexOf(";TYPE=");
  int pMSG  = p.indexOf(";MSG=");

  if (pSRC < 0 || pDST < 0 || pID < 0 || pTYPE < 0 || pMSG < 0)
    return false;

  // extraction
  src  = p.substring(pSRC + 4,  pDST);
  dst  = p.substring(pDST + 5,  pID);
  id   = p.substring(pID  + 4,  pTYPE).toInt();
  type = p.substring(pTYPE + 6, pMSG);
  msg  = p.substring(pMSG  + 5);

  return true;
}

// ======================================================================
//                DUPLICATE DETECTION (ID par source)
// ======================================================================
uint16_t getLastSeen(const String &src)
{
  if (src == "001") return g_lastSeenFrom001;
  if (src == "002") return g_lastSeenFrom002;
  return 0;
}

void setLastSeen(const String &src, uint16_t id)
{
  if (src == "001") g_lastSeenFrom001 = id;
  if (src == "002") g_lastSeenFrom002 = id;
}

// ======================================================================
//                SEND ACK (format A, immédiat)
// ======================================================================
void sendAck(const String &dst, int msgId, const String &originalPlain)
{
    // On construit le texte clair de l’acquittement
    String ackPlain = "ACK(" + originalPlain + ")";

    String content = ackPlain;
    lastEncryptedTX = "";

    // ---- Chiffrement si nécessaire ----
    if (secureMode && g_haveKey)
    {
        String ivB64, ctB64;
        if (!loraEncrypt(ackPlain, g_loraKey, ivB64, ctB64)) {
            Serial.println("ACK Encrypt ERR");
            return;
        }

        content = "SEC1:IV=" + ivB64 + ":CT=" + ctB64;
        lastEncryptedTX = content;
    }

    // ---- Construction du paquet LoRa ----
    String packet =
        "SRC=" + NODE_ID +
        ";DST=" + dst +
        ";ID=" + String(msgId) +
        ";TYPE=ACK;MSG=" + content;

    LoRa.beginPacket();
    LoRa.print(packet);
    LoRa.endPacket();

    Serial.println("TX ACK: " + packet);

    // ---- Affichage TX clair ----
    lastPacketTX =
        "SRC=" + NODE_ID +
        ";DST=" + dst +
        ";ID=" + String(msgId) +
        ";TYPE=ACK;MSG=" + ackPlain;

    lastTX = "ACK (" + ackPlain + ")";
}



// ======================================================================
//                SEND MESSAGE (encrypt si needed)
// ======================================================================
void sendMessage(const String &dst, const String &plain)
{
    if (g_waitingAck) {
        lastPlaintext = "ERR: ACK pending";
        return;
    }

    if (secureMode && !g_haveKey) {
        lastPlaintext = "Secure ON but no key";
        return;
    }

    String content = plain;
    lastEncryptedTX = "";    // reset par défaut

    // ---------------------
    //   CHIFFREMENT
    // ---------------------
    if (secureMode && g_haveKey)
    {
        String ivB64, ctB64;
        if (!loraEncrypt(plain, g_loraKey, ivB64, ctB64)) {
            lastPlaintext = "Encrypt error";
            return;
        }

        // Le message envoyé réellement
        content = "SEC1:IV=" + ivB64 + ":CT=" + ctB64;

        // Pour ENC TX (affichage)
        lastEncryptedTX = content;
    }

    // Générer ID
    int msgId = g_nextMsgId++;
    if (g_nextMsgId > 30000) g_nextMsgId = 1;

    // ---------------------
    //    PACKET RADIO
    // ---------------------
    String packet =
        "SRC=" + NODE_ID +
        ";DST=" + dst +
        ";ID=" + String(msgId) +
        ";TYPE=MSG;MSG=" + content;

    // Envoi LoRa
    LoRa.beginPacket();
    LoRa.print(packet);
    LoRa.endPacket();

    // ---------------------
    //    AFFICHAGE
    // ---------------------

    // TX clair POUR L’UI
    lastPacketTX =
        "SRC=" + NODE_ID +
        ";DST=" + dst +
        ";ID="  + String(msgId) +
        ";TYPE=MSG;MSG=" + plain;

    lastTX = "MSG (" + plain + ")";
    lastPlaintext = "Sent: " + plain;

    Serial.println("TX MSG: " + packet);

    // ---------------------
    //    ACK WAIT
    // ---------------------
    g_waitingAck    = true;
    g_waitAckId     = msgId;
    g_waitAckDst    = dst;
    g_retryCount    = 0;
    g_waitAckPacket = packet;
    g_ackDeadline   = millis() + LORA_ACK_TIMEOUT;
}


// ======================================================================
//                  PARTIE 3/3 — RX + ACK TIMEOUT + LOOP
// ======================================================================

// -------------------------------------------------------------
//                          HANDLE RECEIVE
// -------------------------------------------------------------
void handleReceivePacket(const String &incoming)
{
    lastRSSI = LoRa.packetRssi();
    Serial.println("RX: " + incoming);

    // -------------------------------
    //        PARSING
    // -------------------------------
    String src, dst, type, msgField;
    int msgId;
    String plain = "";

    if (!parsePacket(incoming, src, dst, msgId, type, msgField)) {
        lastPlaintext = "Parse error";
        lastPacketRX = "Parse ERR";
        lastEncryptedRX = "";
        return;
    }

    Serial.printf("Parsed → SRC=%s DST=%s ID=%d TYPE=%s MSG=%s\n",
                  src.c_str(), dst.c_str(), msgId, type.c_str(), msgField.c_str());


    // ============================================================
    //                              ACK RECEIVED
    // ============================================================
    if (type == "ACK")
    {
        // Enregistrer la version chiffrée dans ENC RX
        if (msgField.startsWith("SEC1:IV="))
            lastEncryptedRX = msgField;
        else
            lastEncryptedRX = "";

        String plainAck = "";

        // Déchiffrement si nécessaire
        if (msgField.startsWith("SEC1:IV=") && g_haveKey)
        {
            int ivPos = msgField.indexOf("IV=") + 3;
            int ctPos = msgField.indexOf(":CT=");
            String ivB64 = msgField.substring(ivPos, ctPos);
            String ctB64 = msgField.substring(ctPos + 4);

            if (!loraDecrypt(ivB64, ctB64, g_loraKey, plainAck))
                plainAck = "ACK(DecryptERR)";
        }
        else
        {
            // ACK non chiffré
            plainAck = msgField;
        }

        // ---- Affichage RX clair ----
        lastPacketRX =
            "SRC=" + src +
            ";DST=" + dst +
            ";ID=" + String(msgId) +
            ";TYPE=ACK;MSG=" + plainAck;

        // Message de statut
        if (g_waitingAck && dst == NODE_ID && msgId == g_waitAckId)
        {
            g_waitingAck = false;
            g_retryCount = 0;
            lastPlaintext = "ACK recu (ID=" + String(msgId) + ")";
        }
        else
        {
            lastPlaintext = "ACK inattendu (ID=" + String(msgId) + ")";
        }

        return;
    }



    // ============================================================
    //                              NOT FOR ME
    // ============================================================
    if (dst != NODE_ID) {
        lastPlaintext = "Not for me";
        lastPacketRX = "Ignored";
        lastEncryptedRX = "";
        return;
    }


    // ============================================================
    //                      DUPLICATE CHECK
    // ============================================================
    uint16_t last = getLastSeen(src);
    if (last == (uint16_t)msgId)
    {
        Serial.println("Duplicate detected → resend ACK");

        String plainACK;

        if (msgField.startsWith("SEC1:IV=") && g_haveKey)
        {
            int ivPos = msgField.indexOf("IV=") + 3;
            int ctPos = msgField.indexOf(":CT=");
            String ivB64 = msgField.substring(ivPos, ctPos);
            String ctB64 = msgField.substring(ctPos + 4);
            loraDecrypt(ivB64, ctB64, g_loraKey, plainACK);
        }
        else plainACK = msgField;

        sendAck(src, msgId, plainACK);

        lastPlaintext = "Duplicate";
        lastPacketRX = "Duplicate";
        return;
    }
    setLastSeen(src, msgId);


    // ============================================================
    //                  MESSAGE NORMAL (TYPE=MSG)
    // ============================================================
    if (msgField.startsWith("SEC1:IV="))
    {
        // Partie chiffrée pour ENC RX
        lastEncryptedRX = msgField;

        if (!g_haveKey) {
            lastPlaintext = "Encrypted but NO KEY";
            lastPacketRX = "MSG (NO KEY)";
        }
        else
        {
            int ivPos = msgField.indexOf("IV=") + 3;
            int ctPos = msgField.indexOf(":CT=");

            String ivB64 = msgField.substring(ivPos, ctPos);
            String ctB64 = msgField.substring(ctPos + 4);

            if (loraDecrypt(ivB64, ctB64, g_loraKey, plain)) {
                lastPlaintext = plain;
            } else {
                plain = "Decrypt ERR";
                lastPlaintext = "Decrypt ERR";
            }

            // RX CLAIR → affichage
            lastPacketRX =
                "SRC=" + src +
                ";DST=" + dst +
                ";ID=" + String(msgId) +
                ";TYPE=MSG;MSG=" + plain;
        }
    }
    else
    {
        // Pas de chiffrement
        lastEncryptedRX = "";
        plain = msgField;
        lastPlaintext = msgField;

        lastPacketRX =
            "SRC=" + src +
            ";DST=" + dst +
            ";ID=" + String(msgId) +
            ";TYPE=MSG;MSG=" + msgField;
    }


    // ============================================================
    //                                  SEND ACK
    // ============================================================
    sendAck(src, msgId, plain);
}


// -------------------------------------------------------------
//                  ACK TIMEOUT + RETRIES
// -------------------------------------------------------------
void handleAckTimeout()
{
  if (!g_waitingAck) return;

  unsigned long now = millis();
  if (now < g_ackDeadline) return;

  // timeout reached
  if (g_retryCount >= LORA_MAX_RETRIES) {
    lastPlaintext = "ACK TIMEOUT → abandon (ID=" + String(g_waitAckId) + ")";
    Serial.println(lastPlaintext);

    g_waitingAck = false;
    g_waitAckId = -1;
    g_waitAckPacket = "";
    return;
  }

  // retry
  g_retryCount++;
  lastPlaintext = "Retry " + String(g_retryCount) +
                  " (ID=" + String(g_waitAckId) + ")";
  Serial.println(lastPlaintext);

  if (g_waitAckPacket.length()) {
    Serial.println("Re-TX: " + g_waitAckPacket);

    LoRa.beginPacket();
    LoRa.print(g_waitAckPacket);
    LoRa.endPacket();

    lastTX = g_waitAckPacket;
  }

  g_ackDeadline = now + LORA_ACK_TIMEOUT;
}


// -------------------------------------------------------------
//                BUTTON A : HELLO / OK / PING
// -------------------------------------------------------------
int msgIndex = 0;
const char* messages[] = {"HELLO", "OK", "PING"};

void handleButtonA() {
  String target = (NODE_ID == "001" ? "002" : "001");
  String msg = messages[msgIndex];
  msgIndex = (msgIndex + 1) % 3;
  sendMessage(target, msg);
}


// -------------------------------------------------------------
//                        MAIN LOOP
// -------------------------------------------------------------
void loop()
{
  M5.update();

  // ----------------------
  //   RX LOOPS
  // ----------------------
  int packetSize;
  while ((packetSize = LoRa.parsePacket())) {
    String incoming;
    while (LoRa.available())
      incoming += (char)LoRa.read();

    handleReceivePacket(incoming);
  }

  // ----------------------
  //   ACK TIMEOUT
  // ----------------------
  handleAckTimeout();

  // ----------------------
  //   UI REFRESH
  // ----------------------
  uiRefresh();

  // ----------------------
  //   BUTTONS
  // ----------------------
  if (M5.BtnA.wasPressed()) handleButtonA();

  if (M5.BtnC.wasPressed()) {
    if (!g_haveKey) {
      secureMode = false;
      lastPlaintext = "No key → Secure OFF";
    } else {
      secureMode = !secureMode;
      lastPlaintext = secureMode ? "Secure ON" : "Secure OFF";
    }
    Serial.println(lastPlaintext);
  }

  delay(5);
}