Tales from the crypto#
Tue Feb 17 16:50:41 2026
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 ...
-
when you encrypt with python, you get back a result which is 40 bytes longer than the input plaintext, because it "stores authentication information and the nonce alongside [the ciphertext]"
-
when you decrypt with Rust, it expects the nonce and the ciphertext to be passed as separate arguments to
decrypt -
the nonce is 24 bytes, not 40 bytes, just in case you were thinking that explains the difference
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