diff --git a/README.md b/README.md index 741f8da..e647f6b 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ module → App: SylDa_Server\n of SylDa_Client\n (device type) App → module: KEY?\n module → App: DEVICE_ID:\n (module UUID, 16 bytes = 32 hex) module → App: PSK:\n (PSK, 32 bytes = 64 hex) +module → App: SERVER_UUID:\n (alleen client: opgeslagen server UUID, 32 hex) module → App: NO_KEY\n (als module nog geen key heeft) App → module: SET_KEY:\n (nieuwe PSK schrijven, 64 hex) @@ -139,22 +140,13 @@ module → App: OK\n De server ondersteunt tot **2 gelijktijdige BLE-clients** (MAX_CLIENTS 2). Elke client krijgt een onafhankelijke ECDH-handshake, AES-sleutel en sequence counter. Relais-commando's van alle clients worden gecombineerd: een relais gaat aan als minimaal één client het aan-commando stuurt (OR-logica). -De server kan OTA-updates distribueren naar verbonden clients via het **broker protocol**: - -``` -BROKER_CMD_UPDATE_AVAILABLE (0x10) — server vertelt client dat er een update klaarstaat -BROKER_CMD_CLIENT_READY (0x20) — client meldt zich klaar voor OTA -``` - -Broker packets zijn versleuteld met AES-256-GCM (48 bytes: 4 seq + 8 nonce + 20 plain + 16 tag), onafhankelijk van relay-commando encryptie. - ### Beveiligingsanalyse | Aanval | Hoe geweerd | |---|---| | **Replay** | Sequence counter — zelfde packet werkt niet 2× (`seq <= lastSeq` geweerd) | | **Ongeautoriseerd device** | ECDH pubkey HMAC vereist PSK — verkeerde PSK = afgewezen public key | -| **BLE spoofing (client)** | **Bekend lek (issue #10):** ESP32 client scant op naam "SylDa_Server" i.p.v. UUID — geen server-verificatie. De Android app scant wél op service UUID én verifieert device UUID na connect. | +| **BLE spoofing (client)** | Client gebruikt service UUID + manufacturer data voor scanning, full 16-byte UUID + ECDH PSK-HMAC voor verificatie na connect. 4-byte manufacturer data scan-filter (2^32 combinaties) kan tot onnodige connectiepogingen leiden — deze falen alsnog bij 16-byte UUID check of ECDH handshake. Voorheen (issue #10) werd alleen op naam "SylDa_Server" gescant zonder verdere verificatie. | | **Commando vervalsing** | AES-GCM versleutelt en authenticeert command byte + nonce — wijzigen maakt tag ongeldig | | **Afluisteren** | AES-256-GCM biedt confidentiality — commando's zijn onleesbaar zonder AES-sleutel | | **Brute-force PSK** | 32-byte PSK = 2^256 mogelijkheden — onhaalbaar | @@ -177,16 +169,14 @@ De firmware kan **over-the-air** worden geüpdatet via BLE (zonder USB): Tijdens OTA zijn relais geforceerd UIT en knippert de LED groen. Na afloop (wachtend op confirm) is de LED geel. -> **Client OTA:** Clients kunnen ook OTA ontvangen. De server stuurt `BROKER_CMD_UPDATE_AVAILABLE`, de client antwoordt `BROKER_CMD_CLIENT_READY`, en start vervolgens een eigen BLE OTA-server (`SylDa_Client_OTA_XX`). De app verbindt met de client direct voor de OTA transfer. - ## Firmware De firmware is geschreven in C++ met PlatformIO en de Arduino-framework. BLE: ESP32 NimBLE library, crypto: mbedtls (ingebouwd in ESP-IDF). | Environment | Bronbestand | Rol | |---|---|---| -| `s3lite-server` | `src/main.cpp` | Ontvangt BLE-verbindingen, bedient relais, broker voor OTA-distributie | -| `s3lite-client` | `src/client.cpp` | Verbindt als BLE-central met server, stuurt relais-commando's, ontvangt OTA | +| `s3lite-server` | `src/main.cpp` | Ontvangt BLE-verbindingen, bedient relais | +| `s3lite-client` | `src/client.cpp` | Verbindt als BLE-central met server, stuurt relais-commando's | Firmwareversies: `version.txt` (single source of truth) → `src/version.h` (gegenereerd door `tools/pio_version_pre.py`) + `SylDaVersions.kt` (Android). Versiebeheer via `tools/check_versions.sh`. @@ -217,8 +207,6 @@ De touch sensoren, BLE, en interne knop werken onafhankelijk (OR-logica). De int | VERSION | `beb5483e-36e1-4688-b7f5-ea07361f26db` | READ | Firmware versie string | | OTA | `beb5483e-36e1-4688-b7f5-ea07361f26da` | WRITE_NR, NOTIFY | BLE OTA firmware update | | ECDH | `beb5483e-36e1-4688-b7f5-ea07361f26dc` | WRITE_NR, NOTIFY | ECDH sleuteluitwisseling | -| CLIENT_REGISTRY | `beb5483e-36e1-4688-b7f5-ea07361f26dd` | READ, WRITE_NR, NOTIFY | Client registratie (UUID + versie) | -| CLIENT_CONTROL | `beb5483e-36e1-4688-b7f5-ea07361f26de` | WRITE_NR, NOTIFY | Broker protocol (OTA notificaties) | ### Key provisioning @@ -261,7 +249,7 @@ Setup done. **Client firmware log (geprovisioneerd):** ``` -=== AtomS3 Lite SylDa Client v6 === +=== SylDa Client v5 === PSK loaded from NVS UUID loaded from NVS BLE: scanning... @@ -323,7 +311,6 @@ Firmware binaries: `tools/copy_firmware.sh` kopieert `.bin` bestanden naar `asse - **Key exchange geïntegreerd:** Module bijwerken = versie check → flashen (indien nodig) → PSK-uitwisseling — volledig automatisch - **Thema-selector:** Instellingen → Weergave met Systeem, Licht of Donker - **Permissies:** Vraagt BLUETOOTH_SCAN en BLUETOOTH_CONNECT. Bij weigeren wordt verwezen naar Instellingen -- **Broker protocol:** Ontvangt `CLIENT_CONTROL` notificaties van de server (OTA beschikbaar, client ready) ### Instellingenscherm @@ -346,7 +333,7 @@ De PSK en UUIDs worden opgeslagen in SharedPreferences en overleven app-herstart Vereisten: - Android SDK (API 34+) -- Java 21 (Java 26 werkt niet met Gradle) +- Java 17 target (JDK 21 voor Gradle — Java 26 werkt niet) - Gradle 8.14 (via wrapper) - AGP 8.5.2, Kotlin 2.0.21 (K2 compiler) - USB host support (raw Android USB API, geen externe serial library) @@ -401,8 +388,8 @@ Project_SylDa/ ├── version.txt # Single source of truth voor firmwareversies ├── icon.png / icon.svg # Project icon ├── src/ -│ ├── main.cpp # Server firmware (BLE + ECDH/AES + NVS + USB + touch + relais + OTA + broker) -│ ├── client.cpp # Client firmware (BLE central + ECDH/AES + touch + relais + OTA server mode) +│ ├── main.cpp # Server firmware (BLE + ECDH/AES + NVS + USB + touch + relais + OTA) +│ ├── client.cpp # Client firmware (BLE central + ECDH/AES + touch + relais) │ ├── ecdh_helper.h # ECDH crypto helper (secp256r1, AES-sleutel afleiding, pubkey HMAC) │ └── version.h # Auto-gegenereerd uit version.txt — niet handmatig bewerken ├── tools/ @@ -492,11 +479,10 @@ HMAC-SHA256 (vroegere versies) bood authenticatie maar geen confidentiality. ECD - De app gebruikt raw Android USB API (`UsbDeviceConnection.bulkTransfer`) — geen externe serial library. - `minSdk` is 26 (Android 8.0). `compileSdk`/`targetSdk` blijft op 34. - Sequence counters starten bij 1 (niet 0), wrappen bij 1.000.000. -- BLE MTU: server stelt 248 in (ESP32 NimBLE limiet), app vraagt 512. OTA/ECDH chunks passen zich aan aan `negotiatedMtu - 3`. +- BLE MTU: server stelt 248 in (ESP32 NimBLE limiet), app vraagt 512. OTA/ECDH chunks passen zich aan aan `negotiatedMtu - 7`. - Server krijgt maximaal 2 gelijktijdige BLE-clients. Client slaat server BLE-adres op in NVS voor snelle reconnect (valt terug op scan als adres niet werkt). - Server advertising bevat manufacturer data (0xFFFF) met eerste 4 bytes van device UUID voor snelle identificatie. - OTA: 500-byte chunks (aangepast aan MTU), 3 retries per chunk, SHA-256 hash verificatie, 10s 0xFF confirm timeout. -- Client in OTA server modus advertiseert als `SylDa_Client_OTA_XX` (XX = laatste 2 hex van device UUID). ## Release diff --git a/android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt b/android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt index 1f35149..f74686d 100644 --- a/android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt +++ b/android/app/src/main/java/nl/eddydevink/sylda/BleRelayManager.kt @@ -47,13 +47,7 @@ class BleRelayManager( private val VERSION_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26db") private val OTA_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26da") private val ECDH_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26dc") - private val CLIENT_REGISTRY_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26dd") - private val CLIENT_CONTROL_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26de") private val OTA_CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") - private const val BROKER_PLAIN_SIZE = 20 - private const val BROKER_PKT_SIZE = 4 + 8 + BROKER_PLAIN_SIZE + 16 - private const val BROKER_CMD_UPDATE_AVAILABLE: Byte = 0x10 - private const val BROKER_CMD_CLIENT_READY: Byte = 0x20 private const val SCAN_TIMEOUT_MS = 30000L private const val RECONNECT_DELAY_MS = 2000L @@ -89,13 +83,6 @@ class BleRelayManager( @Volatile private var ecdhDeferred: CompletableDeferred? = null private var ecdhNotifyBuf: ByteArray = ByteArray(0) private var ecdhCccdPending = false - private var registryCharacteristic: BluetoothGattCharacteristic? = null - private var controlCharacteristic: BluetoothGattCharacteristic? = null - private var brokerNotifyBuf: ByteArray = ByteArray(0) - private var brokerDeferred: CompletableDeferred? = null - private var clientUuid: ByteArray? = null - private var clientVersion: Int = 0 - private var clientStatus: Int = 0 interface OtaNotifyCallback { fun onOtaNotify(status: Int, seq: Int) @@ -292,95 +279,6 @@ class BleRelayManager( fun getNegotiatedMtu(): Int = negotiatedMtu - fun getClientUuid(): ByteArray? = clientUuid - fun getClientVersion(): Int = clientVersion - fun getClientStatus(): Int = clientStatus - - @SuppressLint("MissingPermission") - fun sendUpdateAvailable(targetUuid: ByteArray): Boolean { - val gatt = bluetoothGatt ?: return false - val key = aesKey ?: return false - val ctrlChar = controlCharacteristic ?: return false - - val plainPayload = ByteArray(BROKER_PLAIN_SIZE) - plainPayload[0] = BROKER_CMD_UPDATE_AVAILABLE - System.arraycopy(targetUuid, 0, plainPayload, 1, targetUuid.size) - - val pkt = buildBrokerPacket(plainPayload, key) - - ctrlChar.value = pkt - ctrlChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - return gatt.writeCharacteristic(ctrlChar) - } - - @SuppressLint("MissingPermission") - fun subscribeToBrokerNotifications(): Boolean { - val gatt = bluetoothGatt ?: return false - val ctrlChar = controlCharacteristic ?: return false - - if (!gatt.setCharacteristicNotification(ctrlChar, true)) { - Log.e(TAG, "Broker: setCharacteristicNotification returned false") - return false - } - val desc = ctrlChar.getDescriptor(OTA_CCCD_UUID) - if (desc != null) { - desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - if (!gatt.writeDescriptor(desc)) { - Log.w(TAG, "Broker: writeDescriptor for CCCD returned false") - } - } - return true - } - - private fun buildBrokerPacket(plainPayload: ByteArray, key: ByteArray): ByteArray { - val seqBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(seq).array() - val nonceRandom = ByteArray(8) - SecureRandom().nextBytes(nonceRandom) - - val nonce = ByteArray(GCM_NONCE_LEN) - System.arraycopy(seqBytes, 0, nonce, 0, 4) - System.arraycopy(nonceRandom, 0, nonce, 4, 8) - - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(Cipher.ENCRYPT_MODE, - SecretKeySpec(key, "AES"), - GCMParameterSpec(GCM_TAG_LEN * 8, nonce)) - - val cipherOutput = cipher.doFinal(plainPayload) - - val packet = ByteArray(BROKER_PKT_SIZE) - System.arraycopy(seqBytes, 0, packet, 0, 4) - System.arraycopy(nonceRandom, 0, packet, 4, 8) - System.arraycopy(cipherOutput, 0, packet, 12, cipherOutput.size) - return packet - } - - private fun decryptBrokerPacket(data: ByteArray, key: ByteArray): ByteArray? { - if (data.size != BROKER_PKT_SIZE) return null - - val nonce = ByteArray(GCM_NONCE_LEN) - System.arraycopy(data, 0, nonce, 0, 4) - System.arraycopy(data, 4, nonce, 4, 8) - - val ciphertext = ByteArray(BROKER_PLAIN_SIZE) - System.arraycopy(data, 12, ciphertext, 0, BROKER_PLAIN_SIZE) - - val tag = ByteArray(GCM_TAG_LEN) - System.arraycopy(data, 12 + BROKER_PLAIN_SIZE, tag, 0, GCM_TAG_LEN) - - return try { - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - val gcmSpec = GCMParameterSpec(GCM_TAG_LEN * 8, nonce) - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), gcmSpec) - cipher.update(ciphertext) - val result = cipher.doFinal(tag) - result - } catch (e: Exception) { - Log.e(TAG, "Broker: decrypt failed", e) - null - } - } - @SuppressLint("MissingPermission") private fun connectToDevice(device: android.bluetooth.BluetoothDevice) { handler.post { callbacks.onStatusChanged(BleStatus.Connecting) } @@ -438,16 +336,11 @@ class BleRelayManager( relayCharacteristic = service.getCharacteristic(CHARACTERISTIC_UUID) otaCharacteristic = service.getCharacteristic(OTA_UUID) ecdhCharacteristic = service.getCharacteristic(ECDH_UUID) - registryCharacteristic = service.getCharacteristic(CLIENT_REGISTRY_UUID) - controlCharacteristic = service.getCharacteristic(CLIENT_CONTROL_UUID) val uuidChar = service.getCharacteristic(UUID_CHAR_UUID) val versionChar = service.getCharacteristic(VERSION_UUID) if (relayCharacteristic != null && uuidChar != null) { Log.d(TAG, "Reading device UUID characteristic...") gatt.readCharacteristic(uuidChar) - if (controlCharacteristic != null) { - subscribeToBrokerNotifications() - } } else { Log.e(TAG, "Service or characteristics not found") handler.post { callbacks.onStatusChanged(BleStatus.ServiceNotFound) } @@ -514,23 +407,6 @@ class BleRelayManager( ecdhNotifyBuf = value.copyOf() ecdhDeferred?.complete(true) Log.d(TAG, "ECDH notify received (${value.size} bytes)") - } else if (characteristic.uuid == CLIENT_CONTROL_UUID) { - Log.d(TAG, "Broker: control notification received (${value.size} bytes)") - val key = aesKey - if (key != null) { - val plainPayload = decryptBrokerPacket(value, key) - if (plainPayload != null && plainPayload.size >= BROKER_PLAIN_SIZE) { - val cmd = plainPayload[0].toInt() and 0xFF - Log.d(TAG, "Broker: decrypted cmd=0x${cmd.toString(16)}") - if (cmd == BROKER_CMD_CLIENT_READY.toInt()) { - val uuid = plainPayload.copyOfRange(1, 1 + 16) - clientUuid = uuid - clientStatus = 1 - Log.d(TAG, "Broker: client ready for OTA, uuid=${uuid.joinToString("") { "%02X".format(it) }}") - handler.post { callbacks.onClientReady(uuid) } - } - } - } } } diff --git a/android/app/src/main/java/nl/eddydevink/sylda/BleStatus.kt b/android/app/src/main/java/nl/eddydevink/sylda/BleStatus.kt index 5d3dd95..ee44abd 100644 --- a/android/app/src/main/java/nl/eddydevink/sylda/BleStatus.kt +++ b/android/app/src/main/java/nl/eddydevink/sylda/BleStatus.kt @@ -19,5 +19,4 @@ enum class BleStatus { interface BleCallbacks { fun onStatusChanged(status: BleStatus) fun onFirmwareVersion(espVersion: String, appVersion: String) - fun onClientReady(uuid: ByteArray) } diff --git a/android/app/src/main/java/nl/eddydevink/sylda/MainActivity.kt b/android/app/src/main/java/nl/eddydevink/sylda/MainActivity.kt index f1181e7..e18839c 100644 --- a/android/app/src/main/java/nl/eddydevink/sylda/MainActivity.kt +++ b/android/app/src/main/java/nl/eddydevink/sylda/MainActivity.kt @@ -226,14 +226,4 @@ class MainActivity : AppCompatActivity(), BleCallbacks { } } } - - override fun onClientReady(uuid: ByteArray) { - runOnUiThread { - if (isFinishing || isDestroyed || !lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) return@runOnUiThread - Toast.makeText(this, "Client ready for OTA update", Toast.LENGTH_SHORT).show() - val intent = Intent(this, SettingsActivity::class.java) - intent.putExtra("client_ota_uuid", uuid) - settingsLauncher.launch(intent) - } - } } diff --git a/docs/superpowers/specs/2026-08-04-client-ble-ota-spec.md b/docs/superpowers/specs/2026-08-04-client-ble-ota-spec.md deleted file mode 100644 index 219c097..0000000 --- a/docs/superpowers/specs/2026-08-04-client-ble-ota-spec.md +++ /dev/null @@ -1,86 +0,0 @@ -# Spec: Client BLE OTA via Broker Server - -**Status:** Draft -**Datum:** 2026-08-04 -**Auteur:** opencore (AI Agent) - -## 1. Introductie -Dit document beschrijft de architectuur en het protocol voor het uitvoeren van een Firmware Over-The-Air (OTA) update op de SylDa_Client (ESP32) via de SylDa_Server (ESP32) als broker. - -## 2. Architectuur -Het systeem bestaat uit drie componenten: -1. **Android App:** Initiator van de update, bevat de nieuwe firmware binary. -2. **SylDa_Server (Central/Server):** Fungeert als informatiepunt (broker) tussen App en Client. -3. **SylDa_Client (Central/Client):** Ontvanger van de update. Gaat in een "Dual-Role" (Central + Server) tijdens de update procedure. - -## 3. BLE Interface (SylDa_Server) -De Server breidt zijn bestaande service (`4fafc201-1fb5-459e-8fcc-c5c9c331914b`) uit met de volgende characteristics: - -### 3.1 CLIENT_REGISTRY (`beb5483e-36e1-4688-b7f5-ea07361f26dd`) -* **Eigenschappen:** Read, Notify -* **Beveiliging:** AES/GCM encrypted -* **Payload (37 bytes totaal):** - * `[0:3]` Seq (4 bytes) - * `[4:11]` Nonce Random (8 bytes) - * `[12]` Ciphertext: `[UUID (16b) | Version (2b) | Status (1b)]` - * `[13:28]` Auth Tag (16 bytes) -* **Status codes:** - * `0x00`: Normaal / Idle - * `0x01`: Ready for OTA (Advertising) - * `0x02`: Updating (Transfer bezig) - -### 3.2 CLIENT_CONTROL (`beb5483e-36e1-4688-b7f5-ea07361f26de`) -* **Eigenschappen:** Write, Notify -* **Beveiliging:** AES/GCM encrypted -* **Payload (37 bytes totaal):** - * `[0:3]` Seq (4 bytes) - * `[4:11]` Nonce Random (8 bytes) - * `[12]` Ciphertext: `[TargetUUID (16b) | Action (1b)]` - * `[13:28]` Auth Tag (16 bytes) -* **Action codes:** - * `0x10`: `NOTIFY_UPDATE_AVAILABLE` (Server -> Client) - * `0x20`: `CLIENT_READY_FOR_OTA` (Client -> Server -> App) - -## 4. BLE Interface (SylDa_Client OTA Mode) -Wanneer de Client in OTA-modus gaat, start hij een eigen BLE Server met de volgende gegevens: -* **Device Name:** `SylDa_Client_OTA_[Last-4-chars-UUID]` -* **Service UUID:** `4fafc201-1fb5-459e-8fcc-c5c9c331914b` (Gelijk aan Server) -* **OTA Characteristic:** `beb5483e-36e1-4688-b7f5-ea07361f26da` (Gelijk aan Server) -* **UUID Characteristic:** `beb5483e-36e1-4688-b7f5-ea07361f26d9` (Bevat eigen UUID ter verificatie) - -## 5. Sequence Diagram - -1. **Bootstrapping:** - * Client verbindt met Server. - * Client schrijft zijn UUID en Versie naar `CLIENT_REGISTRY` op de Server. -2. **Update Detectie:** - * App verbindt met Server. - * App leest `CLIENT_REGISTRY` en ziet dat Client v5 draait (App heeft v6). -3. **Update Trigger:** - * App stuurt `0x10` (Update Available) naar `CLIENT_CONTROL` (Target: Client UUID). - * Server stuurt Notify naar Client. -4. **Client Voorbereiding:** - * Client ontvangt notificatie. - * Client start `NimBLEServer` en begint te adverteren. - * Client schrijft `0x20` (Ready) naar `CLIENT_CONTROL` op de Server. -5. **App Verbinding naar Client:** - * App ontvangt `0x20` notificatie van de Server. - * App (na gebruikersbevestiging) scant naar `SylDa_Client_OTA` met de juiste UUID. - * App verbindt direct met de Client. -6. **OTA Transfer:** - * App voert de standaard OTA-procedure uit (Start command, chunks, SHA256 verify, End confirm). - * Client verwerkt de OTA en voert zijn relais-taken uit zolang de transfer niet is gestart. -7. **Finalisatie:** - * Client herstart na succesvolle OTA. - * Client verbindt opnieuw met Server en rapporteert de nieuwe versie. - -## 6. Beveiliging en Foutafhandeling -* **Encryptie:** Alle "Broker" communicatie op de Server verloopt via de bestaande AES/GCM laag met de gedeelde PSK. -* **UUID Pinning:** De App MOET de UUID van de Client verifiëren via de `UUID_CHAR_UUID` voordat de OTA start. -* **Timeouts:** De Client stopt de OTA-modus (Server-rol) na een succesvolle update of een handmatige reset. (Gebruiker behoudt controle via de App). -* **Relais:** De Client blijft operationeel tot het moment dat de OTA-transfer daadwerkelijk begint. - -## 7. Rollen en Verantwoordelijkheden -* **Server:** Passief doorgeefluik van status en notificaties. Slaat geen firmware op. -* **Client:** Verantwoordelijk voor het veilig flashen van de eigen firmware en het rapporteren van de status. -* **App:** Verantwoordelijk voor het orkestreren van het proces en het leveren van de firmware. diff --git a/src/client.cpp b/src/client.cpp index ac84754..44a08e6 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -6,12 +6,11 @@ #include #include #include -#include -#include #include #include "version.h" #include "ecdh_helper.h" +#include "serial_utils.h" static const char *TAG = "client"; @@ -39,15 +38,7 @@ CRGB leds[NUM_LEDS]; #define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8" #define UUID_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d9" #define ECDH_UUID "beb5483e-36e1-4688-b7f5-ea07361f26dc" -#define CLIENT_REGISTRY_UUID "beb5483e-36e1-4688-b7f5-ea07361f26dd" -#define CLIENT_CONTROL_UUID "beb5483e-36e1-4688-b7f5-ea07361f26de" -#define VERSION_UUID "beb5483e-36e1-4688-b7f5-ea07361f26db" -#define OTA_UUID "beb5483e-36e1-4688-b7f5-ea07361f26da" #define ESP_UUID_LEN 16 -#define BROKER_PLAIN_SIZE 20 -#define BROKER_PKT_SIZE (4 + 8 + BROKER_PLAIN_SIZE + 16) -#define BROKER_CMD_UPDATE_AVAILABLE 0x10 -#define BROKER_CMD_CLIENT_READY 0x20 #define NVS_NAMESPACE "syl" #define NVS_PSK_NAME "psk" #define NVS_UUID_NAME "uuid" @@ -85,30 +76,6 @@ uint32_t connectionStableMs = 0; uint32_t relayFlashUntil = 0; bool otaInProgress = false; bool otaEnded = false; -bool otaServerMode = false; - -static NimBLEServer *pOtaServer = nullptr; -static NimBLECharacteristic *pOtaChar = nullptr; -static NimBLECharacteristic *pOtaUuidChar = nullptr; - -esp_ota_handle_t otaHandle = 0; -const esp_partition_t *otaPartition = nullptr; -uint32_t otaExpectedSize = 0; -uint32_t otaWritten = 0; -uint8_t otaExpectedSha[32]; -mbedtls_sha256_context otaShaCtx; -bool otaShaStarted = false; -bool otaReady = false; -bool otaConfirmed = false; -bool otaEarlyConfirm = false; -uint32_t otaEndedAt = 0; -#define OTA_CONFIRM_TIMEOUT_MS 10000 - -#define OTA_CMD_START 0x10 -#define OTA_STATUS_ACK 0x00 -#define OTA_STATUS_NAK 0x01 -#define OTA_STATUS_COMPLETE 0x10 -#define OTA_STATUS_ERROR 0x11 static bool bleReady = false; static bool serverFound = false; @@ -160,20 +127,6 @@ class MyClientCallbacks : public NimBLEClientCallbacks { static MyClientCallbacks clientCallbacks; -static bool isHex(const String &hex) { - for (int i = 0; i < hex.length(); i++) { - char c = hex.charAt(i); - if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return false; - } - return true; -} - -static void parseHex(const String &hex, uint8_t *out, int len) { - for (int i = 0; i < len; i++) { - out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16); - } -} - static void buildAesPacket(uint8_t cmd, uint8_t *packet) { uint32_t s = ++seq; packet[0] = (s >> 24) & 0xFF; @@ -220,182 +173,6 @@ static void disconnectClient() { memset(aesKey, 0, AES_KEY_LEN); } -static void sendOtaNotify(uint8_t status, uint32_t seq) { - if (pOtaChar == nullptr) return; - uint8_t buf[5]; - buf[0] = status; - buf[1] = (seq >> 24) & 0xFF; - buf[2] = (seq >> 16) & 0xFF; - buf[3] = (seq >> 8) & 0xFF; - buf[4] = seq & 0xFF; - pOtaChar->setValue(buf, 5); - bool ok = pOtaChar->notify(); - if (!ok) { - ESP_LOGW(TAG, "OTA: notify() failed (status=0x%02X seq=%lu)", status, seq); - } -} - -static void otaReset() { - otaInProgress = false; - otaHandle = 0; - otaPartition = nullptr; - otaExpectedSize = 0; - otaWritten = 0; - otaReady = false; - otaConfirmed = false; - otaEarlyConfirm = false; - if (otaShaStarted) { - mbedtls_sha256_free(&otaShaCtx); - otaShaStarted = false; - } -} - -static void abortOta(const char *reason) { - ESP_LOGE(TAG, "OTA: abort — %s", reason); - if (otaHandle != 0) { - esp_ota_end(otaHandle); - } - otaReset(); - sendOtaNotify(OTA_STATUS_ERROR, 0); -} - -class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks { - void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override { - std::string value = pChar->getValue(); - if (value.empty()) return; - const uint8_t *data = (const uint8_t *)value.data(); - size_t len = value.length(); - - if (len == 5 && data[0] == OTA_CMD_START) { - if (otaInProgress) { - esp_ota_end(otaHandle); - otaReset(); - } - otaExpectedSize = ((uint32_t)data[1] << 24) | ((uint32_t)data[2] << 16) | - ((uint32_t)data[3] << 8) | (uint32_t)data[4]; - otaPartition = esp_ota_get_next_update_partition(nullptr); - if (otaPartition == nullptr) { - ESP_LOGE(TAG, "OTA: no update partition found"); - sendOtaNotify(OTA_STATUS_ERROR, 0); - return; - } - esp_err_t err = esp_ota_begin(otaPartition, otaExpectedSize, &otaHandle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "OTA: begin failed (%d)", err); - sendOtaNotify(OTA_STATUS_ERROR, 0); - return; - } - otaInProgress = true; - otaWritten = 0; - mbedtls_sha256_init(&otaShaCtx); - mbedtls_sha256_starts_ret(&otaShaCtx, 0); - otaShaStarted = true; - ESP_LOGI(TAG, "OTA: started, expecting %lu bytes", otaExpectedSize); - return; - } - - if (len == 32 && otaInProgress) { - memcpy(otaExpectedSha, data, 32); - uint8_t computedSha[32]; - mbedtls_sha256_finish_ret(&otaShaCtx, computedSha); - mbedtls_sha256_free(&otaShaCtx); - otaShaStarted = false; - - ESP_LOGD(TAG, "OTA: SHA256 verify (written=%lu expected=%lu)", otaWritten, otaExpectedSize); - if (memcmp(computedSha, otaExpectedSha, 32) != 0) { - ESP_LOGE(TAG, "OTA: SHA256 mismatch — aborting"); - esp_ota_abort(otaHandle); - otaReset(); - sendOtaNotify(OTA_STATUS_ERROR, 0); - return; - } - - ESP_LOGI(TAG, "OTA: SHA256 OK, waiting for 0xFF confirm"); - otaReady = true; - sendOtaNotify(OTA_STATUS_COMPLETE, 0); - return; - } - - if (!otaInProgress) { - ESP_LOGD(TAG, "OTA: write ignored (not in progress, len=%d data0=0x%02X)", len, data[0]); - return; - } - - if (len == 1 && data[0] == 0xFF) { - ESP_LOGD(TAG, "OTA: 0xFF received (ready=%d confirmed=%d inProgress=%d)", - otaReady, otaConfirmed, otaInProgress); - if (otaReady) { - ESP_LOGI(TAG, "OTA: 0xFF confirm accepted"); - otaConfirmed = true; - } else { - ESP_LOGD(TAG, "OTA: 0xFF arrived early, deferring"); - otaEarlyConfirm = true; - } - return; - } - - if (data[0] < 0x10) { - uint32_t seq = ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) | - ((uint32_t)data[2] << 8) | (uint32_t)data[3]; - size_t dataLen = len - 4; - mbedtls_sha256_update_ret(&otaShaCtx, data + 4, dataLen); - esp_err_t err = esp_ota_write(otaHandle, data + 4, dataLen); - if (err != ESP_OK) { - ESP_LOGE(TAG, "OTA: write error at seq %lu (%d)", seq, err); - sendOtaNotify(OTA_STATUS_NAK, seq); - abortOta("write error"); - return; - } - otaWritten += dataLen; - sendOtaNotify(OTA_STATUS_ACK, seq); - } - } -}; - -static bool startOtaServer() { - if (otaServerMode) return true; - - char otaName[32]; - snprintf(otaName, sizeof(otaName), "SylDa_Client_OTA_%02X%02X", deviceUuid[0], deviceUuid[1]); - - NimBLEDevice::init(otaName); - NimBLEDevice::setMTU(248); - - pOtaServer = NimBLEDevice::createServer(); - - NimBLEService *pOtaService = pOtaServer->createService(SERVICE_UUID); - - pOtaChar = pOtaService->createCharacteristic( - OTA_UUID, - NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY - ); - pOtaChar->setCallbacks(new OtaCharacteristicCallbacks()); - - pOtaUuidChar = pOtaService->createCharacteristic( - UUID_CHAR_UUID, - NIMBLE_PROPERTY::READ - ); - pOtaUuidChar->setValue(deviceUuid, ESP_UUID_LEN); - - NimBLECharacteristic *pVerChar = pOtaService->createCharacteristic( - VERSION_UUID, - NIMBLE_PROPERTY::READ - ); - pVerChar->setValue(FIRMWARE_VERSION_CLIENT); - - pOtaService->start(); - - NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); - pAdvertising->setName(otaName); - pAdvertising->setMinInterval(32); - pAdvertising->setMaxInterval(64); - pAdvertising->start(); - - otaServerMode = true; - ESP_LOGI(TAG, "OTA: server started as %s", otaName); - return true; -} - static bool performEcdhHandshake(NimBLERemoteService *pSvc) { if (!pskProvisioned) { ESP_LOGE(TAG, "ECDH: no PSK"); @@ -531,137 +308,6 @@ static bool connectToServer() { relay1SendState = false; relay2SendState = false; ESP_LOGI(TAG, "BLE: server UUID verified, ECDH done, ready"); - - NimBLERemoteCharacteristic *pRegChar = pSvc->getCharacteristic(CLIENT_REGISTRY_UUID); - if (pRegChar == nullptr) { - ESP_LOGW(TAG, "BLE: CLIENT_REGISTRY characteristic not found"); - } else { - uint8_t plainPayload[BROKER_PLAIN_SIZE]; - memset(plainPayload, 0, BROKER_PLAIN_SIZE); - plainPayload[0] = 0x00; - memcpy(plainPayload + 1, deviceUuid, ESP_UUID_LEN); - uint16_t clientVersion = (uint16_t)atoi(FIRMWARE_VERSION_CLIENT); - plainPayload[1 + ESP_UUID_LEN] = (clientVersion >> 8) & 0xFF; - plainPayload[1 + ESP_UUID_LEN + 1] = clientVersion & 0xFF; - - uint8_t pkt[BROKER_PKT_SIZE]; - memset(pkt, 0, BROKER_PKT_SIZE); - uint32_t s = ++seq; - pkt[0] = (s >> 24) & 0xFF; - pkt[1] = (s >> 16) & 0xFF; - pkt[2] = (s >> 8) & 0xFF; - pkt[3] = s & 0xFF; - - for (int i = 0; i < 8; i++) pkt[4 + i] = (uint8_t)(esp_random() & 0xFF); - - uint8_t nonce[12]; - memcpy(nonce, pkt, 4); - memcpy(nonce + 4, pkt + 4, 8); - - mbedtls_gcm_context gcm; - mbedtls_gcm_init(&gcm); - int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, aesKey, AES_KEY_LEN * 8); - if (ret != 0) { - ESP_LOGE(TAG, "REGISTRY: setkey failed (ret=%d)", ret); - mbedtls_gcm_free(&gcm); - } else { - mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, - BROKER_PLAIN_SIZE, nonce, 12, NULL, 0, - plainPayload, pkt + 12, 16, pkt + 12 + BROKER_PLAIN_SIZE); - mbedtls_gcm_free(&gcm); - - if (pRegChar->writeValue(pkt, BROKER_PKT_SIZE, false)) { - ESP_LOGI(TAG, "REGISTRY: reported uuid + version %d to server", FIRMWARE_VERSION_CLIENT); - } else { - ESP_LOGW(TAG, "REGISTRY: write failed"); - } - } - } - - NimBLERemoteCharacteristic *pControlChar = pSvc->getCharacteristic(CLIENT_CONTROL_UUID); - if (pControlChar == nullptr) { - ESP_LOGW(TAG, "BLE: CLIENT_CONTROL characteristic not found"); - } else { - if (!pControlChar->subscribe(true, [pControlChar](NimBLERemoteCharacteristic *pChar, const uint8_t *data, size_t len, bool isNotify) { - if (len != BROKER_PKT_SIZE) { - ESP_LOGW(TAG, "BROKER: bad notify length %d", (int)len); - return; - } - - uint8_t nonce[12]; - memcpy(nonce, data, 4); - memcpy(nonce + 4, data + 4, 8); - - uint8_t plainPayload[BROKER_PLAIN_SIZE]; - uint8_t ciphertext[BROKER_PLAIN_SIZE]; - memcpy(ciphertext, data + 12, BROKER_PLAIN_SIZE); - - mbedtls_gcm_context gcm; - mbedtls_gcm_init(&gcm); - int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, aesKey, AES_KEY_LEN * 8); - if (ret != 0) { - ESP_LOGE(TAG, "BROKER: setkey failed (ret=%d)", ret); - mbedtls_gcm_free(&gcm); - return; - } - - ret = mbedtls_gcm_auth_decrypt(&gcm, - BROKER_PLAIN_SIZE, nonce, 12, NULL, 0, - data + 12 + BROKER_PLAIN_SIZE, 16, ciphertext, plainPayload); - mbedtls_gcm_free(&gcm); - - if (ret != 0) { - ESP_LOGE(TAG, "BROKER: AES-GCM decrypt failed (ret=%d)", ret); - return; - } - - uint8_t cmd = plainPayload[0]; - ESP_LOGI(TAG, "BROKER: received cmd 0x%02X", cmd); - - if (cmd == BROKER_CMD_UPDATE_AVAILABLE) { - ESP_LOGI(TAG, "BROKER: update available, starting OTA server"); - startOtaServer(); - - uint8_t clientUuid[ESP_UUID_LEN]; - memcpy(clientUuid, plainPayload + 1, ESP_UUID_LEN); - - uint8_t plainResp[BROKER_PLAIN_SIZE]; - memset(plainResp, 0, BROKER_PLAIN_SIZE); - plainResp[0] = BROKER_CMD_CLIENT_READY; - memcpy(plainResp + 1, clientUuid, ESP_UUID_LEN); - - uint8_t pkt[BROKER_PKT_SIZE]; - memset(pkt, 0, BROKER_PKT_SIZE); - uint32_t s = ++seq; - pkt[0] = (s >> 24) & 0xFF; - pkt[1] = (s >> 16) & 0xFF; - pkt[2] = (s >> 8) & 0xFF; - pkt[3] = s & 0xFF; - - for (int i = 0; i < 8; i++) pkt[4 + i] = (uint8_t)(esp_random() & 0xFF); - - uint8_t nonceResp[12]; - memcpy(nonceResp, pkt, 4); - memcpy(nonceResp + 4, pkt + 4, 8); - - mbedtls_gcm_context gcmResp; - mbedtls_gcm_init(&gcmResp); - ret = mbedtls_gcm_setkey(&gcmResp, MBEDTLS_CIPHER_ID_AES, aesKey, AES_KEY_LEN * 8); - if (ret == 0) { - mbedtls_gcm_crypt_and_tag(&gcmResp, MBEDTLS_GCM_ENCRYPT, - BROKER_PLAIN_SIZE, nonceResp, 12, NULL, 0, - plainResp, pkt + 12, 16, pkt + 12 + BROKER_PLAIN_SIZE); - mbedtls_gcm_free(&gcmResp); - - pControlChar->writeValue(pkt, BROKER_PKT_SIZE, false); - ESP_LOGI(TAG, "BROKER: sent CLIENT_READY to server"); - } - } - }, true)) { - ESP_LOGW(TAG, "BROKER: subscribe to CLIENT_CONTROL failed"); - } - } - return true; } @@ -871,46 +517,9 @@ void loop() { bool relay1State = !t1Active || btnActive; bool relay2State = !t2Active; - if (otaServerMode && otaReady) { - esp_err_t err = esp_ota_end(otaHandle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "OTA: esp_ota_end failed (%d) — aborting", err); - otaReset(); - } else { - ESP_LOGI(TAG, "OTA: esp_ota_end OK, waiting for 0xFF confirm"); - otaEnded = true; - otaEndedAt = millis(); - } - otaReady = false; - } - - if (otaServerMode && otaEnded && otaEarlyConfirm) { - ESP_LOGD(TAG, "OTA: processing deferred 0xFF confirm"); - otaConfirmed = true; - otaEarlyConfirm = false; - } - - if (otaServerMode && otaEnded && !otaConfirmed && (millis() - otaEndedAt > OTA_CONFIRM_TIMEOUT_MS)) { - ESP_LOGW(TAG, "OTA: 0xFF confirm timeout (%lu ms) — discarding update", millis() - otaEndedAt); - otaReset(); - } - - if (otaServerMode && otaConfirmed) { - ESP_LOGI(TAG, "OTA: confirmed, setting boot partition"); - esp_err_t err = esp_ota_set_boot_partition(otaPartition); - if (err != ESP_OK) { - ESP_LOGE(TAG, "OTA: set boot partition failed (%d)", err); - } else { - ESP_LOGI(TAG, "OTA: done, rebooting..."); - delay(500); - esp_restart(); - } - otaReset(); - } - CRGB ledColor; - if (otaServerMode || otaInProgress) { + if (otaInProgress) { ledColor = otaEnded ? CRGB::Yellow : (((millis() / 100) % 2) ? CRGB::Green : CRGB::Black); diff --git a/src/main.cpp b/src/main.cpp index a6f3bcf..990d880 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include #include "version.h" #include "ecdh_helper.h" +#include "serial_utils.h" static const char *TAG = "server"; @@ -46,13 +47,6 @@ CRGB leds[NUM_LEDS]; #define VERSION_UUID "beb5483e-36e1-4688-b7f5-ea07361f26db" #define OTA_UUID "beb5483e-36e1-4688-b7f5-ea07361f26da" #define ECDH_UUID "beb5483e-36e1-4688-b7f5-ea07361f26dc" -#define CLIENT_REGISTRY_UUID "beb5483e-36e1-4688-b7f5-ea07361f26dd" -#define CLIENT_CONTROL_UUID "beb5483e-36e1-4688-b7f5-ea07361f26de" - -#define BROKER_CMD_UPDATE_AVAILABLE 0x10 -#define BROKER_CMD_CLIENT_READY 0x20 -#define BROKER_PLAIN_SIZE 20 -#define BROKER_PKT_SIZE (4 + 8 + BROKER_PLAIN_SIZE + 16) #define OTA_CHUNK_SIZE 500 #define OTA_CMD_START 0x10 @@ -87,20 +81,6 @@ struct ClientState { }; ClientState clients[MAX_CLIENTS]; -#define BROKER_STATUS_IDLE 0x00 -#define BROKER_STATUS_READY 0x01 -#define BROKER_STATUS_UPDATING 0x02 - -struct RemoteClientInfo { - uint8_t uuid[ESP_UUID_LEN]; - uint16_t version; - uint8_t status; - bool active; -}; -RemoteClientInfo remoteClients[MAX_CLIENTS]; -NimBLECharacteristic *pRegistryChar = nullptr; -NimBLECharacteristic *pControlChar = nullptr; - uint8_t psk[PSK_LEN]; uint8_t deviceUuid[ESP_UUID_LEN]; bool pskProvisioned = false; @@ -173,10 +153,6 @@ class MyServerCallbacks : public NimBLEServerCallbacks { clients[i].ecdhReady = false; memset(clients[i].aesKey, 0, AES_KEY_LEN); clients[i].ecdh.free(); - remoteClients[i].active = false; - remoteClients[i].version = 0; - remoteClients[i].status = BROKER_STATUS_IDLE; - memset(remoteClients[i].uuid, 0, ESP_UUID_LEN); connHandles[i] = 0xFFFF; connectedCount--; ESP_LOGI(TAG, "BLE: disconnected (slot=%d, clients=%d/%d, mac=%s)", @@ -236,20 +212,6 @@ static bool decryptAesGcm(const uint8_t *packet, int idx, uint8_t *outCmd) { return true; } -static bool isHex(const String &hex) { - for (int i = 0; i < hex.length(); i++) { - char c = hex.charAt(i); - if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return false; - } - return true; -} - -static void parseHex(const String &hex, uint8_t *out, int len) { - for (int i = 0; i < len; i++) { - out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16); - } -} - static void otaReset() { otaInProgress = false; otaHandle = 0; @@ -390,193 +352,6 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks { } }; -static void sendBrokerNotify(int clientIdx, uint8_t cmd, const uint8_t *data, size_t dataLen) { - if (clientIdx < 0 || clientIdx >= MAX_CLIENTS || !connActive[clientIdx]) return; - - uint8_t plainPayload[BROKER_PLAIN_SIZE]; - memset(plainPayload, 0, BROKER_PLAIN_SIZE); - plainPayload[0] = cmd; - memcpy(plainPayload + 1, data, dataLen); - - uint8_t pkt[BROKER_PKT_SIZE]; - memset(pkt, 0, BROKER_PKT_SIZE); - uint32_t s = 0; - pkt[0] = (s >> 24) & 0xFF; - pkt[1] = (s >> 16) & 0xFF; - pkt[2] = (s >> 8) & 0xFF; - pkt[3] = s & 0xFF; - - for (int i = 0; i < 8; i++) pkt[4 + i] = (uint8_t)(esp_random() & 0xFF); - - uint8_t nonce[12]; - memcpy(nonce, pkt, 4); - memcpy(nonce + 4, pkt + 4, 8); - - mbedtls_gcm_context gcm; - mbedtls_gcm_init(&gcm); - int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, clients[clientIdx].aesKey, AES_KEY_LEN * 8); - if (ret != 0) { - ESP_LOGE(TAG, "BROKER: send notify setkey failed"); - mbedtls_gcm_free(&gcm); - return; - } - - mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, - BROKER_PLAIN_SIZE, nonce, 12, NULL, 0, - plainPayload, pkt + 12, 16, pkt + 12 + BROKER_PLAIN_SIZE); - mbedtls_gcm_free(&gcm); - - pControlChar->setValue(pkt, BROKER_PKT_SIZE); - pControlChar->notify(connHandles[clientIdx], true); -} - -class RegistryCharacteristicCallbacks : public NimBLECharacteristicCallbacks { - void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override { - if (otaInProgress) return; - - std::string value = pChar->getValue(); - if (value.empty()) return; - const uint8_t *data = (const uint8_t *)value.data(); - size_t len = value.length(); - - if (len != BROKER_PKT_SIZE) { - ESP_LOGW(TAG, "REGISTRY: bad packet length %d (expected %d)", (int)len, BROKER_PKT_SIZE); - return; - } - - int idx = findClientIdx(connInfo.getConnHandle()); - if (idx < 0 || !clients[idx].ecdhReady) { - ESP_LOGW(TAG, "REGISTRY: write rejected — no ECDH (handle=%d)", connInfo.getConnHandle()); - return; - } - - uint8_t nonce[12]; - memcpy(nonce, data, 4); - memcpy(nonce + 4, data + 4, 8); - - uint8_t plainPayload[BROKER_PLAIN_SIZE]; - uint8_t ciphertext[BROKER_PLAIN_SIZE]; - memcpy(ciphertext, data + 12, BROKER_PLAIN_SIZE); - - mbedtls_gcm_context gcm; - mbedtls_gcm_init(&gcm); - int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, clients[idx].aesKey, AES_KEY_LEN * 8); - if (ret != 0) { - ESP_LOGE(TAG, "REGISTRY: setkey failed (ret=%d)", ret); - mbedtls_gcm_free(&gcm); - return; - } - - ret = mbedtls_gcm_auth_decrypt(&gcm, - BROKER_PLAIN_SIZE, nonce, 12, NULL, 0, - data + 12 + BROKER_PLAIN_SIZE, 16, ciphertext, plainPayload); - mbedtls_gcm_free(&gcm); - - if (ret != 0) { - ESP_LOGE(TAG, "REGISTRY: AES-GCM decrypt failed (ret=%d)", ret); - return; - } - - uint8_t cmd = plainPayload[0]; - if (cmd != 0x00) { - ESP_LOGW(TAG, "REGISTRY: unexpected cmd 0x%02X (expected 0x00)", cmd); - return; - } - - memcpy(remoteClients[idx].uuid, plainPayload + 1, ESP_UUID_LEN); - remoteClients[idx].version = ((uint16_t)plainPayload[1 + ESP_UUID_LEN] << 8) | - (uint16_t)plainPayload[1 + ESP_UUID_LEN + 1]; - remoteClients[idx].status = BROKER_STATUS_IDLE; - remoteClients[idx].active = true; - - ESP_LOGI(TAG, "REGISTRY: client registered (slot=%d, uuid=%02X%02X%02X%02X, ver=%d)", - idx, - remoteClients[idx].uuid[0], remoteClients[idx].uuid[1], - remoteClients[idx].uuid[2], remoteClients[idx].uuid[3], - remoteClients[idx].version); - } -}; - -class ControlCharacteristicCallbacks : public NimBLECharacteristicCallbacks { - void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override { - if (otaInProgress) return; - - std::string value = pChar->getValue(); - if (value.empty()) return; - const uint8_t *data = (const uint8_t *)value.data(); - size_t len = value.length(); - - if (len != BROKER_PKT_SIZE) { - ESP_LOGW(TAG, "BROKER: bad packet length %d (expected %d)", (int)len, BROKER_PKT_SIZE); - return; - } - - int idx = findClientIdx(connInfo.getConnHandle()); - if (idx < 0 || !clients[idx].ecdhReady) { - ESP_LOGW(TAG, "BROKER: write rejected — no ECDH (handle=%d)", connInfo.getConnHandle()); - return; - } - - uint8_t nonce[12]; - memcpy(nonce, data, 4); - memcpy(nonce + 4, data + 4, 8); - - uint8_t plainPayload[BROKER_PLAIN_SIZE]; - uint8_t ciphertext[BROKER_PLAIN_SIZE]; - memcpy(ciphertext, data + 12, BROKER_PLAIN_SIZE); - - mbedtls_gcm_context gcm; - mbedtls_gcm_init(&gcm); - int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, clients[idx].aesKey, AES_KEY_LEN * 8); - if (ret != 0) { - ESP_LOGE(TAG, "BROKER: setkey failed (ret=%d)", ret); - mbedtls_gcm_free(&gcm); - return; - } - - ret = mbedtls_gcm_auth_decrypt(&gcm, - BROKER_PLAIN_SIZE, nonce, 12, NULL, 0, - data + 12 + BROKER_PLAIN_SIZE, 16, ciphertext, plainPayload); - mbedtls_gcm_free(&gcm); - - if (ret != 0) { - ESP_LOGE(TAG, "BROKER: AES-GCM decrypt failed (ret=%d)", ret); - return; - } - - uint8_t cmd = plainPayload[0]; - ESP_LOGI(TAG, "BROKER: received cmd 0x%02X from slot %d", cmd, idx); - - if (cmd == BROKER_CMD_UPDATE_AVAILABLE) { - uint8_t targetUuid[ESP_UUID_LEN]; - memcpy(targetUuid, plainPayload + 1, ESP_UUID_LEN); - - for (int i = 0; i < MAX_CLIENTS; i++) { - if (remoteClients[i].active && - memcmp(remoteClients[i].uuid, targetUuid, ESP_UUID_LEN) == 0) { - ESP_LOGI(TAG, "BROKER: forwarding update notification to client slot %d", i); - sendBrokerNotify(i, BROKER_CMD_UPDATE_AVAILABLE, targetUuid, ESP_UUID_LEN); - break; - } - } - } else if (cmd == BROKER_CMD_CLIENT_READY) { - uint8_t clientUuid[ESP_UUID_LEN]; - memcpy(clientUuid, plainPayload + 1, ESP_UUID_LEN); - - ESP_LOGI(TAG, "BROKER: client ready for OTA"); - for (int i = 0; i < MAX_CLIENTS; i++) { - if (remoteClients[i].active && - memcmp(remoteClients[i].uuid, clientUuid, ESP_UUID_LEN) == 0) { - remoteClients[i].status = BROKER_STATUS_READY; - break; - } - } - } else { - ESP_LOGW(TAG, "BROKER: unknown cmd 0x%02X", cmd); - } - } -}; - class EcdhCharacteristicCallbacks : public NimBLECharacteristicCallbacks { void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override { if (otaInProgress) return; @@ -712,10 +487,6 @@ void setup() { for (int i = 0; i < MAX_CLIENTS; i++) { clients[i].ecdhReady = false; memset(clients[i].aesKey, 0, AES_KEY_LEN); - remoteClients[i].active = false; - remoteClients[i].version = 0; - remoteClients[i].status = BROKER_STATUS_IDLE; - memset(remoteClients[i].uuid, 0, ESP_UUID_LEN); } pinMode(TOUCH1_PIN, INPUT_PULLUP); @@ -765,17 +536,7 @@ void setup() { ); pOtaChar->setCallbacks(new OtaCharacteristicCallbacks()); - pRegistryChar = pService->createCharacteristic( - CLIENT_REGISTRY_UUID, - NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY - ); - pRegistryChar->setCallbacks(new RegistryCharacteristicCallbacks()); - - pControlChar = pService->createCharacteristic( - CLIENT_CONTROL_UUID, - NIMBLE_PROPERTY::WRITE_NR | NIMBLE_PROPERTY::NOTIFY - ); - pControlChar->setCallbacks(new ControlCharacteristicCallbacks()); + pService->start(); NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); pAdvertising->setName("SylDa_Server"); diff --git a/src/serial_utils.h b/src/serial_utils.h new file mode 100644 index 0000000..6ca1137 --- /dev/null +++ b/src/serial_utils.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +static inline bool isHex(const String &hex) { + for (int i = 0; i < hex.length(); i++) { + char c = hex.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return false; + } + return true; +} + +static inline void parseHex(const String &hex, uint8_t *out, int len) { + for (int i = 0; i < len; i++) { + out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16); + } +}