diary @ telent

Un-BLE-vable#

Sat Feb 7 20:17:45 2026

Topics: eculocate rust

More eculocate: I spent the past couple of weeks first on rearranging the client code to be a bit less awful - now there's a command line client as well as an android/termux GUI one, and they share code - and then on adding an affordance for configuring the wifi network that doesn't require hardcoding it at build time.

This is kind of a distraction from plugging it into an actual motorbike, because I could have just hardcoded the wifi details for tethering to my phone, but once it's attached to the bike then I can only flash it OTA and I don't want to be messing around tethering my development machine to my phone just so it can be on the same network as the bike.

In January I said:

There's a convention for provisioning wifi on these devices which involves using a mobile phone app to connect to it using BLE then sending the ssid/password of the chosen access point. In fact there's even a prebuilt Android app which we can use and an esp32 arduino library which we can't (because we have elected to make our lives difficult and use Rust instead). But I am led to believe that "rewrite everything in Rust" is idiomatic for Rust programmers anyway. I haven't done this yet.

And ... I still haven't, or not as such, because change of plan. Because I would have to write the device-side code myself anyway (because Rust), and I would want to write the phone-side code instead of using their app (because I'd like to have one app to do everything instead of separate apps for provisioning and for recording), then I have free rein on the protocol, and the Espressif protocol seems ... quite involved?

(What I didn't learn until checking references for writing this blog post which has already taken way too long it is already Tuesday is that the Espressif protocol I linked there no longer exists except in git history, and appears to have been replaced by a different one called Blufi so I am feeling even less bad about swerving reimplementing it)

Background, if you're lucky enough to not know much about Bluetooth Low Energy: the usual interaction model is "GATT", which can be thought of as a network-based key/value store. A device defines "characteristics" and "attributes" and allows read/write access to them. If you're working "with the grain" of the design, you use this to model some kind of object/record/aggregate/entity - or, alternatively, you could just have attributes for rx and tx and send streams of data over them in any format you like.

The Espressif protocol - as was - defines a protobuf-based protocol that runs over both BLE and also WiFi with SoftAP, whereas if we're restricting ourselves to BLE we can actually leverage GATT instead of using it to emulate a stream interface.

So what have we done instead? We have four characteristics (with hindsight maybe this should have been four attritbutes of one characteristic). eculocate scans for wifi networks it can see, then it sets the attribute max_index to the number ot visible networks. The client then loops throuh 1..max_index writing to the attribute current_index, which causes eculocate to update current_network to contain the ssid of the nth scanned network. When the client finds a network it likes, it writes the corresponding password to secret and eculocate saves the ssid/secret into flash. The loop feels a bit weird, but there isn't any way - at least, I can't see any way - to specify that an attribute is array-valued.

To provide security for the precious wifi credentials we simply enable pairing, which causes the two ends to encrypt with AES-CCM.

Sounds simple, and it probably would be except that USB bluetooth hardware is a market for lemons (I originally said something much much ruder there) and the degree of care and research needed to get one that works properly in Linux far exceeds the degree of care and research I exerted. The first dongle i bought, which identifies as 33fa:0010 and is said to be based on a "Barrot" chipset, is a buggy pile of shite which locks up and resets randomly, even when using a kernel that contains e7d1cad6545c, meaning that every test of the code had to be preceded by removing and reloading the module and thewn it would work about 70% of the time provided I didn't try to pair, which caused it to time out consistently.

Then I dug out a Raspberry Pi Zero W from the junk box, but the builtin bluetooth on that only supports 4.1, and the TrouBLE stack won't pair with anything older than 4.2

(I have since received an Edup B3536 dongle which is based on the Realtek chipset and said to be a whole lot better - but it only arrived after I started this blog post and I haven't tried it in anger yet)

It turns out that with the current hardware configuration pairing is not actually going to help much anyway. The device has no keyboard or display (thus no way to display or confirm a pairing pin), so is restricted to "just works" pairing, which offers no protection against MITM at the time of pairing. Since all we're using Bluetooth for is entering the wifi details, and the BLE service is shut down once it successfully connects to wifi, there is no point setting up a secure connection "for later" as there is no "later".

eculocate goes into "wifi chooser" mode when there's no saved network or when it can't connect to the saved network, so a black hat (or black balaclava) could attack it by separating your motorbike from your phone so the phone is out of range, then turn the ignition on, then configuring a different wifi network. It won't help them a lot as they're still lacking the ed25119 key that controls TCP connections. And at this point if they're sitting on your motorbike and the ignition is on, maybe you have bigger problems ... perhaps we could regard the bike's ignition key as a hardware token.

The proper way to do pairing and have it actually be useful would be to add an NFC reader/writer and use OOB authentication but I am very much feeling like I wil save that for another day.


obligatory post title earworm link

Tales from the crypto#

Tue Feb 17 16:50:41 2026

Topics: eculocate rust

I said in the previous entry that "pairing is not actually going to help much anyway", with the implication that I was done thinking about BLE. Turns out I was not done thinking about BLE. Notwithstanding that the scope for anyone to maliciously connect the device to the wrong wifi is pretty limited, the attack I hadn't considered is that an attacker might be listening to us as we send the credentials for the right wifi network. Which could be bad.

It continues to be true, though, that "pairing is not actually going to help much anyway": although "Just Works" pairing is encypted against passive eavesdropping, an active MITM will still see everything we send, and can be put together for the price of two bluetooth-capable MCUs. So, instead of relying on the transport we're adding encryption/authentication of our own.

getrandom

A lot of Rust crypto stuff depends on the getrandom crate that provides an interface to the OS's randomness source.

getrandom doesn't have a supported target for esp32 no_std, meaning it won't build without a custom randomness source. Further, the mechanisom for adding a custom source has changed between getrandom 0.2 and 0.3. If you follow the current getrandom docs for adding the esp32 hardware random source, and then cargo add some crate that transitively adds a rand_core dependency, you may find as I did that your crate depends on the older version: you now have two versions of getrandom in your project and one of them still doesn't work. I don't have nubers for how likely this is but I've hit it twice with different randomly(sic) selected libraries.

The 0.2.17 docs explain how to do custom implementations for the version of getrandom that everyone's actually using, I can't now remember where I found the source for making it work with esp_hal, but the tl;dr is I did this.

salt and shake

NaCl/libsodium seems a defensible choice of library that avoids the "don't roll your own crypto" injunction. I picked crypto_box from nacl-compat on the Rust side, and expected PyNaCl's nacl.public.Box to work with it. It does, but ...

It's simple once you know what's going on, but it would be a lot simpler to work out what's going on if the docs were a bit more precise than just saying "alongside". For posterity, the first 24 bytes are nonce, and the rest is a concatenation of the ciphertext and a "16 byte authenticator", which is also described as a "MAC tag". The Rust-side decrypt method expects the nonce as its first argument and the combined ciphertext+tag as the second, so although I didn't find out which of those two comes first, blessedly, we don't need to know.

"alongside". Bah.

(It took longer to work this out than it should have, because in the process of sending it across Bluetooth I managed to append a chunk of NUL bytes to the ciphertext, which not surprisingly caused it to fail to decrypt no matter how I carved it up.)

there may be TrouBLE ahead

It wouldn't feel real if I hadn't had to spend some time after that wrestling with bluetooth again.

This time: when you write a characteristic value that exceeds the MTU, instead of writing it all in one go it uses a sequence of PrepareWrite messages to send each chunk and then an ExecuteWrite to stitch it together. TrouBLE (the rust bluetooth stack) handles this insofar as it processes these messages and updates the value in the server, but it means you don't get a GattEvent::Write event that you can use to perform any other action (in our case, save the data to nvs and and reboot).

Either you can match GattEvent::Other and then do arcane things to find out if it was an ExecuteWrite and was for the appropriate attribute handle, or you can do what I did and add an extra "I'm done now" step to the provisioning process where it writes a boolean when it's finished.

I really am running out of excuses not to hook it to my motorbike now