# flash 加密

[English]

本文档旨在引导用户快速了解 ESP32-H21 的 flash 加密功能，通过应用程序代码示例向用户演示如何在开发及生产过程中测试及验证 flash 加密的相关操作。

备注

在本指南中，最常用的命令形式为 `idf.py secure-<command>`，这是对应 `espsecure <command>` 的封装。基于 `idf.py` 的命令能提供更好的用户体验，但与基于 `espsecure` 的命令相比，可能会损失一部分高级功能。

## 概述

flash 加密功能用于加密与 ESP32-H21 搭载使用的片外 flash 中的内容。启用 flash 加密功能后，固件会以明文形式烧录，然后在首次启动时将数据进行加密。因此，物理读取 flash 将无法恢复大部分 flash 内容。

重要

对于生产用途，flash 加密仅应在量产模式下启用。

重要

启用 flash 加密将限制后续 ESP32-H21 更新。在使用 flash 加密功能前，请务必阅读本文档了解其影响。

## 加密分区

启用 flash 加密后，会默认加密以下类型的数据：

- 二级引导加载程序

- 分区表

- NVS 密钥分区

- Otadata

- 所有 `app` 类型的分区

其他类型的数据将视情况进行加密：

- 分区表中标有 `encrypted` 标志的分区。如需了解详情，请参考 加密分区标志。

- 如果启用了安全启动，则会对安全启动引导加载程序摘要进行加密（见下文）。

## 相关 eFuse

flash 加密操作由 ESP32-H21 上的多个 eFuse 控制，具体 eFuse 名称及其描述请参见下表。`espefuse` 工具和基于 `idf.py` 的 eFuse 指令也会使用下表中的 eFuse 名。为了能在 eFuse API 中使用，请在名称前加上 `ESP_EFUSE_`，如：esp_efuse_read_field_bit (ESP_EFUSE_DISABLE_DL_ENCRYPT)。

用于 flash 加密的 eFuse
eFuse | 描述 | 位深
`BLOCK_KEYN` | AES 密钥存储，N 在 0~5 之间。 | 256 位密钥块
`KEY_PURPOSE_N` | 控制 eFuse 块 `BLOCK_KEYN` 的用途，其中 N 在 0~5 之间。对于 flash 加密，唯一的有效值是 `4`，代表 `XTS_AES_128_KEY`。 | 4
`DIS_DOWNLOAD_MANUAL_ENCRYPT` | 设置后，在下载引导模式时禁用 flash 加密。 | 1
`SPI_BOOT_CRYPT_CNT` | 设置 SPI 启动模式后，可启用加密和解密。如果在 eFuse 中设置了一个或三个位，则启用此功能，否则将禁用。 | 3

备注

- 上表中列出的所有 eFuse 位都提供读/写访问控制。

- 这些位的默认值是 0。

对上述 eFuse 位的读写访问由 `WR_DIS` 和 `RD_DIS` 寄存器中的相应字段控制。有关 ESP32-H21 eFuse 的详细信息，请参考 eFuse 管理器。要使用 `idf.py` 更改 eFuse 字段的保护位，请使用以下两个命令：efuse-read-protect 和 efuse-write-protect（`idf.py` 基于 `espefuse` 命令 write-protect-efuse 和 read-protect-efuse 的别名）。例如 `idf.py efuse-write-protect DISABLE_DL_ENCRYPT`。

## flash 的加密过程

假设 eFuse 值处于默认状态，且二级引导加载程序编译为支持 flash 加密，则 flash 加密的具体过程如下：

1. 第一次开机复位时，flash 中的所有数据都是未加密的（明文）。一级 (ROM) 引导加载程序加载二级引导加载程序。

2. 二级引导加载程序将读取 `SPI_BOOT_CRYPT_CNT` eFuse 值 (`0b000`)。因为该值为 0（偶数位），二级引导加载程序将配置并启用 flash 加密块。关于 flash 加密块的更多信息，请参阅 ESP32-H21 技术参考手册。

3. 二级引导加载程序会先判断 eFuse 中是否已经存在有效密钥（例如用 espefuse 工具烧录的密钥），如果存在，则会跳过密钥生成，直接使用现有密钥进行 flash 加密。

4. 如果 eFuse 中不存在可用密钥，则二级引导加载程序使用 RNG（随机数发生器）模块生成 256 位密钥，然后将其写入 `BLOCK_KEYN` eFuse。软件也为存储密钥的块更新了 `KEY_PURPOSE_N`。`BLOCK_KEYN` eFuse 已设置了读写保护位，因此无法通过软件访问密钥。`KEY_PURPOSE_N` 字段也受写保护。flash 加密操作全部在硬件中完成，因此无法通过软件访问密钥。

5. flash 加密块会加密 flash 中的内容，包括二级引导加载程序、应用程序、以及带有 `encrypted` 标志的分区。就地加密需要一定时间，对于大分区来说最多需要一分钟。

6. 二级引导加载程序将 `SPI_BOOT_CRYPT_CNT` (0b001) 中的第一个可用位设置为 1，以标记 flash 内容为加密状态，此时位数为奇数。

7. 对于 开发模式，二级引导加载程序允许 UART 引导加载程序重新烧录加密后的二进制文件。同时，`SPI_BOOT_CRYPT_CNT` eFuse 位不受写入保护。此外，二级引导加载程序默认置位 `DIS_DOWNLOAD_ICACHE`、 `DIS_PAD_JTAG`、 `DIS_USB_JTAG` 和 `DIS_LEGACY_SPI_BOOT`。

8. 对于 量产模式，二级引导加载程序设置所有在开发模式下设置的 eFuse 位以及 `DIS_DOWNLOAD_MANUAL_ENCRYPT`，同时写保护 `SPI_BOOT_CRYPT_CNT` eFuse 位。要修改此行为，请参阅 启用 UART 引导加载程序加密/解密。

9. 重新启动设备以开始执行加密镜像。二级引导加载程序调用 flash 解密块来解密 flash 内容，然后将解密后的内容加载到 IRAM 中。

在开发阶段常需编写不同的明文 flash 镜像并测试 flash 的加密过程。这要求固件下载模式能够根据需求不断加载新的明文镜像。但是，在制造和生产过程中，出于安全考虑，固件下载模式不应有权限访问 flash 内容。

因此需要有两种不同的 flash 加密配置：一种用于开发，另一种用于生产。详情请参考 flash 加密设置 小节。

## flash 加密设置

提供以下 flash 加密模式：

- 开发模式 - 建议仅在开发过程中使用。因为在这种模式下，仍然可以将新的明文固件烧录到设备，并且引导加载程序将使用存储在硬件中的密钥对该固件进行透明加密。此操作间接允许从 flash 中读出固件明文。

- 量产模式 - 推荐用于制造和生产。因为在这种模式下，如果不知道加密密钥，则不可能将明文固件烧录到设备。

本节将详细介绍上述 flash 加密模式，并且逐步说明如何使用它们。

### 开发模式

在开发过程中，可使用 ESP32-H21 内部生成的密钥或外部主机生成的密钥进行 flash 加密。

#### 使用 ESP32-H21 生成的密钥

开发模式允许用户使用固件下载模式下载多个明文镜像。

测试 flash 加密过程需完成以下步骤：

1. 确保你的 ESP32-H21 设备有 相关 eFuse 中所示的 flash 加密 eFuse 的默认设置。

> 请参考如何检查 ESP32-H21 flash 加密状态。

2. 在 项目配置菜单，执行以下操作：

> - 启动时使能 flash 加密。

> - 选择加密模式 （默认是 开发模式）。

> - 选择 UART ROM 下载模式 （默认是 启用）。

> - 选择适当详细程度的引导加载程序日志。

> - 保存配置并退出。

> 启用 flash 加密将增大引导加载程序，因而可能需更新分区表偏移量。请参考 引导加载程序大小。

3. 运行以下命令来构建和烧录完整的镜像。

>     idf.py flash monitor

> 备注

> 这个命令不包括任何应该写入 flash 分区的用户文件。请在运行此命令前手动写入这些文件，否则在写入前应单独对这些文件进行加密。

> 该命令将向 flash 写入未加密的镜像：二级引导加载程序、分区表和应用程序。烧录完成后，ESP32-H21 将复位。在下一次启动时，二级引导加载程序会加密：二级引导加载程序、应用程序分区和标记为 `encrypted` 分区，然后复位。就地加密可能需要时间，对于大分区最多需要一分钟。之后，应用程序在运行时解密并执行。

下面是使用基于 eFuse 的密钥启用 flash 加密后，ESP32-H21 首次启动时的样例输出：

    rst:0x1 (POWERON),boot:0x1f (SPI_FAST_FLASH_BOOT)
    SPIWP:0xee
    mode:DIO, clock div:2
    load:0x40842cf0,len:0x27a4
    load:0x40839580,len:0x944
    load:0x4083ba78,len:0x3f44
    entry 0x408395ee
    I (31) boot: ESP-IDF v5.5-dev-1478-gc15e6e1fb66-dirt 2nd stage bootloader
    I (32) boot: compile time Jan 16 2025 12:04:40
    I (34) boot: chip revision: v0.0
    I (34) boot: efuse block revision: v0.0
    I (37) boot.esp32h21: SPI Speed      : 32MHz
    I (41) boot.esp32h21: SPI Mode       : DIO
    I (45) boot.esp32h21: SPI Flash Size : 4MB
    I (49) boot: Enabling RNG early entropy source...
    I (61) boot: Partition Table:
    I (64) boot: ## Label            Usage          Type ST Offset   Length
    I (70) boot:  0 nvs              WiFi data        01 02 0000e000 00006000
    I (77) boot:  1 storage          Unknown data     01 ff 00014000 00001000
    I (83) boot:  2 factory          factory app      00 00 00020000 00100000
    I (90) boot:  3 nvs_key          NVS keys         01 04 00120000 00001000
    I (96) boot:  4 custom_nvs       WiFi data        01 02 00121000 00006000
    I (103) boot:  5 fat_encrypted    Unknown data     01 81 00127000 00096000
    I (109) boot:  6 fat_not_encr     Unknown data     01 81 001bd000 00096000
    I (116) boot: End of partition table
    I (119) esp_image: segment 0: paddr=00020020 vaddr=42030020 size=0a6e0h ( 42720) map
    I (153) esp_image: segment 1: paddr=0002a708 vaddr=40800000 size=05910h ( 22800) load
    I (165) esp_image: segment 2: paddr=00030020 vaddr=42000020 size=2525ch (152156) map
    I (232) esp_image: segment 3: paddr=00055284 vaddr=40805910 size=0282ch ( 10284) load
    I (239) esp_image: segment 4: paddr=00057ab8 vaddr=40808140 size=00eb8h (  3768) load
    I (248) boot: Loaded app from partition at offset 0x20000
    I (249) boot: Checking flash encryption...
    I (250) efuse: Batch mode of writing fields is enabled
    I (251) flash_encrypt: Using pre-loaded flash encryption key in efuse
    W (257) flash_encrypt: Not disabling UART bootloader encryption
    I (263) flash_encrypt: Disable JTAG...
    I (266) efuse: BURN BLOCK0
    I (271) efuse: BURN BLOCK0 - OK (all write block bits are set)
    I (274) efuse: Batch mode. Prepared fields are committed
    I (279) esp_image: segment 0: paddr=00000020 vaddr=40842cf0 size=027a4h ( 10148)
    I (289) esp_image: segment 1: paddr=000027cc vaddr=40839580 size=00944h (  2372)
    I (294) esp_image: segment 2: paddr=00003118 vaddr=4083ba78 size=03f44h ( 16196)
    I (924) flash_encrypt: bootloader encrypted successfully
    I (1003) flash_encrypt: partition table encrypted and loaded successfully
    I (1004) flash_encrypt: Encrypting partition 1 at offset 0x14000 (length 0x1000)...
    I (1082) flash_encrypt: Done encrypting
    I (1083) esp_image: segment 0: paddr=00020020 vaddr=42030020 size=0a6e0h ( 42720) map
    I (1103) esp_image: segment 1: paddr=0002a708 vaddr=40800000 size=05910h ( 22800)
    I (1115) esp_image: segment 2: paddr=00030020 vaddr=42000020 size=2525ch (152156) map
    I (1182) esp_image: segment 3: paddr=00055284 vaddr=40805910 size=0282ch ( 10284)
    I (1188) esp_image: segment 4: paddr=00057ab8 vaddr=40808140 size=00eb8h (  3768)
    I (1192) flash_encrypt: Encrypting partition 2 at offset 0x20000 (length 0x389a0)...
    I (5680) flash_encrypt: Done encrypting
    I (5681) flash_encrypt: Encrypting partition 3 at offset 0x120000 (length 0x1000)...
    I (5755) flash_encrypt: Done encrypting
    I (5756) flash_encrypt: Encrypting partition 5 at offset 0x127000 (length 0x96000)...
    I (14753) flash_encrypt: Done encrypting
    I (14754) efuse: BURN BLOCK0
    I (14757) efuse: BURN BLOCK0 - OK (all write block bits are set)
    I (14759) flash_encrypt: Flash encryption completed
    I (14760) boot: Resetting with flash encryption enabled...

下面是 ESP32-H21 后续启动时的样例输出，说明 flash 加密已启用

    rst:0x3 (LP_SW_HPSYS),boot:0x1f (SPI_FAST_FLASH_BOOT)
    Saved PC:0x4000321c
    SPIWP:0xee
    mode:DIO, clock div:2
    load:0x40842cf0,len:0x27a4
    load:0x40839580,len:0x944
    load:0x4083ba78,len:0x3f44
    entry 0x408395ee
    I (36) boot: ESP-IDF v5.5-dev-1478-gc15e6e1fb66-dirt 2nd stage bootloader
    I (37) boot: compile time Jan 16 2025 12:04:40
    I (38) boot: chip revision: v0.0
    I (39) boot: efuse block revision: v0.0
    I (42) boot.esp32h21: SPI Speed      : 32MHz
    I (46) boot.esp32h21: SPI Mode       : DIO
    I (50) boot.esp32h21: SPI Flash Size : 4MB
    I (53) boot: Enabling RNG early entropy source...
    I (66) boot: Partition Table:
    I (68) boot: ## Label            Usage          Type ST Offset   Length
    I (74) boot:  0 nvs              WiFi data        01 02 0000e000 00006000
    I (81) boot:  1 storage          Unknown data     01 ff 00014000 00001000
    I (87) boot:  2 factory          factory app      00 00 00020000 00100000
    I (94) boot:  3 nvs_key          NVS keys         01 04 00120000 00001000
    I (101) boot:  4 custom_nvs       WiFi data        01 02 00121000 00006000
    I (107) boot:  5 fat_encrypted    Unknown data     01 81 00127000 00096000
    I (114) boot:  6 fat_not_encr     Unknown data     01 81 001bd000 00096000
    I (121) boot: End of partition table
    I (124) esp_image: segment 0: paddr=00020020 vaddr=42030020 size=0a6e0h ( 42720) map
    I (160) esp_image: segment 1: paddr=0002a708 vaddr=40800000 size=05910h ( 22800) load
    I (173) esp_image: segment 2: paddr=00030020 vaddr=42000020 size=2525ch (152156) map
    I (249) esp_image: segment 3: paddr=00055284 vaddr=40805910 size=0282ch ( 10284) load
    I (257) esp_image: segment 4: paddr=00057ab8 vaddr=40808140 size=00eb8h (  3768) load
    I (266) boot: Loaded app from partition at offset 0x20000
    I (267) boot: Checking flash encryption...
    I (268) flash_encrypt: flash encryption is enabled (1 plaintext flashes left)
    I (271) boot: Disabling RNG early entropy source...
    I (295) cpu_start: Unicore app
    I (300) cpu_start: Pro cpu start user code
    I (304) cpu_start: cpu freq: 32000000 Hz
    I (307) app_init: Application information:
    I (311) app_init: Project name:     flash_encryption
    I (316) app_init: App version:      v5.5-dev-1478-gc15e6e1fb66-dirt
    I (322) app_init: Compile time:     Jan 16 2025 12:04:30
    I (327) app_init: ELF file SHA256:  ba86ca637...
    I (331) app_init: ESP-IDF:          v5.5-dev-1478-gc15e6e1fb66-dirt
    I (337) efuse_init: Min chip rev:     v0.0
    I (341) efuse_init: Max chip rev:     v0.99
    I (345) efuse_init: Chip rev:         v0.0
    I (349) heap_init: Initializing. RAM available for dynamic allocation:
    I (355) heap_init: At 40809F70 len 00043410 (269 KiB): RAM
    I (360) heap_init: At 4084D380 len 00002B60 (10 KiB): RAM
    I (377) spi_flash: detected chip: generic
    I (379) spi_flash: flash io: dio
    W (382) flash_encrypt: Flash encryption mode is DEVELOPMENT (not secure)

在此阶段，如果用户需要更新或重新烧录二进制文件，请参考 重新烧录更新后的分区。

#### 使用主机生成的密钥

可在主机中预生成 flash 加密密钥，并将其烧录到设备中。这样，无需明文 flash 更新便可以在主机上预加密数据并将其烧录。该功能可在 开发模式 和 量产模式 两模式下使用。如果没有预生成的密钥，数据将以明文形式烧录，然后 ESP32-H21 对数据进行就地加密。

备注

不建议在生产中使用该方法，除非为每个设备都单独生成一个密钥。

如需使用主机生成的密钥并将其烧录至设备的 eFuse 中，请完成以下步骤：

1. 确保你的 ESP32-H21 设备有 相关 eFuse 中所示的 flash 加密 eFuse 的默认设置。

> 请参考如何检查 ESP32-H21 flash 加密状态。

2. 通过运行以下命令生成一个随机密钥：

>     idf.py secure-generate-flash-encryption-key my_flash_encryption_key.bin

3. 在第一次加密启动前，使用以下命令将该密钥烧录到设备上，这个操作只能执行 一次。

>     idf.py --port PORT efuse-burn-key BLOCK my_flash_encryption_key.bin XTS_AES_128_KEY

> 其中 `BLOCK` 是 `BLOCK_KEY0` 和 `BLOCK_KEY5` 之间的一个空闲密钥区。

> 如果未烧录密钥并在启用 flash 加密后启动设备，ESP32-H21 将生成一个软件无法访问或修改的随机密钥。

4. 在 项目配置菜单 中进行如下设置：

   - 启动时启用 flash 加密功能

   - 选择加密模式 （默认为 开发模式）

   - 选择适当详细程度的引导加载程序日志

   - 保存配置并退出

> 启用 flash 加密将增大引导加载程序，因而可能需更新分区表偏移量。请参考 引导加载程序大小。

5. 运行以下命令来构建并烧录完整的镜像：

>     idf.py flash monitor

> 备注

> 这个命令不包括任何应该被写入 flash 上的分区的用户文件。请在运行此命令前手动写入这些文件，否则在写入前应单独对这些文件进行加密。

> 该命令将向 flash 写入未加密的镜像：二级引导加载程序、分区表和应用程序。烧录完成后，ESP32-H21 将复位。在下一次启动时，二级引导加载程序会加密：二级引导加载程序、应用程序分区和标记为 `encrypted` 的分区，然后复位。就地加密可能需要时间，对于大的分区来说可能耗时一分钟。之后，应用程序在运行时被解密并执行。

如果使用开发模式，那么更新和重新烧录二进制文件最简单的方法是 重新烧录更新后的分区。

如果使用量产模式，那么可以在主机上预先加密二进制文件，然后将其作为密文烧录。具体请参考 手动加密文件。

#### 重新烧录更新后的分区

如果用户以明文方式更新了应用程序代码并需要重新烧录，则需要在烧录前对其进行加密。请运行以下命令一次完成应用程序的加密与烧录：

    idf.py encrypted-app-flash monitor

如果所有分区都需要以加密形式更新，请运行：

    idf.py encrypted-flash monitor

备注

上述操作仅适用于 `DIS_DOWNLOAD_MANUAL_ENCRYPT` eFuse 位未被烧录的情况。如果该 eFuse 位已被烧录，则需要烧录加密后的密文镜像。

### 量产模式

在量产模式下，UART 引导加载程序无法执行 flash 加密操作，只能 使用 OTA 方案下载新的明文镜像，该方案将在写入 flash 前加密明文镜像。

使用该模式需要执行以下步骤：

1. 确保你的 ESP32-H21 设备有 相关 eFuse 中所示的 flash 加密 eFuse 的默认设置。

> 请参考如何检查 ESP32-H21 flash 加密状态。

2. 在 项目配置菜单，执行以下操作：

> - 启动时使能 flash 加密。

> - 选择量产模式。（注意一旦选择了量产模式，`EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT` eFuse 位将被编程为在 ROM 下载模式下禁用 flash 加密硬件）

> - 选择 UART ROM 下载（推荐永久性的切换到安全模式）。这是默认且推荐使用的选项。如果不需要该模式，也可以改变此配置设置永久地禁用 UART ROM 下载模式。

> - 启用 XTS-AES 伪轮次功能。该选项已默认启用，且配置为最低强度等级，以降低对 flash 加密/解密操作的性能影响。如需了解每个安全等级对性能影响的更多信息，请参考 伪轮次功能。

> - 选择适当详细程度的引导加载程序日志级别。

> - 保存配置并退出。

> 启用 flash 加密将增大引导加载程序，因而可能需更新分区表偏移量。请参考 引导加载程序大小。

3. 运行以下命令来构建并烧录完整的镜像：

>     idf.py flash monitor

> 备注

> 这个命令不包括任何应该被写入 flash 分区的用户文件。请在运行此命令前手动写入这些文件，否则在写入前应单独对这些文件进行加密。

> 该命令将向 flash 写入未加密的镜像：二级引导加载程序、分区表和应用程序。烧录完成后，ESP32-H21 将复位。在下一次启动时，二级引导加载程序会加密：二级引导加载程序、应用程序分区和标记为 `encrypted` 的分区，然后复位。就地加密可能需要时间，对于大的分区来说可能耗时一分钟。之后，应用程序在运行时被解密并执行。

一旦在量产模式下启用 flash 加密，引导加载程序将写保护 `SPI_BOOT_CRYPT_CNT` eFuse。

请使用 OTA 方案 对字段中的明文进行后续更新。

备注

如果用户已经预先生成了 flash 加密密钥并存储了一个副本，并且 UART 下载模式没有通过 CONFIG_SECURE_UART_ROM_DL_MODE 永久禁用，那么可以通过使用 `idf.py secure-encrypt-flash-data --aes-xts` 预加密文件，从而在在本地更新 flash，然后烧录密文。请参考 手动加密文件。

### 最佳实践

在生产中使用 flash 加密时：

- 不要在多个设备之间重复使用同一个 flash 加密密钥，这样攻击者就无法从一台设备上复制加密数据后再将其转移到第二台设备上。

- 如果不需要 UART ROM 下载模式，则应完全禁用该模式，或者永久设置为“安全下载模式”。安全下载模式永久性地将可用的命令限制在更新 SPI 配置、更改波特率、基本的 flash 写入和使用 get-security-info 命令返回当前启用的安全功能摘要。默认在量产模式下第一次启动时设置为安全下载模式。要完全禁用下载模式，请选择 CONFIG_SECURE_UART_ROM_DL_MODE 为“永久禁用 ROM 下载模式（推荐）”或在运行时调用 `esp_efuse_disable_rom_download_mode()`。

- 启用 安全启动 作为额外的保护层，防止攻击者在启动前有选择地破坏 flash 中某部分。

## 外部启用 flash 加密

在上述过程中，对与 flash 加密相关的 eFuse 是通过二级引导加载程序烧写的，或者，也可以借助 `espefuse` 工具烧写 eFuse。如需了解详情，请参考 外部启用 flash 加密。

## 可能出现的错误

一旦启用 flash 加密，`SPI_BOOT_CRYPT_CNT` 的 eFuse 值将设置为奇数位。这意味着所有标有加密标志的分区都会包含加密的密本。如果 ESP32-H21 错误地加载了明文数据，则会出现以下三种典型的错误情况：

1. 如果通过 明文二级引导加载程序镜像 重新烧录了引导加载程序分区，则 ROM 加载器将无法加载二级引导加载程序，并会显示以下错误类型：

>     rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
>     invalid header: 0xb414f76b
>     invalid header: 0xb414f76b
>     invalid header: 0xb414f76b
>     invalid header: 0xb414f76b
>     invalid header: 0xb414f76b
>     invalid header: 0xb414f76b
>     invalid header: 0xb414f76b

> 备注

> 不同应用程序中无效头文件的值不同。

> 备注

> 如果 flash 内容被擦除或损坏，也会出现这个错误。

2. 如果二级引导加载程序已加密，但通过 明文分区表镜像 重新烧录了分区表，引导加载程序将无法读取分区表，从而出现以下错误：

>     rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
>     configsip: 0, SPIWP:0xee
>     clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
>     mode:DIO, clock div:2
>     load:0x3fff0018,len:4
>     load:0x3fff001c,len:10464
>     ho 0 tail 12 room 4
>     load:0x40078000,len:19168
>     load:0x40080400,len:6664
>     entry 0x40080764
>     I (60) boot: ESP-IDF v4.0-dev-763-g2c55fae6c-dirty 2nd stage bootloader
>     I (60) boot: compile time 19:15:54
>     I (62) boot: Enabling RNG early entropy source...
>     I (67) boot: SPI Speed      : 40MHz
>     I (72) boot: SPI Mode       : DIO
>     I (76) boot: SPI Flash Size : 4MB
>     E (80) flash_parts: partition 0 invalid magic number 0x94f6
>     E (86) boot: Failed to verify partition table
>     E (91) boot: load partition table error!

3. 如果引导加载程序和分区表已加密，但使用 明文应用程序镜像 重新烧录了应用程序，引导加载程序将无法加载应用程序，从而出现以下错误：

>     rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
>     configsip: 0, SPIWP:0xee
>     clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
>     mode:DIO, clock div:2
>     load:0x3fff0018,len:4
>     load:0x3fff001c,len:8452
>     load:0x40078000,len:13616
>     load:0x40080400,len:6664
>     entry 0x40080764
>     I (56) boot: ESP-IDF v4.0-dev-850-gc4447462d-dirty 2nd stage bootloader
>     I (56) boot: compile time 15:37:14
>     I (58) boot: Enabling RNG early entropy source...
>     I (64) boot: SPI Speed      : 40MHz
>     I (68) boot: SPI Mode       : DIO
>     I (72) boot: SPI Flash Size : 4MB
>     I (76) boot: Partition Table:
>     I (79) boot: ## Label            Usage          Type ST Offset   Length
>     I (87) boot:  0 nvs              WiFi data        01 02 0000a000 00006000
>     I (94) boot:  1 phy_init         RF data          01 01 00010000 00001000
>     I (102) boot:  2 factory          factory app      00 00 00020000 00100000
>     I (109) boot: End of partition table
>     E (113) esp_image: image at 0x20000 has invalid magic byte
>     W (120) esp_image: image at 0x20000 has invalid SPI mode 108
>     W (126) esp_image: image at 0x20000 has invalid SPI size 11
>     E (132) boot: Factory app partition is not bootable
>     E (138) boot: No bootable app partitions in the partition table

## ESP32-H21 flash 加密状态

确保你的 ESP32-H21 设备有 相关 eFuse 中所示的 flash 加密 eFuse 的默认设置。

要检查你的 ESP32-H21 设备上是否启用了 flash 加密，请执行以下操作之一：

- 将应用示例 security/flash_encryption 烧录到你的设备上。此应用程序会打印 `SPI_BOOT_CRYPT_CNT` eFuse 值，以及是否启用了 flash 加密。

- 查询设备所连接的串口名称，在以下命令中将 `PORT` 替换为串口名称后运行：

      idf.py efuse-summary

## 在加密的 flash 中读写数据

ESP32-H21 应用程序代码可以通过调用函数 `esp_efuse_is_flash_encryption_enabled()` 来检查当前是否启用了 flash 加密。此外，设备可以通过调用函数 `esp_get_flash_encryption_mode()` 来识别 flash 加密模式。

一旦启用 flash 加密，使用代码访问 flash 内容时要更加小心。

### flash 加密范围

当 `SPI_BOOT_CRYPT_CNT` eFuse 设置为奇数位的值，所有通过 MMU 的 flash 缓存访问的 flash 内容都将被透明解密。包括：

- flash 中可执行的应用程序代码 (IROM)。

- 所有存储于 flash 中的只读数据 (DROM)。

- 通过函数 `spi_flash_mmap()` 访问的任意数据。

- ROM 引导加载程序读取的二级引导加载程序镜像。

重要

MMU flash 缓存将无条件解密所有数据。flash 中未加密存储的数据将通过 flash 缓存“被透明解密”，并在软件中存储为随机垃圾数据。

### 读取加密的 flash

如果需要在不使用 flash 缓存 MMU 映射的情况下读取数据，推荐使用分区读取函数 `esp_partition_read()`。该函数只会解密从加密分区读取的数据。从未加密分区读取的数据不会被解密。这样，软件便能以相同的方式访问加密和未加密的 flash。

也可以使用以下 SPI flash API 函数:

- 通过函数 `esp_flash_read()` 读取不会被解密的原（加密）数据。

- 通过函数 `esp_flash_read_encrypted()` 读取和解密数据。

使用非易失性存储器 (NVS) API 存储的数据始终从 flash 加密的角度进行存储和读取解密。如有需要，则由库提供加密功能。详情可参考 NVS 加密。

### 写入加密的 flash

推荐使用分区写入函数 `esp_partition_write()`。此函数只会在将数据写入加密分区时加密数据，而写入未加密分区的数据不会被加密。通过这种方式，软件可以以相同的方式访问加密和非加密 flash。

也可以使用函数 `esp_flash_write_encrypted()` 预加密和写入数据。

此外，ESP-IDF 应用程序中存在但不支持以下 ROM 函数：

- `esp_rom_spiflash_write_encrypted` 预加密并将数据写入 flash

- `SPIWrite` 将未加密的数据写入 flash

由于数据是按块加密的，加密数据最小的写入大小为 16 字节，对齐也是 16 字节。

## 更新加密的 flash

### OTA 更新

如果使用函数 `esp_partition_write()`，对加密分区的 OTA 更新将自动以加密形式写入。

在为已加密设备的 OTA 更新构建应用程序镜像之前，启用项目配置菜单中的 启动时使能 flash 加密 选项。

请参考 OTA 获取更多关于 ESP-IDF OTA 更新的信息。

### 通过串口更新加密 flash

通过串行引导加载程序烧录加密设备，需要串行引导加载程序下载接口没有通过 eFuse 被永久禁用。

在开发模式下，推荐的方法是 重新烧录更新后的分区。

在量产模式下，如果主机上有存储在 eFuse 中的相同密钥的副本，那么就可以在主机上对文件进行预加密，然后进行烧录，具体请参考 手动加密文件。

## 关闭 flash 加密

如果意外启用了 flash 加密，则明文数据的 flash 会使 ESP32-H21 无法正常启动。设备将不断重启，并报错 `flash read err, 1000` 或 `invalid header: 0xXXXXXX`。

对于开发模式下的 flash 加密，可以通过烧录 `SPI_BOOT_CRYPT_CNT` efuse 来关闭加密。每个芯片仅有 1 次机会，请执行以下步骤：

1. 在 项目配置菜单 中，禁用 启动时使能 flash 加密 选项，然后保存并退出。

2. 再次打开项目配置菜单，再次检查你是否已经禁用了该选项，如果这个选项仍被启用，引导加载程序在启动时将立即重新启用加密功能。

3. 在禁用 flash 加密后，通过运行 `idf.py flash` 来构建和烧录新的引导加载程序和应用程序。

4. 使用 `idf.py` 来关闭 `SPI_BOOT_CRYPT_CNT`，请运行以下命令：

>     idf.py efuse-burn SPI_BOOT_CRYPT_CNT

重置 ESP32-H21，flash 加密应处于关闭状态，引导加载程序将正常启动。

## flash 加密的要点

- 使用 XTS-AES-128 加密 flash。flash 加密密钥为 256 位，存储于芯片内部的 `BLOCK_KEYN` eFuse 块中。默认情况下，该密钥受硬件保护，软件无法访问。

- 通过 ESP32-H21 的 flash 缓存映射功能，flash 可支持透明访问——任何映射到地址空间的 flash 区域在读取时都将被透明地解密。

  为便于访问，某些数据分区最好保持未加密状态，或者也可使用对已加密数据无效的 flash 友好型更新算法。由于 NVS 库无法与 flash 加密直接兼容，因此无法加密非易失性存储器的 NVS 分区。详情可参见 NVS 加密。

- 如果以后可能需要启用 flash 加密，则编程人员在编写 使用加密 flash 代码时需小心谨慎。

- 如果已启用安全启动，重新烧录加密设备的引导加载程序则需要“可重新烧录”的安全启动摘要（可参考 flash 加密与安全启动）。

启用 flash 加密将增大引导加载程序，因此可能需更新分区表偏移量。请参考 引导加载程序大小。

重要

在首次启动加密过程中，请勿切断 ESP32-H21 的电源。如果电源被切断，flash 的内容将受到破坏，并需要重新烧录未加密数据。而这类重新烧录将不计入烧录限制次数。

## flash 加密的局限性

flash 加密可以保护固件，防止未经授权的读取与修改。了解 flash 加密系统的局限之处亦十分重要：

- flash 加密功能与密钥同样稳固。因而，推荐在首次启动设备时，在设备上生成密钥（默认行为）。如果在设备外生成密钥，请确保遵循正确的后续步骤，不要在所有生产设备之间使用相同的密钥。

- 并非所有数据都是加密存储。因而在 flash 上存储数据时，请检查你使用的存储方式（库、API 等）是否支持 flash 加密。

- flash 加密无法防止攻击者获取 flash 的高层次布局信息。这是因为每对相邻的 16 字节 AES 块都使用相邻的 AES 密钥。当这些相邻的 16 字节块中包含相同内容时（如空白或填充区域），这些字节块将加密以产生匹配的加密块对。这让攻击者可在加密设备间进行高层次对比（例如，确认两设备是否可能运行相同的固件版本）。

- 单独使用 flash 加密可能无法防止攻击者修改本设备的固件。为防止设备上运行未经授权的固件，可搭配 flash 加密使用 安全启动。

## flash 加密与安全启动

推荐 flash 加密与安全启动搭配使用。但是，如果已启用安全启动，则重新烧录设备时会受到其他限制：

- 如果新的应用程序已使用安全启动签名密钥正确签名，则 OTA 更新 不受限制。

## flash 加密的高级功能

以下部分介绍了 flash 加密的高级功能。

### 加密分区标志

部分分区默认为已加密。通过在分区的标志字段中添加 “encrypted” 标志，可在分区表描述中将其他分区标记为需要加密。在这些标记分区中的数据会和应用程序分区一样视为加密数据。

    # Name,   Type, SubType, Offset,  Size, Flags
    nvs,      data, nvs,     0x9000,  0x6000
    phy_init, data, phy,     0xf000,  0x1000
    factory,  app,  factory, 0x10000, 1M
    secret_data, 0x40, 0x01, 0x20000, 256K, encrypted

请参考 分区表 获取更多关于分区表描述的具体信息。

关于分区加密，还需了解以下信息：

- 默认分区表都不包含任何加密数据分区。

- 启用 flash 加密后，"app" 分区一般都视为加密分区，因此无需标记。

- 如果未启用 flash 加密，则 "encrypted" 标记无效。

- 将可选 `phy` 分区标记为 "encrypted"，可以防止物理访问读取或修改 `phy_init` 数据。

- `nvs` 分区无法标记为 "encrypted" 因为 NVS 库与 flash 加密不直接兼容。

### 启用 UART 引导加载程序加密/解密

在第一次启动时，flash 加密过程默认会烧录以下 eFuse：

- `DIS_DOWNLOAD_MANUAL_ENCRYPT` 在 UART 引导加载程序启动模式下运行时，禁止 flash 加密操作。

- `DIS_PAD_JTAG` 和 `DIS_USB_JTAG` 禁用 JTAG。

- `DIS_DIRECT_BOOT``（即之前的 ``DIS_LEGACY_SPI_BOOT`）禁用传统的 SPI 启动模式。

为了能启用这些功能，可在首次启动前仅烧录部分 eFuse，并用未设置值 0 写保护其他部分。例如:

    idf.py --port PORT efuse-burn DIS_DOWNLOAD_MANUAL_ENCRYPT
    idf.py --port PORT efuse-write-protect DIS_DOWNLOAD_MANUAL_ENCRYPT

备注

请注意在写保护前设置所有适当的位！

一个位可以控制三个 eFuse 的写保护，这意味着写保护一个 eFuse 位将写保护所有未设置的 eFuse 位。

由于 `esptool` 目前不支持读取加密 flash，所以对这些 eFuse 进行写保护从而使其保持未设置目前来说并不是很有用。

### JTAG 调试

默认情况下，当启用 flash 加密（开发或量产模式）时，将通过 eFuse 禁用 JTAG 调试。引导加载程序在首次启动时执行此操作，同时启用 flash 加密。

请参考 JTAG 与 flash 加密和安全引导 了解更多关于使用 JTAG 调试与 flash 加密的信息。

### 手动加密文件

手动加密或解密文件需要在 eFuse 中预烧录 flash 加密密钥（请参阅 使用主机生成的密钥）并在主机上保留一份副本。如果 flash 加密配置在开发模式下，则不需要保留密钥的副本或遵循以下步骤，可以使用更简单的 重新烧录更新后的分区 步骤。

密钥文件应该是单个原始二进制文件（例如：`key.bin`）。

例如，以下是将文件 `my-app.bin` 进行加密、烧录到偏移量 0x10000 的步骤。如下所示，请运行 `idf.py`:

    idf.py secure-encrypt-flash-data --aes-xts --keyfile /path/to/key.bin --address 0x10000 --output my-app-ciphertext.bin my-app.bin

然后可以使用 `esptool` 将文件 `my-app-ciphertext.bin` 写入偏移量 0x10000。 关于为 `esptool` 推荐的所有命令行选项，请查看 idf.py build 成功时打印的输出。

备注

如果 ESP32-H21 在启动时无法识别烧录进去的密文文件，请检查密钥是否匹配以及命令行参数是否完全匹配，包括偏移量是否正确。

`idf.py decrypt-flash-data` 命令可以使用同样的选项（和不同的输入/输出文件）来解密 flash 密文或之前加密的文件。

## 技术细节

以下章节将提供 flash 加密操作的相关信息。

- 有关在 Python 中实现的完整 flash 加密算法，可参见 `espsecure` 源代码中的函数 `_flash_encryption_operation()`。

### flash 加密算法

- ESP32-H21 使用 XTS-AES 块密码模式进行 flash 加密，密钥大小为 256 位。

- XTS-AES 是一种专门为光盘加密设计的块密码模式，它解决了其它潜在模式如 AES-CTR 在此使用情景下的不足。有关 XTS-AES 算法的详细描述，请参考 IEEE Std 1619-2007。

- flash 加密的密钥存储于一个 `BLOCK_KEYN` eFuse 中，默认受保护防止进一步写入或软件读取。

- 有关在 Python 中实现的完整 flash 加密算法，可参见 `espsecure` 源代码中的函数 `_flash_encryption_operation()`。

## 防御侧信道攻击

### 伪轮次功能

ESP32-H21 在 XTS-AES 外设中引入了伪轮次功能，使该外设可以在原始操作轮次之前和之后随机插入伪轮次，并生成伪密钥以执行这些虚拟操作。 这些操作不会改变原始结果，但通过随机化功率曲线，增加了实施侧信道分析攻击的复杂性。

可以通过 CONFIG_SECURE_FLASH_PSEUDO_ROUND_FUNC_STRENGTH 选择伪轮次功能的强度。提高强度会增强该功能所提供的安全性，但也会降低 XTS-AES 操作的速度。

不同强度的伪轮次对 XTS-AES 操作性能的影响
强度 | 性能影响 [1]
--- | ---
低 | < 0.5 %
中 | 6.2 %
高 | 18 %

[1]

上述性能数据由 HAL 加密测试应用中的 XTS-AES 测试程序 test_xts_aes.c 计算得出。

可以根据实际用例需求配置伪轮次功能的强度。例如，提高强度以提供更高的安全性，同时减慢 flash 加解密操作的速度。

考虑到上述性能影响，ESP-IDF 默认为伪轮次功能启用了低强度配置，以尽量减少性能影响。

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。