flash 加密

[English]

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

概述

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

安全启动 是一个独立的功能,可以与 flash 加密一起使用,从而创建更安全的环境。

重要

对于生产用途,flash 加密仅应在“发布”模式下启用。

重要

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

Encrypted Partitions

启用 flash 加密后,系统将默认加密下列类型的 flash 数据:

  • 固件引导加载程序

  • 分区表

  • NVS 密钥分区

  • Otadata

  • 所有 “app” 类型的分区

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

  • 任何在分区表中标有“加密”标志的分区。详情请见 加密分区标志

  • 如果启用了安全启动,则可以加密安全启动引导程序摘要(见下文)。

相关 eFuses

flash 加密操作由 ESP32 上的多个 eFuse 控制。以下是这些 eFuse 列表及其描述,下表中的各 eFuse 名称也在 espefuse.py 工具中使用,为了能在 eFuse API 中使用,请在名称前加上 ESP_EFUSE_,如:esp_efuse_read_field_bit(ESP_EFUSE_DISABLE_DL_ENCRYPT)。

flash 加密过程中使用的 eFuses

eFuse

描述

位深

CODING_SCHEME

控制用于产生最终 256 位 AES 密钥的 block1 的实际位数。可能的值:0 代表 256 位,1 代表 192 位,2 代表 128 位。最终的 AES 密钥根据 FLASH_CRYPT_CONFIG 值得出。

2

flash_encryption (block1)

AES 密钥存储。

256 位密钥块

FLASH_CRYPT_CONFIG

控制 AES 加密过程。

4

DISABLE_DL_ENCRYPT

设置后,在固件下载模式运行时禁用 flash 加密操作。

1

DISABLE_DL_DECRYPT

设置后,在 UART 固件下载模式运行时禁用 flash 解密操作。

1

FLASH_CRYPT_CNT

在启动时启用/禁用加密。如果设置了偶数个比特位 (0、2、4、6),则在启动时加密 flash。如果设置了奇数个比特位 (1、3、5、7),则在启动时不加密 flash。

7

备注

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

  • 这些位的默认值是 0。

对上述 eFuse 位的读写访问由 WR_DISRD_DIS 寄存器中的相应字段控制。有关 ESP32 eFuse 的详细信息,请参考 eFuse 管理器。要使用 espefuse.py 更改 eFuse 字段的保护位,请使用以下两个命令:read_protect_efuse 和 write_protect_efuse。例如 espefuse.py write_protect_efuse DISABLE_DL_ENCRYPT

flash 的加密过程

假设 eFuse 值处于默认状态,且固件的引导加载程序编译为支持 flash 加密,则 flash 加密的具体过程如下:

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

  2. 固件的引导加载程序将读取 FLASH_CRYPT_CNT eFuse 值 (0b0000000)。因为该值为 0(偶数位),固件的引导加载程序将配置并启用 flash 加密块,同时将 FLASH_CRYPT_CONFIG eFuse 的值编程为 0xF。关于 flash 加密块的更多信息,请参考 ESP32 技术参考手册 > eFuse 控制器(eFuse) > flash 加密块 [PDF]。

  3. 固件的引导加载程序使用 RNG(随机数生成)模块生成 AES-256 位密钥,然后将其写入 flash_encryption eFuse 中。由于 flash_encryption eFuse 已设置编写和读取保护位,将无法通过软件访问密钥。flash 加密操作完全在硬件中完成,无法通过软件访问密钥。

  4. flash 加密块将加密 flash 的内容(固件的引导加载程序、应用程序、以及标有“加密”标志的分区)。就地加密可能会耗些时间(对于大分区最多需要一分钟)。

  5. 固件引导加载程序将在 FLASH_CRYPT_CNT (0b0000001) 中设置第一个可用位来对已加密的 flash 内容进行标记。设置奇数个比特位。

  6. 对于 开发模式,固件引导加载程序仅设置 DISABLE_DL_DECRYPTDISABLE_DL_CACHE 的 eFuse 位,以便 UART 引导加载程序重新烧录加密的二进制文件。此外, FLASH_CRYPT_CNT 的 eFuse 位不受写入保护。

  7. 对于 发布模式,固件引导加载程序设置 DISABLE_DL_ENCRYPTDISABLE_DL_DECRYPTDISABLE_DL_CACHE 的 eFuse 位为 1,以防止 UART 引导加载程序解密 flash 内容。它还写保护 FLASH_CRYPT_CNT eFuse 位。要修改此行为,请参阅 启用 UART 引导加载程序加密/解密

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

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

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

flash 加密设置

提供以下 flash 加密模式:

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

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

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

开发模式

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

使用 ESP32 生成的密钥

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

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

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

请参考如何检查 ESP32 flash 加密状态

  1. 项目配置菜单,执行以下操作:

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

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

idf.py flash monitor

备注

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

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

下面是启用 flash 加密后 ESP32 首次启动时的样例输出:

--- idf_monitor on /dev/cu.SLAB_USBtoUART 115200 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
ets Jun  8 2016 00:22:57

rst:0x1 (POWERON_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:13608
load:0x40080400,len:6664
entry 0x40080764
I (28) boot: ESP-IDF v4.0-dev-850-gc4447462d-dirty 2nd stage bootloader
I (29) boot: compile time 15:37:14
I (30) boot: Enabling RNG early entropy source...
I (35) boot: SPI Speed      : 40MHz
I (39) boot: SPI Mode       : DIO
I (43) boot: SPI Flash Size : 4MB
I (47) boot: Partition Table:
I (51) boot: ## Label            Usage          Type ST Offset   Length
I (58) boot:  0 nvs              WiFi data        01 02 0000a000 00006000
I (66) boot:  1 phy_init         RF data          01 01 00010000 00001000
I (73) boot:  2 factory          factory app      00 00 00020000 00100000
I (81) boot: End of partition table
I (85) esp_image: segment 0: paddr=0x00020020 vaddr=0x3f400020 size=0x0808c ( 32908) map
I (105) esp_image: segment 1: paddr=0x000280b4 vaddr=0x3ffb0000 size=0x01ea4 (  7844) load
I (109) esp_image: segment 2: paddr=0x00029f60 vaddr=0x40080000 size=0x00400 (  1024) load
0x40080000: _WindowOverflow4 at esp-idf/esp-idf/components/freertos/xtensa_vectors.S:1778

I (114) esp_image: segment 3: paddr=0x0002a368 vaddr=0x40080400 size=0x05ca8 ( 23720) load
I (132) esp_image: segment 4: paddr=0x00030018 vaddr=0x400d0018 size=0x126a8 ( 75432) map
0x400d0018: _flash_cache_start at ??:?

I (159) esp_image: segment 5: paddr=0x000426c8 vaddr=0x400860a8 size=0x01f4c (  8012) load
0x400860a8: prvAddNewTaskToReadyList at esp-idf/esp-idf/components/freertos/tasks.c:4561

I (168) boot: Loaded app from partition at offset 0x20000
I (168) boot: Checking flash encryption...
I (168) flash_encrypt: Generating new flash encryption key...
I (187) flash_encrypt: Read & write protecting new key...
I (187) flash_encrypt: Setting CRYPT_CONFIG efuse to 0xF
W (188) flash_encrypt: Not disabling UART bootloader encryption
I (195) flash_encrypt: Disable UART bootloader decryption...
I (201) flash_encrypt: Disable UART bootloader MMU cache...
I (208) flash_encrypt: Disable JTAG...
I (212) flash_encrypt: Disable ROM BASIC interpreter fallback...
I (219) esp_image: segment 0: paddr=0x00001020 vaddr=0x3fff0018 size=0x00004 (     4)
I (227) esp_image: segment 1: paddr=0x0000102c vaddr=0x3fff001c size=0x02104 (  8452)
I (239) esp_image: segment 2: paddr=0x00003138 vaddr=0x40078000 size=0x03528 ( 13608)
I (249) esp_image: segment 3: paddr=0x00006668 vaddr=0x40080400 size=0x01a08 (  6664)
I (657) esp_image: segment 0: paddr=0x00020020 vaddr=0x3f400020 size=0x0808c ( 32908) map
I (669) esp_image: segment 1: paddr=0x000280b4 vaddr=0x3ffb0000 size=0x01ea4 (  7844)
I (672) esp_image: segment 2: paddr=0x00029f60 vaddr=0x40080000 size=0x00400 (  1024)
0x40080000: _WindowOverflow4 at esp-idf/esp-idf/components/freertos/xtensa_vectors.S:1778

I (676) esp_image: segment 3: paddr=0x0002a368 vaddr=0x40080400 size=0x05ca8 ( 23720)
I (692) esp_image: segment 4: paddr=0x00030018 vaddr=0x400d0018 size=0x126a8 ( 75432) map
0x400d0018: _flash_cache_start at ??:?

I (719) esp_image: segment 5: paddr=0x000426c8 vaddr=0x400860a8 size=0x01f4c (  8012)
0x400860a8: prvAddNewTaskToReadyList at esp-idf/esp-idf/components/freertos/tasks.c:4561

I (722) flash_encrypt: Encrypting partition 2 at offset 0x20000...
I (13229) flash_encrypt: Flash encryption completed
I (13229) boot: Resetting with flash encryption enabled...

启用 flash 加密后,在下次启动时输出将显示已启用 flash 加密,样例输出如下:

  rst:0x1 (POWERON_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:13652
  ho 0 tail 12 room 4
  load:0x40080400,len:6664
  entry 0x40080764
  I (30) boot: ESP-IDF v4.0-dev-850-gc4447462d-dirty 2nd stage bootloader
  I (30) boot: compile time 16:32:53
  I (31) boot: Enabling RNG early entropy source...
  I (37) boot: SPI Speed      : 40MHz
  I (41) boot: SPI Mode       : DIO
  I (45) boot: SPI Flash Size : 4MB
  I (49) boot: Partition Table:
  I (52) boot: ## Label            Usage          Type ST Offset   Length
  I (60) boot:  0 nvs              WiFi data        01 02 0000a000 00006000
  I (67) boot:  1 phy_init         RF data          01 01 00010000 00001000
  I (75) boot:  2 factory          factory app      00 00 00020000 00100000
  I (82) boot: End of partition table
I (86) esp_image: segment 0: paddr=0x00020020 vaddr=0x3f400020 size=0x0808c ( 32908) map
  I (107) esp_image: segment 1: paddr=0x000280b4 vaddr=0x3ffb0000 size=0x01ea4 (  7844) load
  I (111) esp_image: segment 2: paddr=0x00029f60 vaddr=0x40080000 size=0x00400 (  1024) load
  0x40080000: _WindowOverflow4 at esp-idf/esp-idf/components/freertos/xtensa_vectors.S:1778

  I (116) esp_image: segment 3: paddr=0x0002a368 vaddr=0x40080400 size=0x05ca8 ( 23720) load
  I (134) esp_image: segment 4: paddr=0x00030018 vaddr=0x400d0018 size=0x126a8 ( 75432) map
  0x400d0018: _flash_cache_start at ??:?

  I (162) esp_image: segment 5: paddr=0x000426c8 vaddr=0x400860a8 size=0x01f4c (  8012) load
  0x400860a8: prvAddNewTaskToReadyList at esp-idf/esp-idf/components/freertos/tasks.c:4561

  I (171) boot: Loaded app from partition at offset 0x20000
  I (171) boot: Checking flash encryption...
  I (171) flash_encrypt: flash encryption is enabled (3 plaintext flashes left)
  I (178) boot: Disabling RNG early entropy source...
  I (184) cpu_start: Pro cpu up.
  I (188) cpu_start: Application information:
  I (193) cpu_start: Project name:     flash-encryption
  I (198) cpu_start: App version:      v4.0-dev-850-gc4447462d-dirty
  I (205) cpu_start: Compile time:     Jun 17 2019 16:32:52
  I (211) cpu_start: ELF file SHA256:  8770c886bdf561a7...
  I (217) cpu_start: ESP-IDF:          v4.0-dev-850-gc4447462d-dirty
  I (224) cpu_start: Starting app cpu, entry point is 0x40080e4c
  0x40080e4c: call_start_cpu1 at esp-idf/esp-idf/components/esp32/cpu_start.c:265

  I (0) cpu_start: App cpu up.
  I (235) heap_init: Initializing. RAM available for dynamic allocation:
  I (241) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM
  I (247) heap_init: At 3FFB2EC8 len 0002D138 (180 KiB): DRAM
  I (254) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM
  I (260) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM
  I (266) heap_init: At 40087FF4 len 0001800C (96 KiB): IRAM
  I (273) cpu_start: Pro cpu start user code
  I (291) cpu_start: Starting scheduler on PRO CPU.
  I (0) cpu_start: Starting scheduler on APP CPU.

  Sample program to check Flash Encryption
  This is ESP32 chip with 2 CPU cores, WiFi/BT/BLE, silicon revision 1, 4MB external flash
  Flash encryption feature is enabled
  Flash encryption mode is DEVELOPMENT
  Flash in encrypted mode with flash_crypt_cnt = 1
  Halting...

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

使用主机生成的密钥

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

备注

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

使用主机生成的密钥需完成以下步骤:

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

请参考如何检查 ESP32 flash 加密状态

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

espsecure.py generate_flash_encryption_key my_flash_encryption_key.bin
  1. 在第一次加密启动前,使用以下命令将该密钥烧录到设备上,这个操作只能执行 一次

espefuse.py --port PORT burn_key flash_encryption my_flash_encryption_key.bin

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

  1. 项目配置菜单 中进行如下设置:

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

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

idf.py flash monitor

备注

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

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

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

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

重新烧录更新后的分区

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

idf.py encrypted-app-flash monitor

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

idf.py encrypted-flash monitor

发布模式

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

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

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

请参考如何检查 ESP32 flash 加密状态

  1. 项目配置菜单,执行以下操作:

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

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

idf.py flash monitor

备注

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

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

一旦在发布模式下启用 flash 加密,引导加载程序将写保护 FLASH_CRYPT_CNT eFuse。

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

备注

如果用户已经预先生成了 flash 加密密钥并存储了一个副本,并且 UART 下载模式没有通过 CONFIG_SECURE_UART_ROM_DL_MODE (ESP32 V3 only) 永久禁用,那么可以通过使用 espsecure.py encrypt_flash_data 预加密文件,从而在在本地更新 flash,然后烧录密文。请参考 手动加密文件

最佳实践

在生产中使用 flash 加密时:

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

  • 在使用 ESP32 V3 时,如果生产设备不需要 UART ROM 下载模式,那么则该禁用该模式以增加设备安全性。这可以通过在应用程序启动时调用 esp_efuse_disable_rom_download_mode() 来实现。或者,可将项目 CONFIG_ESP32_REV_MIN 级别配置为 3(仅针对 ESP32 V3),然后选择 CONFIG_SECURE_UART_ROM_DL_MODE 为“永久性的禁用 ROM 下载模式(推荐)”。在早期的 ESP32 版本上无法禁用 ROM 下载模式。

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

可能出现的错误

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

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

rst:0x3 (SW_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
flash read err, 1000
ets_main.c 371
ets Jun  8 2016 00:22:57

rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
flash read err, 1000
ets_main.c 371
ets Jun  8 2016 00:22:57

rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
flash read err, 1000
ets_main.c 371
ets Jun  8 2016 00:22:57

rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
flash read err, 1000
ets_main.c 371
ets Jun  8 2016 00:22:57

rst:0x7 (TG0WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
flash read err, 1000
ets_main.c 371
ets Jun  8 2016 00:22:57

备注

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

  1. 如果固件的引导加载程序已加密,但通过 明文分区表镜像 重新烧录了分区表,引导加载程序将无法读取分区表,从而出现以下错误:

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!
  1. 如果引导加载程序和分区表已加密,但使用 明文应用程序镜像 重新烧录了应用程序,引导加载程序将无法加载应用程序,从而出现以下错误:

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 flash 加密状态

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

要检查您的 ESP32 设备上是否启用了 flash 加密,请执行以下操作之一:

在加密的 flash 中读写数据

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

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

flash 加密范围

FLASH_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 函数:

使用非易失性存储器 (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 无法正常启动。设备将不断重启,并报错 flash read err, 1000invalid header: 0xXXXXXX

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

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

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

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

  4. 使用 espefuse.py (在 components/esptool_py/esptool 中)以关闭 FLASH_CRYPT_CNT,运行:

espefuse.py burn_efuse FLASH_CRYPT_CNT

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

flash 加密的要点

  • 使用 AES-256 加密 flash。flash 加密密钥存储于芯片内部的 flash_encryption eFuse 中,并(默认)受保护,防止软件访问。

  • flash 加密算法采用的是 AES-256,其中密钥随着 flash 的每个 32 字节块的偏移地址“调整”。这意味着,每个 32 字节块(2 个连续的 16 字节 AES 块)使用从 flash 加密密钥中产生的一个特殊密钥进行加密。

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

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

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

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

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

重要

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

flash 加密的局限性

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

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

  • 并非所有数据都是加密存储。因而在 flash 上存储数据时,请检查您使用的存储方式(库、API 等)是否支持 flash 加密。

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

  • 出于相同原因,攻击者始终可获知一对相邻的 16 字节块(32 字节对齐)何时包含相同的 16 字节序列。因此,在 flash 上存储敏感数据时应牢记这点,可进行相关设置避免该情况发生(可使用计数器字节或每 16 字节设置不同的值即可)。具体请参考 NVS 加密

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

flash 加密与安全启动

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

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

  • 只有当选择 可再次烧录 安全启动模式,且安全启动密钥已预生成并烧录至 ESP32 时(可参见 安全启动),明文串行 flash 更新 才可能实现。在该配置下,idf.py bootloader 将生成简化的引导加载程序和安全启动摘要文件,在偏移量 0x0 处进行烧录。当进行明文串行重新烧录步骤时,需在烧录其他明文数据前重新烧录此文件。

  • 如果未重新烧录引导加载程序,则仍然可以 使用预生成的 flash 加密密钥重新烧录。重新烧录引导加载程序时,需在安全启动配置中启用相同的 可重新烧录 选项。

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:

  • DISABLE_DL_ENCRYPT 在 UART 引导加载程序启动模式下运行时,禁止 flash 加密操作。

  • DISABLE_DL_DECRYPT 在 UART 引导加载程序模式下运行时,禁止透明 flash 解密(即使 eFuse FLASH_CRYPT_CNT 已设置为在正常操作中启用 flash 透明解密)。

  • DISABLE_DL_CACHE 在 UART 引导加载程序模式下运行时禁止整个 MMU flash 缓存。

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

espefuse.py --port PORT burn_efuse DISABLE_DL_DECRYPT
espefuse.py --port PORT write_protect_efuse DISABLE_DL_ENCRYPT

重要

保持 DISABLE_DL_DECRYPT 未设置 (0) 会使 flash 加密无效。

对芯片具有物理访问权限的攻击者会使用 UART 引导加载程序模式(使用自定义存根代码)读取 flash 的内容。

设置 FLASH_CRYPT_CONFIG

FLASH_CRYPT_CONFIG eFuse 决定 flash 加密密钥中随块偏移“调整”的位数。详情可参考 flash 加密算法

首次启动固件引导加载程序时,该值始终设置为最大值 0xF

可手动写入这些 eFuse,并在首次启动前对其写保护,以便选择不同的调整值。但不推荐该操作。

强烈建议在 FLASH_CRYPT_CONFIG 未设置时,不要对其进行写保护。否则,它的值将永久为零,而 flash 加密密钥中则无调整位。这导致 flash 加密算法等同于 AES ECB 模式。

JTAG 调试

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

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

手动加密文件

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

密钥文件应该是单个原始二进制文件(例如:key.bin)。

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

espsecure.py encrypt_flash_data --keyfile /path/to/key.bin --address 0x10000 --output my-app-ciphertext.bin build/my-app.bin

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

备注

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

如果您的 ESP32 在 eFuse 中使用了非默认的 FLASH_CRYPT_CONFIG 值,那么则需要向 espsecure.py 传递 --flash_crypt_conf 参数以设置匹配的值。如果设备自己设置了 flash 加密就不会出现这种情况,但如果手动烧录 eFuse 来启用 flash 加密就可能发生这种情况。

espsecure.py decrypt_flash_data 命令可以使用同样的选项(和不同的输入/输出文件)来解密 flash 密文或之前加密的文件。

技术细节

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

flash 加密算法

  • AES-256 在 16 字节的数据块上运行。flash 加密引擎在 32 字节的数据(2 个 串行 AES 块)上加密或解密数据。

  • flash 加密的主密钥存储于 flash_encryption eFuse 中,默认受保护防止进一步写入或软件读取。

  • AES-256 密钥大小为 256 位(32 字节),从 flash_encryption eFuse 中读取。与 flash_encryption 中的存储顺序相比,硬件 AES 引擎使用的是相反的字节顺序的密钥。

    • 如果 CODING_SCHEME eFuse 设置为 0(默认“无”编码方案),则 eFuse 密钥块为 256 位,且密钥按原方式存储(反字节序)。

    • 如果 CODING_SCHEME eFuse 设置为 1(3/4 编码),则 eFuse 密钥块为 192 位(反字节序),信息熵总量减少。硬件 flash 加密仍在 256 字节密钥上运行,在读取后(字节序未反向),密钥扩展为 key = key[0:255] + key[64:127]

  • flash 加密中使用了逆向 AES 算法,因此 flash 加密的“加密”操作相当于 AES 解密,而其“解密”操作则相当于 AES 加密。这是为了优化性能,不会影响算法的有效性。

  • 每个 32 字节块(2 个相邻的 16 字节 AES 块)都由一个特殊的密钥进行加密。该密钥由 flash_encryption 中 flash 加密的主密钥产生,并随 flash 中该字节块的偏移进行 XOR 运算(一次“密钥调整”)。

  • 具体调整量取决于 FLASH_CRYPT_CONFIG eFuse 的设置。该 eFuse 共 4 位,每位可对特定范围的密钥位进行 XOR 运算:

    • Bit 1,对密钥的 0-66 位进行 XOR 运算。

    • Bit 2,对密钥的 67-131 位进行 XOR 运算。

    • Bit 3,对密钥的 132-194 位进行 XOR 运算。

    • Bit 4,对密钥的 195-256 位进行 XOR 运算。

    建议将 FLASH_CRYPT_CONFIG 的值始终保留为默认值 0xF,这样所有密钥位都随块偏移进行 XOR 运算。详情可参见 设置 FLASH_CRYPT_CONFIG

  • 块偏移的 19 个高位(第 5-23 位)由 flash 加密的主密钥进行 XOR 运算。选定该范围的原因为:flash 的大小最大为 16 MB(24 位),每个块大小为 32 字节,因而 5 个最低有效位始终为 0。

  • 从 19 个块偏移位中每个位到 flash 加密密钥的 256 位都有一个特殊的映射,以决定与哪个位进行 XOR 运算。有关完整映射可参见 espsecure.py 源代码中的变量 _FLASH_ENCRYPTION_TWEAK_PATTERN

  • 有关在 Python 中实现的完整 flash 加密算法,可参见 espsecure.py 源代码中的函数 _flash_encryption_operation()