Flash 加密

Flash 加密功能可用于加密 ESP32 所连接的 SPI flash 上面的内容。当 flash 加密被使能后,从 SPI flash 上面读出的数据不足以用于恢复 flash 上面所存储的大多数内容。

Falsh 加密是与 安全启动 相独立的另一个功能,你可以在不使能安全加密的情况下单独使用 flash 加密功能。不过,为了有一个更加安全的环境,我们推荐同时使用这两种技术。

**IMPORTANT: 使能 flash 加密将会限制你今后可以对 ESP32 进行更新的次数。请确保仔细阅读本文档(包括 Flash 加密的限制)并充分理解使能 flash 加密的含义。 **

背景

  • flash 的内容使用秘钥长度为 256 比特的 AES 进行加密。flash 的加密密钥存储在芯片内部的 efuse 中,且(默认)被保护,以免软件访问呢。
  • 通过 ESP32 的 flash cache 映射功能对 flash 的访问是透明的 - 读取时,所有被映射到这段地址空间的 flash 区域都会被透明地解密。
  • 加密是通过给 ESP32 烧写明码文本数据、然后由 bootloader 在第一次启动准备就绪后对这些数据进行加密完成的(如果加密被使能)。
  • 不是所有的 flash 都会被加密。下列 flash 数据不会被加密:
    • Bootloader
    • 安全启动 bootloader digest (如果安全启动被使能)
    • 分区表
    • 所有的 “app” 类型的分区
    • 在分区表中所有标记了 “encrypt” 的分区

为了易于访问,或者为了使用 flash 友好的更新算法(数据加密后对算法有影响),有时候需要某些数据分区不被加密。非易失型存储器的 “NVS” 分区不能被加密。

  • flash 的加密秘钥存储在 ESP32 芯片内部的 efuse 密钥块 1 中。默认情况下,这个密钥是读/写保护的,因此软件不能访问或者修改它。
  • flash 加密算法 使用的是 AES-256,其密钥是通过 flash 的 32 字节的块的偏移地址进行调整的。这意味着,每 32 字节的块(两个连续的 16 字节 AES 块)是通过由 flash 加密密钥推断出来的唯一的密钥(unique key)进行加密的。
  • 尽管运行在芯片上的软件可以透明地对 flash 上面的内容进行解密,但是当 falsh 加密被使能后,UART bootloader(默认)不能对数据进行加密/解密。
  • 如果 flash 加密被使能,当程序员在 使用加密的 flash 写代码时必须进行更加周详的考虑。

Flash 加密的初始化

这里描述的是 flash 加密初始化的默认(且推荐)过程。如果需要实现特殊的功能,也可以自定义该过程,具体细节请参考 Flash 加密高级功能

IMPORTANT: 一旦在某次启动时使能 flash 加密后,随后通过串口对 flash 重新烧写最多只有三次机会。 且需要执行特殊的步骤(参考文档 串口烧写)才能进行烧写。

  • 如果安全启动被使用,则再也不能在物理上进行重新烧写。
  • OTA 可以用于更新 flash 上面的内容,没有受到这种限制。
  • 当在开发过程中使能 flash 加密时,使用 预生成的 flash 加密密钥 可以在物理上对预加密的数据进行无数次的重新烧写。**

使能 flash 加密的过程:

  • bootloader 在被编译时必须使能 flash 加密功能。在 ``make menuconfig``中,进入 “Security Features” 并给 “Enable flash encryption on boot” 选择 “Yes”。
  • 如果同时还使能了安全启动,最好同时也选上这些选项。请先参考文档 安全启动
  • 像常规方法一样编译和烧写 bootloader、分区表和工厂 app。这些分区在初次被写入 flash 时是未被加密的。
  • 第一次启动时,bootloader 将会看到 FLASH_CRYPT_CNT efuse 被设置为 0(工厂默认),然后它会使用硬件随机数发生器生成一个 falsh 加密密钥。这个密钥会被存储在 efuse 中,且具有软件读/写保护的功能。
  • 所有的加密分区然后被 bootloader 加密。加密需要一段时间(大分区最多会需要一分钟)。

IMPORTANT: 当第一次启动在进行加密时,不要中断对 ESP32 的供电。如果供电被中断,flash 中的内容将会被破坏,然后需要使用未加密的数据进行重新烧写。这种重新烧写不会影响烧写次数限制。

  • 当烧写完成后,efuses 会在 UART bootloader 运行期间禁止对加密 flash 的访问。请查看 使能 UART Bootloader 加密/解密 以了解高级特性。
  • efuse FLASH_CRYPT_CONFIG 被烧写为最大值(0xF),以得到一个位数最大的密钥(在 flash 算法中被调整)。请查看 ref:setting-flash-crypt-config 以了解高级特性。
  • 最后,FLASH_CRYPT_CNT efuse 被烧写为一个初始值 1。就是这个 efuse 激活了 flash 透明加密层并限制了随后可重新烧写的次数。关于 FLASH_CRYPT_CNT efuse 的更多细节请参考章节 更新加密的 Flash
  • bootloader 自身复位,并从新的被加密的 flash 重启。

使用加密的 Flash

ESP32 应用程序代码可以通过调用 esp_flash_encryption_enabled() 来检查当前是否使能了 flash 加密功能。

当 flash 加密被使能后, 从代码中访问 flash 上面的内容时需要考虑一些额外的东西。

Flash 加密的范围

无论什么时候,只要 FLASH_CRYPT_CNT efuse 被设置了一个新的奇数比特位,所有通过 MMU cache 访问的 flash 内容都会被透明地解密。这包括:

  • flash 上面的应用程序可执行代码(IROM)
  • 存储在 flash 中的所有只读数据(DROM)
  • 所有通过 esp_spi_flash_mmap() 访问的数据
  • 正在被 ROM bootloader 读取的软件 bootloader 镜像

IMPORTANT: MMU flash cache 会无条件地解码所有数据。在 flash 中存储的未加密数据会通过 flash cache 被透明地解密,然后对软件来说就是垃圾数据。

读取加密的 Flash

如果要在读取数据时不使用 flash cache MMU 映射,我们推荐使用分区读取函数 esp_partition_read()。当使用这个函数时,只有从加密分区中读取的数据会被解密,其它分区读取的数据不会被解密。通过这种方法,软件可以以同一种方法访问加密和未加密的 falsh。

以其它 SPI 读 API 所读取的数据不会被解密:

  • 通过 esp_spi_flash_read() 读取的数据不被解密。
  • 通过 ROM 函数 SPIRead() 读取的数据不被解密(该函数不支持 esp-idf 应用程序)。
  • 使用非易失性存储器 (NVS) API 存储的数据总是以解密形式进行存储/读取。

写加密的 Flash

只要可能,我们都推荐使用分区写函数 esp_partition_write。当使用该函数时,只有往加密分区中写的数据会被加密,往其它分区写的数据不会被加密。通过这种方法,软件可以以同一种方法访问加密和未加密的 falsh。

当参数 write_encrypted 设置为 true 时,函数 esp_spi_flash_write 将以加密的形式写数据,否则则会以未加密的形式写数据。

ROM 函数 esp_rom_spiflash_write_encrypted 将会写加密数据到 flash,ROM 函数 SPIWrite 将会写未加密数据到 flash 中(这些函数不支持 esp-idf 应用程序)。

未加密数据的最小写尺寸是 4 字节(且是 4 字节对齐的)。由于数据加密是以块为单位的,加密数据的最小写尺寸是 16 字节(且是以 16 字节对齐的)。

更新加密的 Flash

OTA 更新

对加密分区进行 OTA 更新时,只要使用的函数是 esp_partition_write,该分区就会被自动以加密的形式进行写。

串口烧写

如果没有使用安全启动,FLASH_CRYPT_CNT efuse 允许通过串口烧写的方式(或其它物理方式)对 flash 进行更新,但最多有三次额外的机会。

该过程涉及烧写明码文本数据、更改(bump):ref:FLASH_CRYPT_CNT 的值,从而引起 bootloader 对该数据进行重新加密。

有限的更新

这种情况下只有 4 次串口烧写机会,其中还包括初始加密 flash 在内的那次机会。

当第四次加密被禁止后,FLASH_CRYPT_CNT efuse 将拥有一个最大值 0xFF,加密被永久禁止。

使用 OTA 更新通过预生成的 Flash 加密密钥重新烧写 可以绕过这种限制。

串口烧写的注意事项

  • 当使用串口重新烧写时,需要重新烧写所有使用明码文本初始化的分区(包括 bootloader),但是可以跳过非 “当前所选择” 的那个 OTA 分区(除非在上面发现了明码文本应用程序镜像,否则不会待其进行重新加密)。不过,所有有 “加密” 标记的分区会被无条件地重新加密,这也意味着已被加密的数据会被再次加密而被破坏。
    • 使用 make flash 会烧写所有需要烧写的分区。
  • 如果安全启动被使能,除非你的安全启动使用了 “重新烧写” 选项并烧写了预生成的密钥(参考 Secure Boot 文档),否则你不能使用串口进行串行烧写。在这种情况下,你可以在偏移地址 0x0 处重新烧写一个明码文本的安全启动 digest 和 bootloader 镜像。在烧写其它明码文本数据之前,必须要先烧写这个 digest。

串口重新烧写的过程

  • 像平常一样编译应用程序。
  • 像平常一样给设备烧写明码文本数据(make flashesptool.py 命令),烧写之前的所有加密分区(包括 bootloader)。
  • 此时,设备将不能启动(提示消息 flash read err, 1000),这是因为它期望看到的是一个加密的 bootloader,而实际上却是一个明码文本。
  • 使用命令 espefuse.py burn_efuse FLASH_CRYPT_CNT 烧写 FLASH_CRYPT_CNT efuse。espefuse.py 会自动给计数比特加 1,并禁止加密。
  • 复位设备,然后设备会重新加密明码文本分区,然后再次烧写 FLASH_CRYPT_CNT efuse 以重新使能加密功能。

禁止串口更新

如果需要阻止今后使用串口更新明码文本,可以在 flash 加密被使能后(即第一次启动完成后)使用 espefuse.py 写保护 FLASH_CRYPT_CNT efuse

espefuse.py --port PORT write_protect_efuse FLASH_CRYPT_CNT

这将会禁止今后做任何改动,以禁止/重新使能 flash 加密。

通过预生成的 Flash 加密密钥重新烧写

你也可以在 PC 上面预生成一个 flash 加密密钥,然后将它烧写到 ESP32 的 efuse 密钥块中。这样做的好处是可以在主机对数据预加密,然后将加密后的数据烧写到 ESP32 上面,从而不需要明码文本烧写更新。

这在开发过程中是很有用的,因为它没有 4 次重烧的限制。此外,即使安全启动被使能,也可以无限次地重新烧写,因为 bootloader 不需要每次都被烧写。

IMPORTANT 这种方法只是为了方便开发,不要用于实际的产品设备。如果要为产品生成 flash 加密数据,请确保使用一个高质量的随机数源产生加密密钥,且不要在多个设备之间共享同一个 flash 加密密钥。

预生成 Flash 加密密钥

Flash 加密密钥是一个 32 字节的随机数。你可以使用 espsecure.py 生成一个随机密钥

espsecure.py generate_flash_encryption_key my_flash_encryption_key.bin

(随机数的质量与 OS 以及 Python 所安装的随机数源相关。)

另外,如果你正在使用 安全启动,且有一个安全启动签名密钥,你可以生成一个安全启动私有签名密钥的 SHA-256 digest,并使用它作为 flash 加密密钥

espsecure.py digest_private-key --keyfile secure_boot_signing_key.pem my_flash_encryption_key.bin

(如果你在安全启动中使能了 可重新烧写模式,则这 32 字节的数据还将作为安全启动 digest 密钥。)

通过这种从全球启动签名密钥生成 flash 加密密钥的方式意味着你只需要存储一个密钥文件。不过,这种方法 完全不适用于 实际产品中的设备。

烧写 Flash 加密密钥

生成 flash 加密密钥后,你还需要将它烧写到 ESP32 的 efuse 密钥块中。这必须在加密启动前完成,否则 ESP32 将会产生一个随机密钥,导致软件不能访问/修改 flash 上的内容。

将密钥烧写到设备(只需要一次)

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

使用预生成的密钥第一次烧写

烧写完密钥后,按照默认 Flash 加密的初始化 步骤进行操作,并为第一次启动烧写一个明码文本镜像。bootloader将会使用预先烧写的密钥使能 flash 加密并加密所有分区。

使用预生成的密钥重新烧写

当第一次启动使能加密后,重新烧写加密的镜像需要一步额外的手工步骤,即预加密我们需要烧写到 flash 中的数据。

假设这是你用于烧写明码文本数据的命令

esptool.py --port /dev/ttyUSB0 --baud 115200 write_flash 0x10000 build/my-app.bin

二进制应用程序镜像 build/my-app.bin 被烧写到偏移地址 0x10000 处。这里的文件名和偏移地址要用于加密数据

espsecure.py encrypt_flash_data --keyfile my_flash_encryption_key.bin --address 0x10000 -o build/my-app-encrypted.bin build/my-app.bin

上面这条命令会使用所提供的密钥加密 my-app.bin,并产生一个加密文件 my-app-encrypted.bin。请确保这里的地址参数与你将要烧写二进制镜像的地址相匹配。

然后,使用 esptool.py 烧写加密后的二进制文件

esptool.py --port /dev/ttyUSB0 --baud 115200 write_flash 0x10000 build/my-app-encrypted.bin

至此,已经不需要其它的步骤了,因为数据已经被加密并烧写完成了。

禁止 Flash 加密

如果你由于某些原因意外地把 flash 加密功能使能了, the next flash of plaintext data will soft-brick the ESP32 (设备将会连续重启,并打印错误 flash read err, 1000)。

你可以通过写 FLASH_CRYPT_CNT efuse 再次禁止 flash 加密:

  • 首先,运行 make menuconfig 并取消 “Security Features” 下面的复选框 “Enable flash encryption boot”。
  • 退出配置菜单并保存新的配置。
  • 再次运行 make menuconfig ,再次检查是否真的禁止了该选项!如果该选项被使能,bootloader 启动时会立即再次重新加密
  • 运行 make flash to 编译并烧写 flash 加密未被使能的 bootloader 和应用程序。
  • 运行 espefuse.py (在 components/esptool_py/esptool 下面) 禁止 FLASH_CRYPT_CNT efuse)::
    espefuse.py burn_efuse FLASH_CRYPT_CNT

给 ESP32 复位,此时 flash 加密功能会被禁止,bootloader 会像平常一样启动。

Flash 加密的限制

Flash 加密功能可以阻止读取加密的 flash 的内容,保护固件,使其不会在未授权时被读取和修改。如果打算使用 flash 加密系统,则非常有必要理解它的限制:

  • Flash 加密的安全性完全由密钥决定。因此,我们推荐在设备第一次启动时由设备产生密钥(这是默认的行为)。如果密钥是在设备外产生的,请确保其过程的正确性。
  • 不是所有的数据都会被加密存储。如果需要在 flash 上存储数据,请先检查你所使用的方法(库、API 等)是否支持 flash 加密。
  • Flash 加密不能阻止知道 flash 顶层布局的攻击者。这是因为每一对相邻的 16 字节 AES 块使用的是同样的 AES 密钥。当这些相邻的 16 字节块包含相同的内容(例如空区域或填充(pading)区域)时,这些块将会加密生成匹配的加密块对,攻击者可以在加密设备间进行顶层比较(例如判断两个设备运行的固件是否是同一版本)。
  • 同样的理由,攻击者还可以判断两个相连的 16 字节块(32 字节对齐的)是否包含相同的内容。因此一定要记住,如果你想在 flash 上面存储敏感的数据,需要对你的存储方法进行进行一定的设计,确保不会发生这样的事(每 16 个字节使用一个字节的计数器或者其它某些不同的值是足够的)。
  • 仅使用 Flash 加密功能不能阻止攻击者修改设备的固件。如果要阻止设备运行未被授权的固件,需要结合使用 flash 加密功能和 安全启动 功能。

Flash 加密高级功能

下列信息用于描述 flash 加密的高级功能:

加密分区标志

某些分区默认被加密。否则,可以将任何分区标记为需要加密:

在描述 分区表 的 CSV 文件中,存在一个标志字段。该字段通常是空的。如果你在这个字段中写上 “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
  • 默认的分区表不包含任何加密数据分区。
  • “app” 分区不需要被标记为 “encrypted”,因为它们总是被当做加密的。
  • 如果 flash 加密功能未被使能,则 “encrypted” 标志不会起任何作用。
  • 如果你希望从物理上阻止访问/修改带有 phy_init 数据的 phy 分区,你也可以将该分区标记为 “encrypted”。
  • nvs 分区不能被标记为 “encrypted”。

使能 UART Bootloader 加密/解密

默认情况下,在第一次启动时,flash 加密过程会烧写 efuses DISABLE_DL_ENCRYPTDISABLE_DL_DECRYPTDISABLE_DL_CACHE:

  • DISABLE_DL_ENCRYPT,当运行在 UART bootloader 模式时,禁止 flash 加密功能。
  • DISABLE_DL_DECRYPT,当运行在 UART bootloader 模式时,即使 FLASH_CRYPT_CNT efuse 被设置为以正常操作使能 flash 透明解密,也禁止 flash 透明解密。
  • DISABLE_DL_CACHE,当运行在 UART bootloader 模式时,禁止整个 MMU flash cache。

可以只烧写其中一部分 efuses,让其它 efuses 在第一次启动前写保护(将其值复原为 0),例如

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

(注意,通过一次写保护,这三个 efuses 都会被禁止。因此,有必要在写保护前设置所有的比特。)

IMPORTANT: 当前,对这些 efuses 写保护使其复原不是很有用,因为 esptool.py 不支持读/写加密的 flash。

IMPORTANT: 如果 DISABLE_DL_DECRYPT 被复原(0),这会使 flash 加密不起作用,因为进行物理访问的攻击者可以使用 UART bootloader 模式(使用自定义的桩代码)读出 flash 中的内容。

设置 FLASH_CRYPT_CONFIG

efuse FLASH_CRYPT_CONFIG 用于判断 flash 加密密钥的比特数,具体细节请参考 Flash 加密算法

bootloader 在第一次启动时总是会将这个值设为最大值 0xF

可以在第一次启动前对该 efuse 进行手工写,使其写保护,以选择不同的调整值。不过不推荐这样做。

强烈推荐当 FLASH_CRYPT_CONFIG 的值被设置为零时不要使其写保护。如果它被设置零,flash 加密密钥中没有比特会被调整,flash 加密算法与 AES ECB 模式相同。

技术细节

下面的章节将介绍 flash 加密操作的一些参考信息。

FLASH_CRYPT_CNT efuse

FLASH_CRYPT_CNT 是一个 8 比特的字段,用于控制 flash 加密。Flash 加密的使能/禁止就是基于该 efuse 中被设置为 “1” 的比特的数量。

  • 当偶数比特(0,2,4,6,8)被设置时:Flash 加密被禁止,所有加密的数据将不能被解密。
    • 如果在编译 bootloader 时选择了 “Enable flash encryption on boot”,则 bootloader 遇到的就是这种情形,它会立即对所找都的所有为加密数据进行加密,将 efuse 中的其它比特设置为 ‘1’,表示当前奇数比特被设置了。
      1. 第一次明码文本启动时,比特计数值是 0,bootloader 会将其修改为 1(值 0x1)。
      2. 在下一次明码文本更新后,比特计数值被手工设置 2(值 0x3)。重新加密后,bootloader 会将比特计数值改为 3(值 0x7)。
      3. 在下一次明码文本更新后,比特计数值被手工设置 4(值 0x0F)。重新加密后,bootloader 会将比特计数值改为 5(值 0x1F)。
      4. 在下一次明码文本更新后,比特计数值被手工设置 6(值 0x3F)。重新加密后,bootloader 会将比特计数值改为 7(值 0x7F)。
  • 当奇数比特(1,3,5,7)被设置时:透明读取加密 flash 被使能。
  • 当所有的 8 比特(efuse 值 0xFF)被设置后:透明地读加密 flash 被禁止,所有的加密数据将永久不可访问。bootloader 会检测到这个条件,然后挂起。如果要绕过这种状态来加载为授权的代码,必须使用安全启动或 FLASH_CRYPT_CNT efuse 被写保护。

Flash 加密算法

  • AES-256 操作在数据的 16 字节块之上。flash 加密引擎以 32 字节块(两个 AES 块)加密/解密数据。
  • AES 算法被用于逆向 falsh 加密,因此 flash 加密的加密操作就是 AES 的解密,解密操作就是 AES 加密。这样做是性能的原因,且不会改变算法的性能。
  • 主 flash 加密密钥存储在 efuse (BLK2) 中,且默认具有写保护和软件读保护。
  • 每 32 字节块(两个相连的 16 字节 AES 块)使用一个唯一的密钥进行加密。该密钥由 efuse 中的主 flash 加密密钥推断(与 flash 中该块的偏移量异或,这个偏移量又叫做 “密钥调整值”)而来。
  • 还需要根据 efuse FLASH_CRYPT_CONFIG 的值进行特殊调整。这是一个 4 比特的 efuse,其中每个比特都需要与密钥的某个范围内的比特进行异或:。
    • 比特 1 与密钥的比特 0-66 进行异或。
    • 比特 2 与密钥的比特 67-131 进行异或。
    • 比特 3 与密钥的比特 132-194 进行异或。
    • 比特 4 与密钥的比特 195-256 进行异或。

推荐将 FLASH_CRYPT_CONFIG 保持为默认值 0xF,这样所有的比特都是直接与块的偏移量进行异或的。具体细节请查看 设置 FLASH_CRYPT_CONFIG

  • 块偏移量的高 19 比特(比特 5 ~ 比特 23)被用于与主 flash 加密密钥进行异或。选择这个比特范围有两个原因: flash 的最大容量是 16 MB(24 个比特);每个块是 32 字节的,因此最低 5 比特总是零。
  • 19 比特的块偏移量与 flash 加密密钥之间存在一个特殊的映射,用于判断哪个比特是用于异或的。关于完整的映射关系,请查看 espsecure.py 源文件中的变量 _FLASH_ENCRYPTION_TWEAK_PATTERN
  • 如果想要查看用 Python 语言写的完整的 flash 加密算法,请参考 espsecure.py 源文件中的函数 _flash_encryption_operation()