How to Implement Secure Secret Storage with OTP and eFUSE on ESP32
Introduction
Every IoT device that authenticates needs somewhere safe to store secrets. Stashing API keys or auth tokens in flash memory is asking for trouble — anyone with physical access can dump the firmware and read them out. The ESP32 has a better option: eFUSEs. These are one-time programmable (OTP) memory cells burned directly into the silicon. Once written, they can't be modified or erased, and with the right configuration, they can't be read back by software either.
This guide walks through using eFUSEs on the ESP32 to store authentication secrets and implementing a one-time password (OTP) scheme for device authentication. We'll use ESP-IDF v5.3+ for eFUSE operations since the Arduino layer doesn't expose the eFUSE API directly.
Watch out: eFUSE programming is permanent. There's no undo. Test everything on a dev board you don't mind bricking before going anywhere near production hardware.
Prerequisites
- Working knowledge of embedded C programming and the ESP32 platform
- ESP-IDF v5.3+ installed and configured (eFUSE operations require the full SDK, not just the Arduino layer)
- Understanding of basic cryptographic concepts — hashing, HMAC, symmetric keys
- USB cable for flashing the ESP32
Parts/Tools
- ESP32 development board (ESP32-S3 or ESP32-C3 recommended — they have improved eFUSE layouts and security features over the original ESP32)
- USB cable
- ESP-IDF v5.3+ with the
espefusecommand-line tool - A TOTP/HOTP library or custom HMAC-SHA1 implementation
- Serial terminal (minicom, PuTTY, or the IDF Monitor)
Steps
- Set Up the Environment
- Install ESP-IDF v5.3+ following the official docs. Make sure the
idf.pyandespefuse.pytools are on your PATH. - Connect your ESP32 dev board via USB and verify the connection:
idf.py --port /dev/ttyUSB0 flash monitor - Read the current eFUSE state to see what's already been programmed on your chip:
espefuse.py --port /dev/ttyUSB0 summaryThis dumps all eFUSE blocks and their values. On a fresh chip, most user-accessible blocks will be all zeros.
- Install ESP-IDF v5.3+ following the official docs. Make sure the
- Configure eFUSE for Secure Storage
- The ESP32 has several eFUSE blocks. For storing custom secrets, you'll use the key blocks (BLK1, BLK2, BLK3 on original ESP32, or the expanded key slots on ESP32-S3/C3).
- Generate a 256-bit secret key that will serve as the HMAC seed for OTP generation:
# Generate a random 32-byte key python3 -c "import os; open('secret_key.bin','wb').write(os.urandom(32))" - Burn the key into an eFUSE key block using
espefuse.py:espefuse.py --port /dev/ttyUSB0 burn_key \ BLOCK_KEY0 secret_key.bin USERThe
USERkey purpose means your application code can read this key. For higher security, you can set the purpose toHMAC_UP(on S3/C3) which lets the hardware HMAC peripheral use the key without exposing it to software at all. - After burning, enable read protection on the key block so it can't be dumped:
espefuse.py --port /dev/ttyUSB0 burn_efuse RD_DIS_KEY0Again — this is permanent. Triple-check you have the key backed up somewhere safe before locking it down.
- Implement OTP Generation
- In your ESP-IDF application, read the secret from eFUSE (if using USER key purpose) or invoke the hardware HMAC peripheral:
#include "esp_efuse.h" #include "esp_efuse_table.h" #include "mbedtls/md.h" // Read key from eFUSE block (USER purpose only) uint8_t secret_key[32]; esp_efuse_read_block(EFUSE_BLK_KEY0, secret_key, 0, 256); // Generate HMAC-based OTP uint8_t hmac_result[32]; uint32_t counter = get_otp_counter(); // Your counter or timestamp mbedtls_md_hmac( mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), secret_key, 32, (uint8_t*)&counter, sizeof(counter), hmac_result ); // Truncate to 6-digit OTP uint32_t otp = (hmac_result[0] << 24 | hmac_result[1] << 16 | hmac_result[2] << 8 | hmac_result[3]) % 1000000; - For time-based OTP (TOTP), replace the counter with
time(NULL) / 30after syncing the ESP32's clock via SNTP. Counter-based HOTP is simpler but requires both sides to stay in sync.
- In your ESP-IDF application, read the secret from eFUSE (if using USER key purpose) or invoke the hardware HMAC peripheral:
- Authenticate the Device Using OTP
- Build a validation function that compares a received OTP against the locally generated one:
bool validate_otp(uint32_t user_otp) { uint32_t expected = generate_otp(); // Allow a window of +/- 1 step for clock drift (TOTP) for (int i = -1; i <= 1; i++) { if (user_otp == generate_otp_with_offset(i)) { return true; } } return false; } - Call this during your authentication flow — over BLE, HTTPS, MQTT, or whatever transport you're using.
- Tip: always use constant-time comparison for security-sensitive comparisons to prevent timing attacks. The simple
==above works for integer OTPs, but for byte-array comparisons, usembedtls_ct_memcmp().
- Build a validation function that compares a received OTP against the locally generated one:
Troubleshooting
- eFUSE burn fails or returns an error:
Make sure the eFUSE block hasn't already been programmed — you can't overwrite a burned eFUSE. Run
espefuse.py summaryto check the current state. Also confirm you're not trying to write to a block that's been read/write protected. - Can't read the key back from eFUSE:
You probably enabled read protection (which is correct for production). For development, burn the key with read protection disabled first, test your code, then lock it down on the final device.
- OTP doesn't match between device and server:
For TOTP, clock drift is the usual culprit. Make sure the ESP32's time is synced via SNTP, and implement a validation window of +/- 1 or 2 time steps. For HOTP, check that both sides have the same counter value.
- ESP32 won't boot after eFUSE changes:
If you accidentally enabled secure boot or flash encryption eFUSEs without proper setup, the chip may refuse to boot unsigned firmware. On the original ESP32, this is irreversible. On ESP32-S3/C3 with the revocable secure boot scheme, you have more options. This is why you test on sacrificial dev boards first.
Wrapping Up
eFUSEs give you hardware-rooted secret storage that software exploits can't easily extract. Combined with an OTP scheme, you have a solid authentication mechanism for IoT devices that doesn't rely on secrets sitting in readable flash. The ESP32-S3 and C3 variants take this further with hardware HMAC peripherals that can use eFUSE keys without ever exposing them to your application code — worth exploring if your threat model includes firmware-level attacks.