Private & Public Key Encryptie¶
IDENTITEIT · VERTROUWEN · VERTROUWELIJKHEID · ZONDER INTERNET · CC310
Opmerking
Bron. De sectie Hardwarecrypto op nRF52 is geverifieerd tegen de
firmware zelf: MeshCore v1.17.1, commit d929643, 14 augustus 2026 —
bestanden platformio.ini, src/Utils.cpp, src/Identity.cpp,
src/helpers/NRF52Board.cpp en
src/helpers/radiolib/RadioLibWrappers.h. De rest van dit hoofdstuk
beschrijft het protocol en de sleutelsoorten; die tekst is nog niet tegen
een afzonderlijke commit getoetst en draagt daarom geen eigen pin.
Het probleem: communicatie zonder autoriteit¶
MeshCore is ontworpen voor situaties waarin er geen internet, geen servers en geen centrale autoriteit beschikbaar is. Denk aan noodcommunicatie bij een ramp, off-grid operaties in afgelegen gebieden, of gewoon een autonoom lokaal netwerk dat niet afhankelijk is van telecomproviders of cloudinfrastructuur.
Dat klinkt mooi, maar het schept een probleem. In een normaal netwerk (internet, WhatsApp, Signal) staat er altijd een vertrouwde derde partij tussen: een server die identiteiten controleert, certificaten uitgeeft, en sleutels uitwisselt. Op een open mesh-radionetwerk bestaat die partij niet. Elke node is gelijk — er is niemand die zegt "deze node is echt wie hij zegt dat hij is."
De cryptografie in MeshCore lost drie problemen op die ontstaan wanneer je die vertrouwde derde partij weghaalt:
Opmerking
1. Identiteit — Hoe weet ik met wie ik communiceer als er geen server is die gebruikers registreert? 2. Authenticiteit — Hoe weet ik dat een bericht echt van die node komt en niet is vervalst door iemand anders? 3. Vertrouwelijkheid — Hoe voorkom ik dat derden meelezen op een open radiokanaal waar letterlijk iedereen kan luisteren?
Het antwoord op alle drie is hetzelfde fundament: public/private key cryptografie. Elk communicatietype in MeshCore — channels, DM's, Room Servers — maakt op een andere manier gebruik van dit fundament, afgestemd op de specifieke behoefte van dat type communicatie.
Het Ed25519 Keypair: je identiteit op het mesh¶
Bij het eerste flashen genereert elke node een Ed25519 keypair. Dit is geen willekeurig detail — het is het moment waarop de node zijn unieke, onvervalsbare identiteit krijgt. Zonder centrale registratie is dit keypair het enige bewijs dat een node is wie hij zegt dat hij is.
| Sleutel | Grootte | Doel |
|---|---|---|
| Private Key | 64 bytes | Geheim — verlaat nooit het apparaat. Bewijst identiteit door berichten te ondertekenen, en maakt het mogelijk om gedeelde geheimen te berekenen voor versleutelde communicatie. |
| Public Key | 32 bytes | Openbaar — wordt verspreid via ADVERT-pakketten. Andere nodes gebruiken deze om jouw identiteit te herkennen, jouw handtekeningen te verifiëren, en versleutelde berichten naar jou te sturen. |
De eerste byte (2 hex-karakters) van de public key fungeert als verkorte node-identifier in trace paths en pakket-routing. In een groeiend mesh kunnen deze 1-byte identifiers botsen; een custom key generator kan dan een uniek prefix garanderen.
Opmerking
Vergelijk het met een handtekening: je public key is je naam die iedereen kent. Je private key is de manier waarop je je handtekening zet — uniek, niet na te maken, en het bewijs dat jij het bent.
ADVERT: jezelf bekend maken zonder server¶
Op het internet registreer je een account bij een dienst en die dienst vertelt anderen dat je bestaat. Op een mesh is er geen dienst. De oplossing: elke node kondigt zichzelf aan via een ADVERT-pakket (payload type 0x04).
┌──────────────┬────────────┬──────────────┬──────────────────┐
│ Public Key │ Timestamp │ Signature │ App Data │
│ 32 bytes │ 4 bytes │ 64 bytes │ 0-32 bytes │
└──────────────┴────────────┴──────────────┴──────────────────┘
Dit pakket wordt altijd flood-routed — elke repeater stuurt het door. Elk veld dient een specifiek doel in het opbouwen van vertrouwen zonder centrale autoriteit:
- Public Key (32 bytes) — jouw volledige publieke sleutel, zodat andere nodes jou kunnen "leren kennen" en later beveiligd met jou kunnen communiceren
- Ed25519 Signature (64 bytes) — de handtekening dekt public key, timestamp en app data, gezet met jouw private key. Dit lost het authenticiteits-probleem op: niemand kan een ADVERT namens jou vervalsen, want alleen jij bezit de private key die bij de public key hoort
- Timestamp (4 bytes) — bescherming tegen replay-aanvallen: als iemand een oud ADVERT opnieuw uitstuurt, weigeren ontvangers het omdat ze al een nieuwere timestamp van dezelfde afzender hebben
- App Data (maximaal 32 bytes) — een flags-byte met het node-type (chat / repeater / room server / sensor), optioneel GPS-coördinaten, en de naam. Er is geen apart lengteveld: de app data loopt tot het einde van de payload
Waarschuwing
Dit is het fundament: zonder ADVERT-uitwisseling is geen enkele beveiligde communicatie mogelijk. Het ontvangen van iemands public key via een ADVERT is het moment waarop je DM's naar die node kunt versturen. Geen ADVERT ontvangen = geen versleutelde communicatie mogelijk.
Channel Berichten: open plein vs. afgesloten kamer¶
Niet alle communicatie hoeft privé te zijn. Een mesh-netwerk heeft ook behoefte aan open communicatie — een digitaal marktplein waar iedereen kan meelezen, en aan groepskanalen voor specifieke onderwerpen of teams. Hier wordt een ander type sleutel gebruikt: de PSK (Pre-Shared Key).
Waarom geen public/private keys voor channels?¶
Het ECDH-mechanisme dat DM's beveiligt, werkt per definitie tussen twee nodes — het berekent een gedeeld geheim uit twee keypairs. Een groepschannel heeft echter tientallen of honderden deelnemers die allemaal dezelfde berichten moeten kunnen lezen. Dat vereist een gedeelde groepssleutel (PSK) in plaats van per-paar sleutels. Het is een bewuste architectuurkeuze: groepscommunicatie offert individuele cryptografische identiteit op voor schaalbaarheid.
Wat is een PSK?¶
Een PSK (Pre-Shared Key) is een encryptiesleutel die vooraf wordt gedeeld tussen alle deelnemers — via QR-code, persoonlijk overhandigd, of via een ander beveiligd kanaal buiten het mesh om. Alle nodes met dezelfde PSK kunnen berichten op dat channel lezen en schrijven. Het persoonlijke keypair van de node speelt hierbij geen rol.
Pakketstructuur (payload type 0x05)¶
┌──────────────┬──────────┬──────────────────────────┐
│ Channel Hash │ MAC │ Encrypted Payload │
│ 1 byte │ 2 bytes │ variable │
└──────────────┴──────────┴──────────────────────────┘
Na decryptie met de PSK bevat de payload:
┌───────────┬───────┬────────────────────┐
│ Timestamp │ Flags │ "NaamNode: tekst" │
│ 4 bytes │ 1 byte│ variable │
└───────────┴───────┴────────────────────┘
Drie typen channels, drie niveaus van openheid¶
De keuze tussen channel-typen is een bewuste afweging tussen bereikbaarheid en vertrouwelijkheid:
| Channel Type | Hoe de PSK bepaald wordt | Doel |
|---|---|---|
| Public | Vaste, bekende key (bv. AQ==) |
Open communicatie — het dorpsplein van het mesh waar iedereen welkom is |
| Hashtag (#naam) | Hash van de channel-naam | Thematische organisatie — als een gedeelde tafel in een café, niet geheim maar wel afgeschermd van de drukte |
| Private | Random gegenereerd, out-of-band gedeeld | Besloten groep — als een vergaderruimte met slot, alleen toegankelijk voor wie de sleutel heeft ontvangen |
Hoe het over de ether gaat¶
Channel-berichten worden altijd flood-routed: elke repeater stuurt het pakket blindelings door, ook als die repeater de inhoud niet kan lezen. Dit is essentieel voor het doel van channels — maximaal bereik. De Channel Hash (1 byte) is een snelle filter zodat een node kan checken of het een passende PSK heeft zonder alles te moeten decrypten.
Waarschuwing
Let op: Het public channel is technisch versleuteld (AES-128), maar omdat de PSK algemeen bekend is, biedt het geen werkelijke privacy. Het doel van encryptie hier is niet geheimhouding, maar het correct kunnen decoderen van het bericht uit het radiosignaal.
Direct Messages: privégesprek zonder tussenpersoon¶
Channels lossen het probleem van groepscommunicatie op, maar voor een privégesprek tussen twee personen zijn ze ongeschikt — iedereen met de PSK kan meelezen. Hier komen de public en private keys tot hun recht via ECDH (Elliptic Curve Diffie-Hellman).
Het doel: een gedeeld geheim zonder het ooit uit te wisselen¶
Het bijzondere van ECDH is dat twee nodes een identiek geheim kunnen berekenen zonder dat geheim ooit over de radio te sturen. Dit is het belangrijkste in een mesh-omgeving: alles wat je over de ether stuurt, kan door elke repeater en elke luisteraar worden opgepikt. Met ECDH hoeft het geheim zelf nooit de radio op.
Het ECDH-proces¶
- Node A kent de public key van Node B (ontvangen via ADVERT)
- Node A berekent:
shared_secret = ECDH(A_private, B_public) - Node B berekent onafhankelijk:
shared_secret = ECDH(B_private, A_public) - Beide uitkomsten zijn wiskundig identiek — eigenschap van elliptische krommen
- Het shared secret wordt eenmalig berekend bij het toevoegen van een contact en gecached
- Alle berichten worden AES-versleuteld met dit shared secret
Opmerking
Waarom werkt dit zonder centrale autoriteit? Omdat het vertrouwen zit in de wiskunde, niet in een server. Zelfs als een aanvaller alle ADVERT-pakketten met public keys onderschept, kan hij het shared secret niet berekenen — daarvoor is een van de twee private keys nodig, en die verlaten nooit het apparaat.
Pakketstructuur (payload type 0x02)¶
┌───────────┬──────────┬──────────┬──────────────────────────┐
│ Dest Hash │ Src Hash │ MAC │ Encrypted Payload │
│ 1 byte │ 1 byte │ 2 bytes │ variable │
└───────────┴──────────┴──────────┴──────────────────────────┘
De Dest Hash en Src Hash zijn de eerste bytes van de public keys van ontvanger en afzender. Repeaters kunnen het pakket routeren (ze zien de hashes), maar niet de inhoud lezen. Dit is het hart van de privacy: zelfs de infrastructuur die je bericht doorgeeft, kan niet meelezen.
Na decryptie met het ECDH shared secret:
┌───────────┬───────┬──────────┬──────────────────┐
│ Timestamp │ Flags │ Text │ Optional: Attempt│
│ 4 bytes │ 1 byte│ variable │ 1 byte │
└───────────┴───────┴──────────┴──────────────────┘
Routing en bevestiging¶
Anders dan channels kunnen DM's zowel flood- als direct-routed zijn. Als het mesh eerder een pad naar de ontvanger heeft geleerd, volgt het bericht dat specifieke pad — efficiënter en minder belastend voor het netwerk. De ontvanger stuurt een ACK terug als bewijs dat het bericht ongewijzigd is aangekomen: SHA256 over timestamp, flags, tekst en de public key van de afzender, afgekapt op 4 bytes. De ACK-payload van een gewone DM is 6 bytes — die 4 hash-bytes plus een byte met het pogingnummer en een willekeurige byte, die de pakkethash uniek houden. Alleen de eerste 4 bytes worden vergeleken.
Hoe het pad geleerd wordt en waarom een direct gerouteerde DM geen regiocode draagt, staat in Direct Messages.
Room Servers: groepschat met geheugen én anonimiteit¶
Channels zijn real-time en hebben geen geheugen — als je offline bent, mis je berichten. DM's zijn privé maar alleen één-op-één. Room Servers combineren het beste van beide: groepscommunicatie met berichtopslag. Maar het inloggen op een Room Server brengt een extra privacyrisico: je zou je vaste identiteit blootgeven aan de server.
Oplossing: ephemeral (tijdelijke) keys¶
Bij het benaderen van een Room Server wordt een ANON_REQ-pakket (type 0x07) verstuurd. Hiervoor genereert de node een wegwerp-keypair — een tijdelijke identiteit die alleen voor deze sessie bestaat:
- De node maakt een ephemeral Ed25519 keypair aan
- De ephemeral public key gaat mee in het pakket
- De Room Server leidt hieruit een tijdelijk shared secret af
- Na authenticatie (wachtwoord) krijgt de gebruiker de laatste ongelezen berichten
Het doel: je vaste identiteit (je echte public key) wordt niet direct blootgesteld bij het eerste contact. Dit biedt een extra privacylaag — de Room Server hoeft niet te weten wie je "echt" bent om je berichten te leveren.
| Room Type | Toegang | Typisch gebruik |
|---|---|---|
| Public Room | Leeg of breed gedeeld wachtwoord | Open groepscommunicatie met berichtgeschiedenis |
| Private Room | Geheim wachtwoord | Besloten teamcommunicatie met opslag |
| Read-only Room | Alleen admins schrijven | Aankondigingen, bulletins, nieuwsfeeds |
Het grotere plaatje: waarom verschillende mechanismen?¶
Op het eerste gezicht lijkt het complex: PSK's, ECDH, ephemeral keys — waarom niet gewoon één systeem? Het antwoord ligt in de fundamenteel verschillende doelen van elk communicatietype:
| Type | Primair doel | Encryptie | Rol Keypair | Routing |
|---|---|---|---|---|
| Public Channel | Maximaal bereik, open communicatie | AES-128 met bekende PSK | Geen | Flood |
| Hashtag Channel | Thematisch groeperen zonder registratie | AES-128 met hash van naam | Geen | Flood |
| Private Channel | Beveiligde groepscommunicatie | AES-128 met random PSK | Geen | Flood |
| Direct Message | Maximale privacy tussen twee nodes | AES-128 met ECDH secret | Centraal | Flood of Direct |
| Room Server | Groepschat met geheugen én anonimiteit | Wachtwoord + ephemeral ECDH | Tijdelijk keypair | Direct |
Het spectrum loopt van volledig open (public channel: iedereen kan meeluisteren) naar maximaal privé (DM: alleen de twee betrokken nodes). Elk communicatietype is een bewuste afweging tussen bereikbaarheid, schaalbaarheid en vertrouwelijkheid — en elk gebruikt precies het cryptografische mechanisme dat bij die afweging past.
De rode draad: keys in elk pakket¶
De public keys zijn niet alleen relevant voor encryptie — ze zijn verweven in de basis van elk pakket dat over het mesh vliegt:
┌────────┬──────────┬─────────────────────────┐
│ Header │ Path Len │ Path[] │
│ 1 byte │ 1 byte │ 1 byte per hop, max 64 │
└────────┴──────────┴─────────────────────────┘
Header byte = payload type + route type (flood of direct)
Path[] = lijst van node-identifiers (eerste bytes van public keys)
Bij flood routing voegt elke repeater zijn eigen identifier (= eerste byte public key) toe aan het path. Bij direct routing staat het volledige geplande pad er al in en pelt elke hop er één af.
De public keys functioneren dus op twee niveaus tegelijk:
- Routing-adressen — de eerste byte van de public key is het "huisnummer" waarmee pakketten hun weg door het mesh vinden
- Cryptografische basis — de volledige public key is de grondstof voor ECDH shared secrets (DM's) en handtekeningverificatie (ADVERT's)
Dit dubbele gebruik maakt het systeem elegant: dezelfde identiteit die een node uniek maakt op het netwerk, is tegelijk de sleutel tot beveiligde communicatie — allemaal zonder dat er ooit een server, een provider of een certificaatautoriteit aan te pas komt.
Hardwarecrypto op nRF52¶
Alles hierboven beschrijft wát er rekenkundig gebeurt. Sinds v1.17.1 is op nRF52-borden veranderd wíe het doet: niet de processor, maar de CryptoCell CC310, een aparte eenheid in dezelfde chip. Dezelfde bewerkingen, dezelfde uitkomsten, geen protocolwijziging — twee nodes merken onderling niets van het verschil. Het gevolg zit in het apparaat, niet in het netwerk.
De vlag hangt aan de familie, niet aan een bord¶
USE_CC310_HW_CRYPTO is geen bordeigenschap. De vlag staat in [nrf52_base]
(r.81) in de platformio.ini in de repository-root, de sectie waarvan elk
nRF52-variantbestand erft:
platformio.ini r.90-94
build_flags = ${arduino_base.build_flags}
-D NRF52_PLATFORM
-D LFS_NO_ASSERT=1
-D EXTRAFS=1
-D USE_CC310_HW_CRYPTO=1
Er is dus geen nRF52-build zonder hardwarecrypto — dit geldt voor elke nRF52-build, niet voor een enkel bord. ESP32, RP2040 en STM32 kennen de vlag niet en volgen overal de softwareweg. Welke variant op welke familie draait staat in MeshCore Platforms.
De eenheid wordt aangezet in NRF52Board::begin(), op
src/helpers/NRF52Board.cpp r.29, en weer afgesloten bij het uitschakelen van
de randapparatuur vóór de diepe slaapstand, op r.364. Buiten die twee plekken
is er niets om in te stellen.
Zes bewerkingen verhuizen¶
Elk van de zes staat in de broncode als een #ifdef USE_CC310_HW_CRYPTO met
de softwareweg in de #else-tak. Beide takken blijven dus in de repository
staan; welke wordt meegecompileerd hangt alleen van de vlag af.
| Bewerking | Functie | Softwareweg | Hardwareweg | Plek |
|---|---|---|---|---|
| SHA-256 over één blok | Utils::sha256 |
SHA256 uit de Crypto-library |
CRYS_HASH |
src/Utils.cpp r.24 |
| SHA-256 over twee fragmenten | Utils::sha256 (overload) |
idem, met update() per fragment |
CRYS_HASH_Init / _Update / _Finish |
src/Utils.cpp r.36 |
| AES-ECB ontsleutelen | Utils::decrypt |
AES128 |
SaSi_Aes* |
src/Utils.cpp r.53 |
| AES-ECB versleutelen | Utils::encrypt |
AES128 |
SaSi_Aes* |
src/Utils.cpp r.85 |
| Handtekening controleren | Identity::verify |
Ed25519::verify |
CRYS_ECEDW_Verify |
src/Identity.cpp r.22 |
| Toevalsgetallen | RadioNoiseListener::random |
radioruis | nRFCrypto.Random, gecombineerd met radioruis |
src/helpers/radiolib/RadioLibWrappers.h r.93 |
De eerste vier en de zesde zijn een ruil van bibliotheek: dezelfde invoer, dezelfde uitvoer, een andere uitvoerder. De vijfde is de enige waar de firmware zelf uitlegt waarom.
De handtekeningcontrole, uitgeschreven¶
Een ADVERT komt binnen, de node moet de Ed25519-handtekening controleren, en dat gebeurt in het ontvangstpad van de radio. De reden voor de hardwareweg staat letterlijk in de broncode:
src/Identity.cpp r.22-33
bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) const {
#ifdef USE_CC310_HW_CRYPTO
// nRF52840 CryptoCell CC310 hardware Ed25519 verification. The software
// implementations need ~3KB of stack (which can overflow the Adafruit core's
// 4KB loop task stack from the advert receive path); the hardware path
// needs much less, around 600-700bytes. The CC310 workspace is static, faster,
// should save power at scale as well.
static CRYS_ECEDW_TempBuff_t cc310_tmp;
CRYSError_t rc = CRYS_ECEDW_Verify((uint8_t*)sig, CRYS_ECEDW_SIGNATURE_BYTES,
(uint8_t*)pub_key, CRYS_ECEDW_MOD_SIZE_IN_BYTES,
(uint8_t*)message, (size_t)msg_len, &cc310_tmp);
return rc == CRYS_OK;
De softwarematige controle neemt ongeveer 3 kB stack in beslag. De loop-taak van de Adafruit-core heeft er 4 kB, en de controle wordt aangeroepen vanuit het advert-ontvangstpad — dus vanuit een aanroepketen die zelf ook al stack gebruikt. Dat kan overlopen. De hardwareweg heeft 600 à 700 bytes nodig en zet zijn werkgeheugen bovendien statisch neer, buiten de stack om.
Het is dus geen versnelling die erbij is gezocht. Het is een stackprobleem dat wordt opgelost; de snelheidswinst en het lagere stroomverbruik noemt het commentaar er als tweede en derde bij.
De toevalsgenerator vervangt de radioruis niet¶
Bij de vijf bewerkingen hierboven neemt de hardware het werk over. Bij de
zesde komt er iets bíj. RadioNoiseListener haalt zijn entropie uit de ruis
op de radio-ingang; met de CC310 erbij wordt die ruis niet vervangen maar
ge-XOR'd met de hardwarematige uitvoer:
src/helpers/radiolib/RadioLibWrappers.h r.93-103
void random(uint8_t* dest, size_t sz) override {
#ifdef USE_CC310_HW_CRYPTO
nRFCrypto.Random.generate(dest, (uint16_t)sz);
for (int i = 0; i < sz; i++) {
dest[i] ^= _radio->randomByte() ^ (::random(0, 256) & 0xFF); // combine with Radio's entropy
}
#else
for (int i = 0; i < sz; i++) {
dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF);
}
#endif
Let op het verschil tussen de twee takken: ^= tegenover =. In de
softwaretak is de radioruis de enige bron; in de hardwaretak wordt de CC310
gevuld en daarna met diezelfde radioruis bewerkt. Twee onafhankelijke bronnen
door elkaar is niet zwakker dan de sterkste van de twee, en het commentaar op
src/helpers/NRF52Board.cpp r.30 zegt waarom de CC310 erbij is gehaald: zijn
uitvoer hangt niet af van de omgeving, en radioruis wel.
Dit is het enige punt van de zes waar de hardware iets toevoegt aan het resultaat in plaats van het op een andere manier uit te rekenen. Waar het keypair vandaan komt dat hiermee wordt gemaakt, staat hierboven onder Het Ed25519 Keypair.
Bronnen¶
Firmware, commit d929643 (v1.17.1, 14 augustus 2026) — voor de sectie
Hardwarecrypto op nRF52:
platformio.ini—USE_CC310_HW_CRYPTOin[nrf52_base]src/Utils.cpp— hash en AES, beide takkensrc/Identity.cpp— de handtekeningcontrole en de stackredeneringsrc/helpers/NRF52Board.cpp— starten en afsluiten van de eenheidsrc/helpers/radiolib/RadioLibWrappers.h— de toevalsgenerator
Verwant in deze documentatie:
- Crypto: rweather en ed25519 — de libraries achter de softwareweg
- Platformrealisatie — wat de nRF52-familie verder apart doet
- MeshCore Pakketstructuur — waar de handtekening in het pakket zit