Decrypting my Flume water monitor's MQTT traffic with a passive relay

stevecrozz1 pts0 comments

Decrypting Flume Water Monitor Traffic

about me

lithostech.com —<br>connecting the tubes

In my previous post, I tore<br>apart a Flume water monitor and traced its communication path from the sensor,<br>through the bridge, and up to Flume’s cloud MQTT server. I identified the<br>encryption library (LibHydrogen), extracted a firmware ELF, and even managed to<br>coerce the bridge into sending plaintext by corrupting the public key in flash.<br>But that approach was destructive; the bridge couldn’t complete its handshake<br>with Flume’s server, so real data stopped flowing.

Thinking I had reached a dead end, only to find another path turned out to be a<br>pretty common occurrence for me. More and more thoughts on what I could have<br>done differently, and what further research I could have done kept coming to<br>me. And so I set out to continue to pursue my unhealthy obsession of figuring<br>out exactly how I can read this data on the network.

Why I Thought This Would Be Harder

The firmware contains a full LibHydrogen Noise N key exchange implementation.<br>In the disassembly, hydro_kx_n_1 lives at 0x4023382c (client side) and<br>flume_kx_init_client at 0x40234550 calls it with PSK=NULL and the<br>server’s public key loaded from flash. These functions have real<br>cross-references to session key storage locations. Everything pointed to the<br>bridge negotiating ephemeral session keys with Flume’s server, which would have<br>made passive decryption impossible without writing a custom key pair to flash<br>and implementing the full key exchange in the relay. I actually did do this and<br>hoped to see it working, only to face yet another disappointment.

From the previous investigation, when I invalidated the magic prefix on the key<br>storage area, Flume’s server responded with error 174: “Phydro crypto key<br>exchange function failed.” That error message, combined with the key exchange<br>code in the firmware, made it seem certain that session keys were in play.

But when I actually captured live traffic and tried decrypting with just the<br>static device key, everything decrypted cleanly. No session keys involved. I’m<br>sure I tried this last time, but I must not have had all secretbox parameters<br>correct.

The Noise N code exists in the firmware but isn’t active for normal MQTT<br>traffic. It might be used for OTA updates, or a provisioning step that only<br>runs once, or it may be a remnant of an older protocol version. I don’t know.<br>What matters is that in practice, the bridge encrypts and decrypts all messages<br>with the same static 32-byte key stored in flash. Or at least mine does.

The Encryption Parameters

The bridge encrypts all MQTT payloads using LibHydrogen’s secretbox<br>construction with a single symmetric key for both directions. That key lives at<br>a fixed flash address (0x3f9010) and can be read without modifying the<br>device.

The encryption parameters are straightforward:

Algorithm: hydro_secretbox (LibHydrogen)

Key: 32 bytes at flash address 0x3f9010

Context: the ASCII string 12345678

Message ID: 0 (constant for all messages)

Some might see these SecretBox context and message ID parameters and have a<br>chuckle, but I don’t think Flume has done anything wrong in choosing them. The<br>libhydrogen docs say about context that its purpose is to mitigate accidental<br>bugs by separating<br>domains. There’s<br>probably only one domain in this scheme, so 12345678 is just as appropriate as<br>any other context. And regarding message IDs, the docs also say

If this mechanism is not required by an application, using a constant msg_id<br>such as 0 is also totally fine. Message identifiers are optional and do not<br>have to be unique.

They were findable in the firmware, or guessable although clearly I didn’t<br>guess them correctly last time. Regardless, with these parameters known, any<br>message can be decrypted.

Reading the Key

The bridge’s ESP8266 has labeled UART pins on the board. By connecting a 3.3V<br>USB-to-serial adapter and pulling GPIO0 low during power-on, the chip enters<br>UART download mode. From there, esptool can read arbitrary flash regions:

esptool read_flash 0x3f9010 0x20 device_sk.bin<br>xxd -p -c 32 device_sk.bin

This prints a 64-character hex string, the device secret key. The bridge<br>doesn’t need to be reflashed or modified in any way. Remove the GPIO0 jumper,<br>reassemble, and the bridge boots normally.

The Relay Architecture

With the key and encryption parameters in hand, I built<br>flumewatch, a transparent<br>man-in-the-middle relay that sits between the bridge and Flume’s cloud server.<br>It forwards all traffic untouched while decrypting a copy of each message for<br>local consumption.

The bridge connects to mqtt.prod.flumetech.com on port 1883 (plain TCP, no<br>TLS). By redirecting this traffic at the network layer, either with DNS<br>overrides or destination NAT on the router, the bridge connects to the relay<br>instead.

The relay is a Node.js process running an Aedes<br>MQTT broker. When the bridge connects, the relay:

Accepts the bridge’s MQTT CONNECT (capturing its client ID and...

bridge flume mqtt relay flash message

Related Articles