Chapter 234: Flash Encryption Implementation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the purpose and importance of flash encryption for securing your device.
  • Explain the underlying hardware and software mechanisms of ESP32 flash encryption.
  • Enable and configure flash encryption in both “Development” and “Release” modes.
  • Securely update firmware on an encrypted device.
  • Identify the differences in flash encryption across various ESP32 variants.
  • Diagnose and troubleshoot common issues related to flash encryption.

Introduction

In the world of commercial IoT products, your firmware is your intellectual property (IP). Leaving it unprotected on a device’s flash memory is like leaving the blueprints to your invention out in the open. Anyone with physical access to the device could potentially read the entire flash chip, reverse-engineer your application, steal sensitive data like Wi-Fi credentials or cloud certificates, and clone your product.

Flash Encryption is a fundamental security feature provided by the ESP32 that prevents such unauthorized access. It uses hardware-accelerated AES encryption to render the contents of the external SPI flash unreadable without a unique, device-specific key. This process is transparent to your application code but provides a powerful layer of physical security. This chapter will guide you through the theory, activation, and management of this critical feature, transforming your prototype into a more commercially viable and secure product.

Theory

Flash Encryption protects the software stored in the ESP32’s off-chip SPI flash memory. The mechanism relies on a combination of a hardware AES accelerator and one-time-programmable (OTP) memory bits called eFuses.

The Core Concept: On-the-Fly Decryption

Imagine the flash memory as a locked safe and the encryption key as the only combination that can open it. On the ESP32, this key is stored in the eFuses, which are part of the chip’s silicon itself and cannot be read out by software after being write-protected.

When the CPU needs to execute code or read data from flash, the request is intercepted by a flash cache controller. This controller fetches the encrypted block from the SPI flash, passes it to the hardware AES engine for decryption using the key from the eFuses, and then forwards the decrypted, plaintext content to the CPU. This entire process happens automatically and at high speed, making it transparent to your running application. Any data written to the flash is similarly encrypted on the fly.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    subgraph "CPU/Core"
        A[CPU Requests<br>Code/Data]
    end

    subgraph "Flash Cache Controller & AES Hardware"
        B{Flash Controller<br>Intercepts Request}
        C[Fetches Encrypted Block<br>from SPI Flash]
        D((AES Engine))
        E[Decrypted/Plaintext<br>Content]
    end

    subgraph "External SPI Flash (Encrypted)"
        F([Encrypted Firmware & Data])
    end

    subgraph "eFuse (On-Chip OTP Memory)"
        G([Device-Specific<br><b>FLASH_ENCRYPTION_KEY</b>])
    end

    A --> B;
    B --> C;
    C --> D;
    F -- Encrypted Block --> C;
    G -- "Uses Key" --> D;
    D -- "On-the-Fly Decryption" --> E;
    E -- "Returns Plaintext to CPU" --> A;

    %% Styling
    classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef hardware fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef data fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46;

    class A start;
    class B,C,E process;
    class D,G hardware;
    class F data;

The Encryption Key and eFuses

The encryption key is a 256-bit AES key. It is stored in a specific eFuse block (typically BLK1 or BLK2). The crucial aspect of eFuses is that they are OTP—once a bit is burned (changed from 0 to 1), it can never be changed back.

To manage the encryption state, another eFuse is used: FLASH_CRYPT_CNT. This 7-bit field acts as a lifecycle controller:

  • State 1 (0b0000001): Flash encryption is enabled for the first time. The bootloader will generate a key, burn it to the key block eFuse, and then encrypt the flash.
  • State 2 (0b0000011): Flash encryption is enabled and active.
  • State 3 (0b0000111): Flash encryption is enabled, but new key generation is disabled. This is part of the “Release” mode.
  • State 4 (0b0001111, etc.): Further transitions for security updates.

Each time a bit is programmed to ‘1’, the state change is permanent. This prevents an attacker from downgrading the security level.

Development vs. Release Mode

ESP-IDF offers two primary modes for flash encryption, controlled via menuconfig.

Feature Development Mode Release Mode
Primary Use Case Prototyping, debugging, and active development. Final production-ready firmware for commercial products.
Key Generation Key is generated on-chip on first boot. If the device is reflashed with plaintext, a new key is generated and the flash is re-encrypted. Key is generated once (either on-chip or pre-generated). The device is permanently locked to this single key.
Flexibility High. Allows for easy recovery and complete reflashing during development. None. This is a permanent, one-way action. The device cannot be reverted to development mode.
Security Level Not Secure for Production. An attacker with physical access could wipe the device and flash their own encrypted firmware. Highest Security. Prevents re-encryption with a new key, making it impossible to load unauthorized firmware.
eFuse State (FLASH_CRYPT_CNT) Typically set to 0b...001. The bootloader is permitted to escalate the state and re-encrypt with a new key. Set to a state like 0b...011 or higher, which write-protects the key and disables new key generation.
Plaintext Flashing Allowed once per key cycle. The bootloader handles re-encryption automatically. Bricks the device. The bootloader will refuse to boot and cannot re-encrypt, rendering the device unusable until flashed with firmware encrypted with the original key.
  1. Development Mode (Default): This is the recommended mode for the development phase. The flash encryption key is generated on the device during the first boot. If the device is reflashed with a new plaintext image, the bootloader will detect this, generate a new key, and re-encrypt the flash. This is convenient for development but is not secure for production, as it allows an attacker with physical access to wipe and re-encrypt the device with their own firmware.
  2. Release Mode: This is the most secure mode, intended for production devices. In this mode, FLASH_CRYPT_CNT is configured to prevent new keys from being generated. Once a device is encrypted in Release Mode, it will only boot firmware that has been encrypted with the original key. Attempting to flash a plaintext binary will result in a “bricked” device (it will fail to boot) because the bootloader no longer has permission to re-encrypt the flash. The original key is either generated on the chip during the first boot and is never revealed, or it is pre-generated on a host PC and programmed into the eFuses during manufacturing.

Warning: Enabling Release Mode is a permanent, one-way action. There is no way to disable it or revert to Development Mode. Only enable this on production-ready firmware.

Practical Example: Enabling Flash Encryption (Development Mode)

Let’s enable flash encryption on a basic “hello_world” project. The beauty of this feature is that you don’t need to change your application code at all.

%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans'}}}%%
graph TD
    A["Start: Plaintext<br>Project"] -- 1- Configure --> B{"ESP-IDF Menuconfig"};
    B -- "Enable Flash Encryption<br>(Development Mode)" --> C["Save Configuration"];
    C -- 2- Build --> D["idf.py build"];
    D -- "Generates App Binary" --> E["idf.py flash"];
    E -- 3- Flash (First Time) --> F{Device First Boot};
    F -- "Detects Encryption Flag" --> G["Generates Key,<br>Encrypts All Partitions"];
    G -- "Burns eFuses & Resets" --> H{Device Second Boot};
    H -- "Confirms 'Flash is Encrypted'" --> I["Application Runs<br>on Encrypted Device"];

    subgraph "Firmware Updates"
        J["Modify Code"] --> K["idf.py build"];
        K --> L["idf.py flash"];
        L -- "Securely Updates Firmware" --> I;
    end
    
    %% Styling
    classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef check fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;

    class A start;
    class B decision;
    class C,D,E,J,K,L process;
    class F,H check;
    class G,I success;

Step 1: Configure Flash Encryption in menuconfig

  1. Open your project in VS Code.
  2. Launch the configuration menu by running the ESP-IDF: SDK Configuration editor (menuconfig) command from the command palette (or run idf.py menuconfig in the terminal).
  3. Navigate to Security features --->.
  4. Select Enable flash encryption on boot. Press Enter to enable it.
  5. Keep the default (Development (NOT SECURE)) mode for now.
  6. (Optional but recommended) Under Enable usage of secure boot V2, you can also check Hardware secure boot. Secure Boot is a related feature that ensures the device only runs signed code. We will cover it in the next chapter, but it’s good practice to enable it alongside flash encryption.
  7. Save the configuration and exit.

Step 2: Build the Project

Build your project as usual. The build system will note that flash encryption is configured.

Bash
idf.py build

Step 3: Flash the Plaintext Firmware (First and Only Time)

This is the most critical step. You are now flashing the last plaintext image this device will ever accept.

  1. Connect your ESP32 board.
  2. Run the flash command.
Bash
idf.py -p (YOUR_PORT) flash

Step 4: Monitor the First Encrypted Boot

Immediately after flashing, you must monitor the device’s output to observe the encryption process. This will only happen once.

Bash
idf.py -p (YOUR_PORT) monitor

You will see a log similar to this. The exact messages may vary slightly by chip version.

Plaintext
...
I (31) boot: ESP-IDF v5.x-dirty 2nd stage bootloader
...
I (48) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x04000 h..
...
I (70) boot: Enabling flash encryption...
I (71) boot: Generating new flash encryption key...
I (141) efuse: Burning EFUSE_BLK1_R_DIS...
I (141) efuse: Burning EFUSE_WR_DIS_FLASH_CRYPT_CNT...
I (147) efuse: Burned a new key for Flash Encryption
I (153) boot: Reading partition table from flash, address 0x8000
I (159) boot: Verifying partition table signature...
I (165) boot: Partition table verified, assigning partitions
I (172) boot: Encrypting partition 0 (nvs) at 0x9000 (0x6000 bytes)...
I (200) boot: Done encrypting
I (203) boot: Encrypting partition 1 (phy_init) at 0xf000 (0x1000 bytes)...
I (212) boot: Done encrypting
I (215) boot: Encrypting partition 2 (factory) at 0x10000 (0x100000 bytes)...
I (1231) boot: Done encrypting
I (1234) boot: Flash encryption finished
I (1238) boot: Resetting...
...
ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
...
I (25) boot: ESP-IDF v5.x-dirty 2nd stage bootloader
...
I (47) boot: Loading app from partition at offset 0x10000
I (47) boot: Checking flash encryption...
I (47) boot: Flash is encrypted
I (56) esp_image: segment 0: paddr=0x00010020 vaddr=0x3f400020 size=0x04000 h..
...
I (81) app_start: Starting app project.
...
Hello world!

Log Analysis:

  1. Enabling flash encryption...: The bootloader detects the FLASH_CRYPT_CNT eFuse is 0 and the setting is enabled in the app binary.
  2. Generating new flash encryption key...: A true random number generator creates the key.
  3. Burning EFUSE...: The key and control bits are permanently written to the eFuses.
  4. Encrypting partition...: The bootloader reads each partition (NVS, PHY, app) into RAM, encrypts it, and writes it back to flash.
  5. Resetting...: A mandatory reset occurs to reload the bootloader with encryption now fully active.
  6. Flash is encrypted: On the second boot, the bootloader confirms that the flash is encrypted and proceeds to load the application normally.

Your device is now encrypted!

Step 5: Updating Firmware on an Encrypted Device

Now that encryption is active, you cannot use a generic esptool.py command to write plaintext binaries. However, the ESP-IDF build system makes this seamless.

Simply run the flash command again:

Bash
idf.py -p (YOUR_PORT) flash

The toolchain automatically detects that the device is encrypted. It communicates with the serial bootloader, which securely accepts the encrypted image and decrypts it using the hardware AES engine before writing it to flash. You do not need to manage the key yourself.

Variant Notes

While the user experience is highly consistent, there are minor underlying differences between ESP32 variants:

Chip Family AES Encryption Mode Key Storage eFuse Block Notes
ESP32 AES-256 (CBC Mode) BLK1 The original implementation. CBC (Cipher Block Chaining) is effective but considered less robust than XTS.
ESP32-S2 AES-128/256 (XTS Mode) BLK_KEY0BLK_KEY5 Introduced the more secure XTS mode, which protects against manipulation and tampering of encrypted data blocks.
ESP32-C3 / ESP32-S3 AES-128 (XTS Mode) Key block location managed by hardware. Continues the use of AES-XTS. The security model is mature and consistent with the ESP32-S2.
ESP32-C6 / ESP32-H2 AES-128 (XTS Mode) Key block location managed by hardware. Modern RISC-V cores that also implement the robust AES-XTS security scheme for flash encryption.
  • ESP32: Uses AES-256 in CBC mode. The key is typically stored in BLK1.
  • ESP32-S2, ESP32-S3, ESP32-C3: Use the more secure AES-XTS (XOR-Encrypt-XOR with Tweakable Block Cipher) mode, which provides better protection against certain cryptographic attacks. The key block may differ (e.g., BLK_KEY0 on S2).
  • ESP32-C6, ESP32-H2: Also support AES-XTS and follow the modern security model. The RISC-V architecture does not change the fundamental flash encryption flow.

Tip: The ESP-IDF framework abstracts these differences. As long as you use the idf.py build and flash tools, the correct procedure for your target chip will be used automatically.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Enabled “Release” Mode Too Early Cannot flash new plaintext firmware. Device may be in a boot loop or unresponsive to standard flashing. Error message might include “secure boot check failed”. This is irreversible. The device is now permanently locked. For development, discard the board. For production, this is the intended secure behavior. Always use “Development” mode until final manufacturing.
Lost Pre-Generated Key You have the encrypted device, but cannot build new firmware for it. The build will fail, or the flashed firmware will not boot because it’s encrypted with the wrong key. There is no recovery. Implement a robust key management and backup policy (e.g., using a Hardware Security Module or a managed vault) if using host-generated keys. Otherwise, prefer on-chip key generation.
JTAG Debugging Fails Debugger (e.g., OpenOCD) fails to connect to the ESP32. You might see errors like “Liberty scan failed” or “could not halt device”. In menuconfigSecurity features, change JTAG setting to “Enable (with restrictions)”. Be aware this slightly reduces the security posture but is necessary for debugging encrypted devices.
Flashing with Third-Party Tools Using a command like esptool.py write_flash ... fails with an error. The device rejects the plaintext binary after encryption is active. Always use the integrated IDF command: idf.py -p (PORT) flash. This tool automatically handles the secure bootloader protocol to update the encrypted device safely.
Device in Boot Loop After Encryption The device continuously resets. The log shows the bootloader starting, but it fails before reaching the application. Often shows “invalid header” or “magic word mismatch”. This could mean a partition was not encrypted correctly or there’s a power issue during the initial encryption. Try a full idf.py erase_flash followed by reflashing the plaintext binary and monitoring the first boot carefully. Ensure a stable power supply.

Exercises

  1. Enable Basic Encryption: Take the gpio_blink example project from the ESP-IDF examples. Enable Flash Encryption in “Development” mode, flash it, and capture the log from the first boot. Identify the lines in the log that confirm key generation and partition encryption.
  2. Perform an Encrypted Update: After completing Exercise 1, change the blink period in the gpio_blink source code. Rebuild and re-flash the project using idf.py flash. Observe how the update succeeds seamlessly. Does the device re-encrypt everything? Why or why not?
  3. Research Pre-Provisioning: Read the official ESP-IDF documentation on host-generated keys. Write a short paragraph explaining a scenario where pre-generating a key and burning it during manufacturing would be preferable to on-chip key generation.

Summary

  • Flash Encryption is a critical hardware-backed feature that protects your firmware and data from being read directly from the SPI flash chip.
  • It uses a device-unique AES key stored in permanent eFuses, which is inaccessible to software.
  • The encryption/decryption process is handled automatically by the hardware, making it transparent to your application.
  • Development Mode is flexible and allows re-encryption with new keys, suitable for the development phase.
  • Release Mode is a permanent, one-way setting for production that locks the device to a single encryption key.
  • ESP-IDF’s build and flash tools (idf.py) abstract away the complexity, automatically handling the encryption process during firmware updates.
  • Always enable security features like Flash Encryption and Secure Boot for production devices to protect your intellectual property.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top