NVS Architecture
theoryWhat Is NVS?
NVS stands for Non-Volatile Storage. It is a key-value store implemented directly in the ESP32's flash memory by Espressif's ESP-IDF framework. The purpose is simple: give application developers a reliable way to persist small pieces of data across reboots and power cycles, without needing an external EEPROM or a full filesystem.
From the application developer's perspective, NVS behaves like a dictionary: you open a named namespace, write a key-value pair, close the handle, and that value survives a reset. From a forensic perspective, NVS is a structured binary format sitting at a known location in flash — and it can contain anything the developer chose to store there.
// What a developer writes
nvs_handle_t handle;
nvs_open("wifi", NVS_READWRITE, &handle);
nvs_set_str(handle, "password", "MyWifiPass123");
nvs_commit(handle);
nvs_close(handle);
// What you will find in flash
// → namespace: "wifi", key: "password", value: "MyWifiPass123"
That string is sitting in flash right now on millions of shipped ESP32 devices.
Why NVS Exists: Flash Write Cycle Constraints
NAND and NOR flash memory have a fundamental physical limitation: each storage cell can only be erased and rewritten a finite number of times before it degrades. For the SPI NOR flash used by ESP32 devices, the typical endurance spec is 100,000 erase cycles per sector.
A sector is 4KB. If an application naively wrote its WiFi password to the same 4KB sector every time it changed, that sector would fail after 100,000 updates. In a device that reconnects to WiFi frequently, that could happen within months.
NVS solves this through wear leveling: it distributes writes across all pages in the NVS partition rather than always writing to the same location. When a page fills up, NVS moves to the next available page. When all pages are used, the oldest clean pages are reclaimed. The developer never sees this complexity — it happens transparently inside the NVS library.
The consequence for forensics: old data is not immediately overwritten. A page that NVS has moved away from may still contain its previous entries in flash. Deleted keys are marked as erased in the page bitmap but the underlying data bytes may persist until the page is explicitly reclaimed and erased. This is a well-known source of forensic evidence in NVS dumps.
NVS Internal Structure
NVS organizes flash into a sequence of pages, each exactly 4096 bytes (one flash erase sector). A typical default NVS partition (24KB = 0x6000 bytes) contains six pages.
Page Layout
Each NVS page has this structure:
Offset Size Field
──────────────────────────────────────────────────────────
0x000 32B Page header
0x020 32B Entry state bitmap (tracks status of all 126 entries)
0x040 4032B Entry storage (126 entries × 32 bytes each)
Page Header
The page header (32 bytes) contains:
| Field | Size | Description |
|---|---|---|
| Magic | 4B | 0xA5A5A5A5 — identifies a valid NVS page |
| Sequence number | 4B | Monotonically increasing, used for wear leveling ordering |
| Version | 1B | NVS format version |
| State | 1B | 0xFF = empty, 0xFE = active, 0xFC = full, 0xF8 = freeing |
| CRC32 | 4B | Covers the header fields |
| Reserved | 16B | Zero-filled |
If the magic is not 0xA5A5A5A5, the page has either never been written or has been erased. An erased NVS partition will show all 0xFF bytes.
Entry State Bitmap
Immediately after the header sits a 32-byte bitmap that tracks the state of each of the 126 possible entries on the page. Each entry uses 2 bits:
11— Empty (available for writing)10— Written (active entry)00— Erased (deleted entry, data may still be present in flash)01— Invalid
This bitmap is where you will find evidence of deleted keys. An entry marked 00 (erased) in the bitmap but still containing readable data bytes is a classic NVS artifact.
Entry Structure
Each entry is 32 bytes:
Offset Size Field
──────────────────────────────────────────────────────────
0x00 1B Namespace index (which namespace this entry belongs to)
0x01 1B Type (see type table below)
0x02 1B Span (number of consecutive entries used by this value)
0x03 1B Chunk index (for blob multi-entry sequences)
0x04 4B CRC32 of this entry
0x08 8B Key (null-terminated string, max 15 chars + null = 16B)
0x18 8B Value (for small types) or data chunk (for strings/blobs)
The key is capped at 15 characters plus a null terminator. The namespace name is also capped at 15 characters. These limits exist because the key field is exactly 8 bytes — wait, note that the key occupies bytes 0x08–0x17 (16 bytes total), allowing 15 printable characters plus a null byte.
Entry Types
NVS supports the following data types:
| Type Code | Type Name | Size | Description |
|---|---|---|---|
0x01 |
uint8 |
1B | Unsigned 8-bit integer |
0x11 |
int8 |
1B | Signed 8-bit integer |
0x02 |
uint16 |
2B | Unsigned 16-bit integer |
0x12 |
int16 |
2B | Signed 16-bit integer |
0x04 |
uint32 |
4B | Unsigned 32-bit integer |
0x14 |
int32 |
4B | Signed 32-bit integer |
0x08 |
uint64 |
8B | Unsigned 64-bit integer |
0x18 |
int64 |
8B | Signed 64-bit integer |
0x21 |
string |
variable | Null-terminated string, uses span field |
0x41 |
blob |
variable | Binary data, uses span and chunk_index |
0x42 |
blob_data |
variable | Continuation chunk of a blob |
0x48 |
blob_idx |
8B | Index entry for a blob |
For integer types, the value fits entirely in the 8-byte value field of the entry. For strings and blobs, the span field tells the parser how many consecutive 32-byte entries are consumed by this value. A 60-byte string needs ceiling(60/32) = 2 additional data entries, so span = 3 (1 header + 2 data).
Namespaces
Namespaces are NVS's mechanism for organizing keys — think of them as directories. Each namespace is a string identifier (max 15 chars), and all key-value pairs are scoped within a namespace.
How namespaces work internally:
Namespaces are themselves stored as NVS entries, using the reserved namespace index 0 (the NVS system namespace). Each namespace entry assigns a numeric index (1–254) to the namespace name string. Subsequent data entries in that namespace reference this numeric index in their namespace_index field.
Entry 0: namespace_index=0, type=string, key="wifi", value → index=1
Entry 1: namespace_index=1, type=string, key="ssid", value="HomeNetwork"
Entry 2: namespace_index=1, type=string, key="password", value="SuperSecret99"
Entry 3: namespace_index=0, type=string, key="device", value → index=2
Entry 4: namespace_index=2, type=string, key="serial", value="ESP-A1B2C3"
This means when parsing NVS manually, you first collect all namespace entries (those with namespace_index=0) to build the namespace index-to-name mapping, then parse data entries using that map.
Default Partition Locations
In the ESP-IDF default partition table, NVS lives at:
| Partition | Type | Offset | Size |
|---|---|---|---|
nvs |
data/nvs | 0x9000 |
0x6000 (24KB) |
phy_init |
data/phy | 0xF000 |
0x1000 (4KB) |
factory |
app/factory | 0x10000 |
varies |
The NVS partition starts at byte offset 36864 (0x9000) from the beginning of the flash chip. On a 4MB flash image, you can extract it with:
dd if=full_dump.bin bs=1 skip=$((0x9000)) count=$((0x6000)) of=nvs.bin
Custom partition tables can place NVS anywhere, which is why reading the partition table first (Lesson 3) is the correct workflow.
NVS Encryption
ESP-IDF supports encrypting NVS contents using AES-XTS (AES with XEX-based Tweaked CodeBook mode with ciphertext Stealing). When NVS encryption is enabled:
- A separate
nvs_keyspartition stores the AES key material (typically at an offset like0x310000) - All NVS entry data — both keys and values — are encrypted
- Page headers and bitmaps are still partially readable (the magic and sequence number remain) but entry content is ciphertext
- The
nvs_keyspartition itself may be protected by Flash Encryption (a separate ESP32 feature that encrypts the entire flash using a hardware key stored in eFuses)
When you encounter an NVS partition where entries look like random bytes despite correct magic numbers in page headers, you are looking at encrypted NVS. If Flash Encryption is not enabled, the nvs_keys partition is readable in plaintext and you can decrypt NVS. If Flash Encryption is enabled, recovering the key requires accessing the hardware eFuses — a much harder target.
In practice, most consumer ESP32 products do not enable NVS encryption. The developer wrote credentials into NVS, shipped the device, and left the flash readable.
Why NVS Is a Forensic Goldmine
Consider what ESP32 developers actually store in NVS. These are patterns from real, publicly available ESP-IDF projects:
// WiFi provisioning (ESP-IDF WiFi station code)
nvs_set_str(handle, "ap_ssid", config.wifi_ssid);
nvs_set_str(handle, "ap_pass", config.wifi_password);
// MQTT broker credentials (IoT telemetry devices)
nvs_set_str(handle, "mqtt_user", "device_001");
nvs_set_str(handle, "mqtt_pass", "Br0k3rS3cr3t!");
nvs_set_str(handle, "mqtt_host", "mqtt.company-internal.com");
// AWS IoT (common in commercial products)
nvs_set_str(handle, "aws_endpoint", "a1b2c3d4e5.iot.us-east-1.amazonaws.com");
nvs_set_blob(handle, "client_cert", cert_pem, cert_len);
nvs_set_blob(handle, "private_key", key_pem, key_len);
// Device identity
nvs_set_str(handle, "serial", "SN-20240315-A7F2");
nvs_set_str(handle, "activation", "ACT-5XK9-M2P1-ZRQW");
// HTTP API tokens
nvs_set_str(handle, "api_key", "sk-prod-a8f3c1d9e2b6...");
None of this is hypothetical. These patterns appear in ESP-IDF projects on GitHub, in production IoT products sold on Amazon, and in industrial sensors deployed in factory networks.
The developer's mental model is: "This is stored in flash, users can't easily read flash." The hardware hacker's mental model is: "This is stored in flash, I have esptool and a USB cable."
Key Takeaway
NVS is a structured, well-documented binary format at a predictable flash offset. It contains whatever the developer persisted — including credentials that were never intended to be externally readable. The format is open, the tools are free, and the data is sitting there on every shipped ESP32 device that hasn't enabled Flash Encryption. This is what we are going to extract in the following lessons.