Wiki / Concepts

NVS (ESP32)

ESP-IDF's key-value store in a dedicated flash partition, where Wi-Fi credentials, tokens, and config persist across reboots, plaintext unless encryption is explicitly enabled.

Layout of one 4 KB NVS page
Page header 32 B
Entry state bitmap 32 B
Entries (126 x 32 B) 4032 B
An NVS partition is a series of 4096-byte pages. Each page opens with a 32-byte header (state + sequence number), a 32-byte bitmap giving each of the 126 entry slots a 2-bit state (empty / written / erased), then the 32-byte entries themselves. Keys are stored as plaintext strings; this structure is what nvs_partition_gen.py and esp32_nvs parse.

NVS (Non-Volatile Storage) is the ESP-IDF key-value store, held in a dedicated flash partition, that firmware uses to persist small pieces of data, Wi-Fi credentials, device tokens, calibration, and config, across reboots and power loss. It is the ESP32's answer to "where do I keep settings", and for an attacker it is usually the richest thing in a flash dump.

What it is

NVS is a simple, wear-levelled key-value database that lives in a partition of type data, subtype nvs, declared in the ESP32 partition table. Data is grouped into namespaces (a string like "wifi" or "mqtt"), and within each namespace stored as typed key-value entries (integers, strings, blobs). Physically the partition is a chain of 4096-byte pages; each page carries a header, a per-entry state bitmap, and up to 126 fixed-size 32-byte entry slots (larger values span multiple slots). See the diagram for the layout of a single page.

Why it matters

NVS is where ESP32 firmware stashes the runtime secrets that are not in the code itself, and that makes it the first place to look after a dump. Wi-Fi SSIDs and PSKs, cloud API keys, provisioning tokens, MQTT passwords, device identifiers, they land in NVS because the firmware needs them to survive a reboot. Dump the flash of a device whose security fuses are un-burned and the NVS partition very often hands the Wi-Fi keys and cloud credentials straight over, in plaintext.

How it works

The store organises data into namespaces and key-value entries inside flash pages, with a lightweight wear-levelling and CRC scheme so writes spread across the partition and survive power failure mid-write. A page's state bitmap marks each entry slot as empty, written, or erased, which is how NVS reclaims space without rewriting whole pages. By default NVS is not encrypted: the keys and values are readable plaintext unless flash encryption or the separate NVS encryption feature is explicitly enabled, in which case the entries are stored as ciphertext under a key held in eFuse.

From an attacker's view

The offline path is: dump the flash over UART download mode, locate the nvs partition from the partition table, carve it, and parse it.

# 1. Dump the whole flash over the UART download bootloader.
esptool.py -p /dev/ttyUSB0 read_flash 0x0 0x400000 dump.bin

# 2. Read the partition table to find the nvs partition offset and size.
gen_esp32part.py dump.bin                 # or parse the table at 0x8000

# 3. Carve the nvs partition, then parse the pages, or just grep it.
dd if=dump.bin of=nvs.bin bs=1 skip=$((0x9000)) count=$((0x6000))
strings -n6 nvs.bin | grep -iE 'ssid|psk|pass|token|mqtt|http'

For a structured read, nvs_partition_gen.py (ships with ESP-IDF) and the third-party esp32_nvs parser walk the page/entry structure and print namespaces and typed values. A plain strings pass is often enough to spot plaintext Wi-Fi keys. If the partition is encrypted, you first need the NVS key partition or the eFuse-held flash-encryption key, which a running-chip attack or a side channel may or may not yield.

Pitfalls

  • NVS is not a secret store. Plaintext by default is the norm, not the exception, treat anything in it as readable unless you have confirmed encryption is on.
  • The partition offset is not fixed, it comes from the target's own partition table (commonly 0x9000, but always verify). Do not hardcode 0x9000 blindly.
  • Deleted keys may linger: NVS marks entries erased in the bitmap rather than wiping them immediately, so old credentials can survive in the raw dump until the page is compacted.

Further reading