Project_SylDa/codereview.md

16 KiB
Raw Blame History

Code Review — Sylvia Touch Relay Android App

Datum: 26 juli 2026 Reviewer: opencode (GLM-5.1) Scope: Volledige Android app (android/)


Samenvatting

De Android app voor de Sylvia Touch Relay is een goed functionerende BLE-client met HMAC-authenticatie, USB key provisioning en firmware flashing. De codebase is compact (~1300 regels Kotlin) en doet wat het moet doen. Er zijn echter enkele significante verbeterpunten rondom beveiliging, resource management, error handling en code organisatie.

Beoordeling: Functioneel en bruikbaar, met aandachtspunten voor productie-kwaliteit.


Bevindingen per categorie

1. Beveiliging

1.1 HMAC Key opslag in SharedPreferences (BELANGRIJK)

Bestand: SettingsActivity.kt:162, UsbKeyReceiver.kt:215,252

De HMAC key wordt opgeslagen in plain-text SharedPreferences. Dit is het primaire beveiligingsmechanisme van de app en zou met EncryptedSharedPreferences (Jetpack Security) beschermd moeten worden. Op een geroot device is de key nu triviaal uit te lezen via de prefs XML file.

Aanbeveling: Gebruik androidx.security:security-crypto met EncryptedSharedPreferences:

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()
val prefs = EncryptedSharedPreferences.create(
    context, PREFS_NAME, masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

1.2 Sequence counter niet gepersisteerd (MEDIUM)

Bestand: MainActivity.kt:53

private var seq = 1

De sequence counter reset naar 1 bij elke app-herstart. Na herstart kan de app een sequence sturen die lager is dan de laatste sequence die de ESP heeft ontvangen. De ESP's lastSeq reset ook bij reconnect, dus dit is alleen een probleem als de app herstart zonder BLE disconnect. In de praktijk valt dit mee omdat de ESP bij elke nieuwe BLE-verbinding de lastSeq reset. Maar persistentie van seq zou de replay-bescherming robuuster maken.

Aanbeveling: Overweeg de sequence counter in SharedPreferences te persisten, óf документeer dat de ESP-side reset het dekt.

1.3 Log-aanvallen lekken key-materiaal (BELANGRIJK)

Bestand: UsbKeyReceiver.kt:216,229

Log.d(TAG, "HMAC key saved: $upperKey")
Log.d(TAG, "Generated key: $newKey")

De volledige HMAC key wordt naar logcat gelogd op DEBUG-niveau. Dit is een beveiligingslek: elke app met READ_LOGS of ADB-toegang kan deze key uitlezen.

Aanbeveling: Verwijder deze log statements of vervang door: Log.d(TAG, "HMAC key saved (${upperKey.length} chars)").


2. Resource Management & Memory Leaks

2.1 BLE scan callback niet afgehandeld bij geen permissies (MEDIUM)

Bestand: MainActivity.kt:227-230

if (!hasPermissions()) {
    requestPermissions()
    return
}

Als permissies geweigerd worden en de gebruiker keert terug naar de app, wordt startScan() niet opnieuw aangeroepen. De app blijft hangen in "Geen permissies" zonder feedback.

Aanbeveling: Bij geweigerde permissies in onRequestPermissionsResult, toon een uitleg of stuur de gebruiker naar app-instellingen.

2.2 Meerdere geneste Handler.postDelayed zonder cancel (MEDIUM)

Bestand: MainActivity.kt:89,147,157

Er zijn meerdere handler.postDelayed calls die BLE scans starten. Als onResume() meerdere keren wordt aangeroepen (bijv. bij snelle activity-transities), kunnen er meerdere scan-timers lopen. De isScanning guard helpt, maar dequeued niet de overtollige callbacks.

Aanbeveling: Gebruik een specifieke Runnable in plaats van lambda's zodat deze met handler.removeCallbacks(runnable) afgebroken kunnen worden, óf gebruik een flag die ook pending scans voorkomt.

2.3 SettingsActivity receivers niet veilig geregistreerd op API 14+ (LAAG)

Bestand: SettingsActivity.kt:115-117

De RECEIVER_NOT_EXPORTED flag wordt gebruikt voor usbPermissionReceiver met een custom action ACTION_USB_PERMISSION. Dit is correct voor custom actions. Echter, de usbAttachedReceiver en usbDetachedReceiver zouden met RECEIVER_NOT_EXPORTED problemen kunnen geven omdat systeem-broadcasts (zoals ACTION_USB_DEVICE_ATTACHED) exported moeten zijn om ontvangen te worden.

Aanbeveling: Gebruik RECEIVER_EXPORTED voor systeem-broadcasts (ACTION_USB_DEVICE_ATTACHED/DETACHED) en RECEIVER_NOT_EXPORTED voor de custom ACTION_USB_PERMISSION.

2.4 ProgressDialog is deprecated (MEDIUM)

Bestand: SettingsActivity.kt:153,204,258,272

ProgressDialog is deprecated sinds API level 26. Het werkt nog, maar wordt niet meer onderhouden en kan UI inconsistency veroorzaken op nieuwe Android versies.

Aanbeveling: Vervang door AlertDialog met een ProgressBar of MaterialAlertDialogBuilder met een custom layout.


3. Concurrency & Threading

3.1 Background threads zonder lifecycle-awareness (BELANGRIJK)

Bestand: SettingsActivity.kt:155-178

Thread {
    val result = readKeyFromEsp(usbManager, device)
    if (isFinishing || isDestroyed) return@Thread
    handler.post { ... }
}.start()

De isFinishing || isDestroyed check is goed, maar komt ná een potentiële 15-seconden USB operatie. Als de Activity vernietigd wordt tijdens de USB-leesoperatie, draait de thread nutteloos door en wordt er nog USB I/O gedaan op een vernietigde context.

Aanbeveling: Gebruik coroutines met lifecycleScope voor betere cancellation:

lifecycleScope.launch(Dispatchers.IO) {
    val result = readKeyFromEsp(usbManager, device)
    withContext(Dispatchers.Main) { ... }
}

3.2 EspFlasher thread kan niet geannuleerd worden (MEDIUM)

Bestand: EspFlasher.kt:29-37

De flasher draait in een raw Thread die niet annuleerbaar is. Als de gebruiker de Activity verlaat tijdens het flashen (wat de setCancelable(false) probeert te voorkomen), draait de thread door.

Aanbeveling: Overweeg een cancelable mechanisme of gebruik coroutines.

3.3 BLE writes zonder acknowledgment fallback (LAAG) — AFGESLOTEN, bewuste keuze

Bestand: MainActivity.kt:277-289

WRITE_TYPE_NO_RESPONSE betekent dat er geen bevestiging komt van de ESP. Als de packet verloren gaat, heeft de app geen retry mechanisme. De knop-toestand wijzigt wel ("ON"), maar het relais schakelt niet.

BESLUIT: Afgewezen om dezelfde reden als 6.1 — ACK-based writes veroorzaken dat loslaten wordt genegeerd als de ACK te lang duurt, waardoor het relais blijft hangen. Zie Ontwerpbeslissingen in README.


4. Code Organisatie & Onderhoudbaarheid

4.1 UsbKeyReceiver dupliceert SettingsActivity logica (BELANGRIJK)

Bestand: UsbKeyReceiver.kt vs SettingsActivity.kt:314-395

UsbKeyReceiver bevat een volledige kopie van de CDC key-exchange logica die ook in SettingsActivity.readKeyFromEsp() staat. Beide implementeren:

  • CDC interface finding
  • CDC control setup (line coding, DTR/RTS)
  • KEY? command + response parsing
  • SET_KEY command + OK verification
  • Random key generation

Daarnaast heeft UsbKeyReceiver zijn eigen kopie van findCdcDataInterface, findBulkEndpoint, en findCdcControlInterface — deze bestaan ook al in UsbUtils.

Aanbeveling: Extracteer de key-exchange logica naar een gedeelde class (bijv. UsbKeyExchange) die door beide callers gebruikt wordt. Gebruik UsbUtils consequent.

4.2 UsbKeyReceiver niet geregistreerd in AndroidManifest (MEDIUM)

Bestand: UsbKeyReceiver.kt — niet in AndroidManifest.xml

UsbKeyReceiver is defined als een BroadcastReceiver maar wordt nergens geregistreerd (noch statisch in AndroidManifest, noch dynamisch in een Activity). Het is dode code.

Aanbeveling: Of registreer de receiver (dynamisch in MainActivity of statisch in Manifest met intent-filter voor ACTION_USB_DEVICE_ATTACHED), of verwijder de class als de functionaliteit via SettingsActivity voldoende is.

4.3 Magic numbers en hardcoded waarden (LAAG)

Bestand: MainActivity.kt:43-44, SettingsActivity.kt:34-37

Constants zoals SCAN_TIMEOUT_MS, ESP_VENDOR_ID, KEY_TIMEOUT_MS zijn gedefinieerd — dit is goed. Maar er zijn ook ongedefinieerde magic numbers:

  • MainActivity.kt:250 — SCAN_TIMEOUT_MS (gedefinieerd, goed)
  • SettingsActivity.kt:334 — retry count 3 als literal
  • SettingsActivity.kt:336,347,373 — Thread.sleep(500) hardcoded
  • UsbSerialTransport.kt:16,22 — timeout 5000 hardcoded als magic number

Aanbeveling: Definieer deze als constants in de companion objects.

4.4 getString() niet gebruikt voor user-facing tekst (LAAG)

Bestand: meerdere — bijv. MainActivity.kt:155,234,245, SettingsActivity.kt:222,237

User-facing strings zijn hardcoded in Kotlin:

statusText.text = "Geen HMAC sleutel - open Instellingen"
statusText.text = "Verbinden..."

strings.xml bevat slechts 5 entries die grotendeels ongebruikt zijn.

Aanbeveling: Verplaats alle user-facing strings naar strings.xml en gebruik getString(R.string.xxx) voor lokalisatie-ondersteuning en consistentie.

4.5 Geen proguard rules voor de flasher library (LAAG)

Bestand: app/build.gradle:26

minifyEnabled false voor release. Als men in de toekomst minify aanzet, kan de esp32-flash-lib obfuscation-breuk lijden zonder proguard rules.

Aanbeveling: Voeg proguard rules toe voor org.dkaukov.esp32.* wanneer minify geactiveerd wordt.


5. Error Handling

5.1 Geen handling voor BLE write failure (BELANGRIJK)

Bestand: MainActivity.kt:284-287

char.value = packet
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(char)
seq++

Op Android API < 13 retourneert writeCharacteristic() een boolean die succes/falen aangaf. Op API 13+ is het callback-based. Er wordt geen onCharacteristicWrite callback geïmplementeerd in gattCallback. Als de write faalt (bijv. characteristic niet beschikbaar, buffer vol), wordt de sequence counter toch opgehoogd, wat leidt tot desync.

Aanbeveling: Implementeer onCharacteristicWrite callback en verhoog seq pas na bevestiging.

5.2 GATT status codes niet gecontroleerd (MEDIUM)

Bestand: MainActivity.kt:73,94,100

override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {

De status parameter wordt genegeerd. Wanneer status != GATT_SUCCESS, betekent dit dat de verbinding onverwacht verbroken is (niet een normale disconnect). De app behandelt en behandelt alle disconnects hetzelfde (reconnect na 2s), maar logt niet wat de oorzaak was.

Aanbeveling: Log de status code bij faalsituaties:

if (status != BluetoothGatt.GATT_SUCCESS) {
    Log.w(TAG, "GATT error: $status, state: $newState")
}

5.3 Broakkoppel exception swallowing (LAAG)

Bestand: SettingsActivity.kt:123-125, 393

try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {}
try { connection.close() } catch (e: Exception) {}

Exceptions worden leeg gevangen. Dit kan bugs maskeren.

Aanbeveling: Log minimaal op WARNING niveau:

try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {
    Log.w(TAG, "Failed to unregister receiver", e)
}

6. UI & UX

6.1 Knop-state niet gesynchroniseerd met relay-state (MEDIUM) — AFGESLOTEN, bewuste keuze

Bestand: MainActivity.kt:207-222

De knop tekst wijzigt naar "ON" bij ACTION_DOWN en "HOLD" bij ACTION_UP, ongeacht of het BLE-commando daadwerkelijk is aangekomen. Gezien het gebruik van WRITE_TYPE_NO_RESPONSE is er geen bevestiging.

Oorspronkelijke aanbeveling: Geef visuele feedback die onderscheid maakt tussen "commando verzonden" en "relais bevestigd" — bijv. een dimmed state of pending indicator.

BESLUIT: Afgewezen. Eerdere versies gebruikten WRITE_TYPE_DEFAULT (met ACK), maar de BLE write acknowledgement duurde lang genoeg dat de gebruiker de knop kon loslaten vóór de ACK arriveerde. Bij ACTION_UP tijdens een pending write werd het loslaten genegeerd en bleven het relais en de app hangen in de ON-status. WRITE_TYPE_NO_RESPONSE verwijdert die latency. De UI reflecteert de gebruikersintentie in plaats van bevestigde relay-state — een aanvaardbare afweging. Gedocumenteerd in code (MainActivity.kt) en README (Ontwerpbeslissingen).

6.2 Geen dark mode support (LAAG) — AFGESLOTEN

Bestand: AndroidManifest.xml:17

De app forceert Light theme, ook op devices in dark mode.

Oplossing: Overgeschakeld naar Theme.AppCompat.DayNight met teal accent-kleuren. Thema-selector in Instellingen met 3 opties: Systeem (volgt telefoon), Licht, Donker. Gekozen voor de best-practice aanpak: geen custom font-slider of contrast-kleurenkiezer — Android's systeem-lettergrootte en hoog-contrast instellingen zijn al voldoende voor de doelgroep.

6.3 Geen visuele indicator voor scan timeout (LAAG)

Bestand: MainActivity.kt:264-265

Wanneer de scan timeout bereikt wordt:

if (scanTimedOut) {
    statusText.text = "Device not found"
}

Er is geen retry-knop of automatische retry. De gebruiker moet de app herstarten.

Aanbeveling: Voeg een "Opnieuw scannen" knop toe of retry automatisch na 5 seconden.


7. Build & Dependencies

7.1 Verouderde AGP versie (MEDIUM)

Bestand: android/build.gradle:2

id 'com.android.application' version '8.2.2' apply false

AGP 8.2.2 is van begin 2024. De nieuwste is 8.5+. Nieuwere versies bevatten bug fixes en betere Kotlin support.

Aanneveling: Upgrade AGP naar 8.5+ (compatibel met Gradle 8.14).

7.2 compileSdk 34, niet 35 (LAAG) — AFGESLOTEN, bewuste keuze

Bestand: app/build.gradle:8,13

compileSdk en targetSdk zijn 34 (Android 14). Android 15 (API 35) is uitgebracht.

BESLUIT: Afgewezen. De doelgroep bestaat uit lichamelijk gehandicapte gebruikers die zeer terughoudend zijn met het updaten van hun telefoon. API 34 maximaliseert device-compatibiliteit. Gedocumenteerd in build.gradle en README (Ontwerpbeslissingen).

7.3 Kotlin 1.9.24, niet 2.x (LAAG) — AFGESLOTEN, geüpgraded

Kotlin is geüpgraded naar 2.0.21 (K2 compiler).


8. Firmware / App inconsistenties

8.1 Line coding byte-array verschilt (MEDIUM)

Bestand: SettingsActivity.kt:430-433 vs UsbKeyReceiver.kt:164

// SettingsActivity:
byteArrayOf(0x00, 0xC2.toByte(), 0x01, 0x00, 0x00, 0x08, 0x08)
// UsbKeyReceiver:
byteArrayOf(0x00, 0xC2.toByte(), 0x01, 0x00, 0x00, 0x00, 0x08)

Het 6e byte verschilt: 0x08 (SettingsActivity) vs 0x00 (UsbKeyReceiver). De juiste CDC line coding is 115200 8N1:

  • Byte 0-3: baudrate little-endian (115200 = 0x00C20000 → 0x00, 0xC2, 0x01, 0x00)
  • Byte 4: stop bits (0 = 1 stop bit)
  • Byte 5: parity (0 = none)
  • Byte 6: data bits (8)

UsbKeyReceiver heeft de juiste waarde (0x00 voor parity). SettingsActivity heeft 0x08 — dit zegt parity=space. Het werkt waarschijnlijk omdat de ESP32-S3 CDC deze instellingen negeert, maar het is inconsistent.

Aanbeveling: Fix SettingsActivity.kt:432 naar 0x00 en extracteer naar een gedeelde functie.


Prioritering

Prioriteit Aantal Actie
Belangrijk 5 Alle 5 opgelost
MEDIUM 8 Alle 8 opgelost
LAAG 8 Alle 8 opgelost / afgesloten

Top 5 aanbevelingen (snelste impact)

  1. Verwijder key logging (UsbKeyReceiver.kt:216,229) — 2 regels, direct beveiligingswinst
  2. Schakel over naar EncryptedSharedPreferences — beschermt de HMAC key op device
  3. Consolideer UsbKeyReceiver — 318 regels dode code of duplicatie; verwijderen of integreren
  4. Fix line coding byte (SettingsActivity.kt:432) — consistentie bug
  5. Implementeer onCharacteristicWrite — voorkomt sequence desync

Conclusie

De app is solide voor hobbyist-gebruik en bevat doordachte beveiligingsarchitectuur (HMAC, sequence counters, USB key provisioning). Voor productie-kwaliteit zijn de belangrijkste punten:

  • Beveiliging: Key-opslag versleutelen, logging van key-materiaal verwijderen
  • Code kwaliteit: Duplicatie weghalen (UsbKeyReceiver), coroutines ipv raw Threads
  • Robustheid: BLE write acknowledgements, GATT status logging
  • Modernisering: EncryptedSharedPreferences, DayNight theme, AGP/Kotlin upgrade

De firmware (src/main.cpp) is netjes geschreven en heeft geen aanbevelingen buiten scope van deze review.