# NVS 加密

[English]

## 概述

本文档主要介绍 NVS 加密功能，这一功能有助于实现设备在 flash 中的安全存储。

存储在 NVS 分区中的数据可以用 XTS-AES 进行加密，与磁盘加密标准 IEEE P1619 中提到的加密方式类似。加密时，每个条目都被视作一个 `sector`，而条目的相对地址（相对于分区起始位置）作为 `sector-number` 输入加密算法。

根据要使用的具体方案，可以选择启用 CONFIG_NVS_ENCRYPTION 和 CONFIG_NVS_SEC_KEY_PROTECTION_SCHEME > `CONFIG_NVS_SEC_KEY_PROTECT_USING_FLASH_ENC` 或 `CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC` 实现 NVS 加密。

## NVS 加密：基于 flash 加密的方案

在这个方案中，NVS 加密所需的密钥存储在另一个分区中，该分区用 flash 加密 进行保护。因此，使用该方案时，必须先启用 flash 加密。

启用 flash 加密 时需同时启用 NVS 加密，因为 Wi-Fi 驱动程序会将凭据（如 SSID 和密码）储存在默认的 NVS 分区中。如已启用平台级加密，那么则需要同时启用 NVS 加密。

要用这一方案进行 NVS 加密，分区表中必须包含 NVS 密钥分区。在分区表选项 ( `menuconfig` > `Partition Table` ) 中，有两个包含 NVS 密钥分区 的分区表，可通过项目配置菜单 ( `idf.py menuconfig`) 进行选择。要了解如何配置和使用 NVS 加密功能，请参考示例 security/flash_encryption。

### NVS 密钥分区

应用如果要使用 NVS 加密（使用基于 flash 加密的方案）编译时，须使用类型为 `data` 和子类型为 `nvs_keys` 的密钥分区。该分区应被标记为 `encrypted` 且最小为 4 KB （最小分区大小）。参考 分区表 了解详情。在分区表选项 ( `menuconfig` > `Partition Table`) 中，有两个包含 NVS 密钥分区 的额外分区表，可以直接用于 NVS 加密。分区的结构如下所示：

    +-----------+--------------+-------------+----+
    |              XTS encryption key (32)        |
    +---------------------------------------------+
    |              XTS tweak key (32)             |
    +---------------------------------------------+
    |                  CRC32 (4)                  |
    +---------------------------------------------+

可以通过以下两种方式之一生成 NVS 密钥分区 中的 XTS 加密密钥：

在 ESP32-S2 芯片上生成密钥

> - 启用 NVS 加密时，可使用 API 函数 `nvs_flash_init()` 来初始化加密的默认 NVS 分区。该 API 函数会内部生成 ESP 芯片的 XTS 加密密钥，并找到第一个 NVS 密钥分区。

> - 然后，该 API 函数使用 nvs_flash/include/nvs_flash.h 提供的 `nvs_flash_generate_keys()` API 函数，自动生成 NVS 密钥并存储到该分区。只有当相应的密钥分区为空时，才会生成和存储新密钥。然后，就可以利用 `nvs_flash_secure_init_partition()` 用此密钥分区来读取安全配置，以初始化自定义的加密 NVS 分区。

> - API 函数 `nvs_flash_secure_init()` 和 `nvs_flash_secure_init_partition()` 不会内部生成密钥。如果要使用这两个 API 函数初始化加密的 NVS 分区，可以在启动后使用 `nvs_flash.h` 提供的 `nvs_flash_generate_keys()` API 函数生成密钥，然后由该 API 函数将生成的密钥以加密形式写入密钥分区中。

> 备注

> 请注意，在使用此方法启动应用程序前，必须完全擦除 `nvs_keys` 分区。否则，应用程序可能会生成 `ESP_ERR_NVS_CORRUPT_KEY_PART` 错误代码，该代码假设 `nvs_keys` 分区不为空并且包含格式错误的数据。可以使用以下命令来实现：

>     parttool.py --port PORT --partition-table-file=PARTITION_TABLE_FILE --partition-table-offset PARTITION_TABLE_OFFSET erase_partition --partition-type=data --partition-subtype=nvs_keys

>     # 如果启用了 Flash Encryption 或 Secure Boot，需要使用 "--esptool-erase-args=force" 来抑制错误：
>     # "Active security features detected, erasing flash is disabled as a safety measure.  Use --force to override ..."
>     parttool.py --port PORT --esptool-erase-args=force --partition-table-file=PARTITION_TABLE_FILE --partition-table-offset PARTITION_TABLE_OFFSET erase_partition --partition-type=data --partition-subtype=nvs_keys

使用预生成的 NVS 密钥分区

> 如果 NVS 密钥分区 中的密钥不是由应用程序生成，则需要使用预先生成的密钥分区。可以使用 NVS 分区生成程序 生成包含 XTS 加密密钥的 NVS 密钥分区。然后使用以下两个命令将预生成的密钥分区存储到 flash 上：

> 1. 构建并烧写分区表

>     idf.py partition-table partition-table-flash

> 2. 使用 parttool.py （参见 分区表 中分区工具相关章节）将密钥存储在 flash 上的 NVS 密钥分区 中

>     parttool.py --port PORT --partition-table-offset PARTITION_TABLE_OFFSET write_partition --partition-name="name of nvs_key partition" --input NVS_KEY_PARTITION_FILE

>     # 如果启用了 Flash Encryption 或 Secure Boot，需要使用 "--esptool-erase-args=force" 来抑制错误：
>     # "Active security features detected, erasing flash is disabled as a safety measure.  Use --force to override ..."
>     parttool.py --port PORT --esptool-erase-args=force --partition-table-offset PARTITION_TABLE_OFFSET write_partition --partition-name="name of nvs_key partition" --input NVS_KEY_PARTITION_FILE

> 备注

> 如果设备是在 flash 加密开发模式下加密的，那么要更新 NVS 密钥分区就需要使用 parttool.py 来加密 NVS 密钥分区，并提供一个指向你构建目录中未加密分区表的指针 (build/partition_table)，因为设备上的分区表也是加密的。命令如下：

>     parttool.py --esptool-write-args encrypt --port PORT --partition-table-file=PARTITION_TABLE_FILE --partition-table-offset PARTITION_TABLE_OFFSET write_partition --partition-name="nvs_key 分区名称" --input NVS_KEY_PARTITION_FILE

>     # 如果启用了 Flash Encryption 或 Secure Boot，需要使用 "--esptool-erase-args=force" 来抑制错误：
>     # "Active security features detected, erasing flash is disabled as a safety measure.  Use --force to override ..."
>     parttool.py --esptool-erase-args=force --esptool-write-args encrypt --port PORT --partition-table-file=PARTITION_TABLE_FILE --partition-table-offset PARTITION_TABLE_OFFSET write_partition --partition-name="name of nvs_key partition" --input NVS_KEY_PARTITION_FILE

由于密钥分区被标记为 `encrypted`，且 flash 加密 已启用，引导加载程序会在首次启动时使用 flash 加密密钥对此分区进行加密。

一个应用程序可以使用不同的密钥对不同的 NVS 分区进行加密，从而拥有多个密钥分区。应用程序应为加密或解密操作提供正确的密钥分区和密钥信息。

## NVS 加密：基于 HMAC 外设的方案

此方案中，用于 NVS 加密的 XTS 密钥来自 eFuse 中编程的 HMAC 密钥，其目的是 `esp_efuse_purpose_t::ESP_EFUSE_KEY_PURPOSE_HMAC_UP`。由于加密密钥在运行时生成，不存储在 flash 中，因此这个功能不需要单独的 NVS 密钥分区。

备注

通过这个方案， 无需启用 flash 加密 就能在 ESP32-S2 上实现安全存储。

重要

注意，此方案使用一个 eFuse 块来存储获取加密密钥所需的 HMAC 密钥。

- NVS 加密启用时后，可用 API 函数 `nvs_flash_init()` 来初始化加密的默认 NVS 分区。该 API 函数首先检查 CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID 处是否存在一个 HMAC 密钥。

备注

CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID 配置的有效范围为 `0` (`hmac_key_id_t::HMAC_KEY0`) 到 `5` (`hmac_key_id_t::HMAC_KEY5`)。默认情况下该配置为 `-1`，须在构建用户应用程序之前进行修改。

- 如果找不到密钥，会内部生成一个密钥，并储存在 CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID 指定的 eFuse 块中。

- 如果找到用于 `esp_efuse_purpose_t::ESP_EFUSE_KEY_PURPOSE_HMAC_UP` 的密钥，该密钥也会用于 XTS 加密密钥的生成。

- 如果指定的 eFuse 块被 `esp_efuse_purpose_t::ESP_EFUSE_KEY_PURPOSE_HMAC_UP` 以外目的的密钥占用，则会引发错误。

- 然后，API `nvs_flash_init()` 使用 nvs_flash/include/nvs_flash.h 提供的 `nvs_flash_generate_keys_v2()` API 函数，自动生成所需的 NVS 密钥。该密钥还可用于读取安全配置（参见 `nvs_flash_read_security_cfg_v2()`）并通过 `nvs_flash_secure_init_partition()` 初始化自定义的加密 NVS 分区。

- API 函数 `nvs_flash_secure_init()` 和 `nvs_flash_secure_init_partition()` 不会内部生成密钥。使用这些 API 函数初始化加密的 NVS 分区时，可在启动后用 API 函数 `nvs_flash_generate_keys_v2()` 生成密钥，或使用 `nvs_flash_read_security_cfg_v2()` 获取并填充 NVS 安全配置结构 `nvs_sec_cfg_t`，将其输入到上述 API 中。

备注

可以使用以下命令预先在 eFuse 中设置自己的 HMAC 密钥：

    idf.py -p PORT efuse-burn-key <BLOCK_KEYN> <hmac_key_file.bin> HMAC_UP

## 加密读/写

NVS API 函数 `nvs_get_*` 或 `nvs_set_*` 也可用于读取和写入加密的 NVS 分区。

加密默认的 NVS 分区

- 要为默认 NVS 分区启用加密，无需额外的步骤。在启用 CONFIG_NVS_ENCRYPTION 时，API 函数 `nvs_flash_init()` 会根据使用的方案（由 CONFIG_NVS_SEC_KEY_PROTECTION_SCHEME 设置）在内部执行一些额外步骤，为默认的 NVS 分区启用加密。

- 在基于 flash 加密的方案中，加密密钥由找到的第一个 NVS 密钥分区 生成。在 HMAC 方案中，密钥由 CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID 中烧录的 HMAC 密钥生成（参考 API 文档以了解更多详细信息）。

另外，还可使用 API 函数 `nvs_flash_secure_init()` 为默认 NVS 分区启用加密。

加密自定义 NVS 分区

- 要为一个自定义的 NVS 分区启用加密，使用 API 函数 `nvs_flash_secure_init_partition()` 代替 `nvs_flash_init_partition()`。

- 使用 API 函数 `nvs_flash_secure_init()` 和 `nvs_flash_secure_init_partition()` 时，为了在启用加密的情况下执行 NVS 读/写操作，应用程序应遵守以下步骤：

  > 1. 填充 NVS 安全配置结构 `nvs_sec_cfg_t`

  >    > - 对基于 flash 加密的方案

  >    >   > - 使用 API 函数 esp_partition_find* 查找密钥分区和 NVS 数据分区。

  >    >   > - 使用 API 函数 `nvs_flash_read_security_cfg()` 或 `nvs_flash_generate_keys()` 填充 `nvs_sec_cfg_t` 结构体。

  >    > - 对基于 HMAC 的方案

  >    >   > - 用 `nvs_sec_config_hmac_t` 为设置特定方案配置数据，并使用 API `nvs_sec_provider_register_hmac()` 注册此基于 HMAC 的方案。该 API 也将用于填充特定方案的句柄（参见 `nvs_sec_scheme_t`）。

  >    >   > - 使用 API 函数 `nvs_flash_read_security_cfg_v2()` 或 `nvs_flash_generate_keys_v2()` 填充 `nvs_sec_cfg_t` 结构体。

  >    >     nvs_sec_cfg_t cfg = {};
  >    >     nvs_sec_scheme_t *sec_scheme_handle = NULL;

  >    >     nvs_sec_config_hmac_t sec_scheme_cfg = {};
  >    >     hmac_key_id_t hmac_key = HMAC_KEY0;
  >    >     sec_scheme_cfg.hmac_key_id = hmac_key;

  >    >     ret = nvs_sec_provider_register_hmac(&sec_scheme_cfg, &sec_scheme_handle);
  >    >     if (ret != ESP_OK) {
  >    >     return ret;
  >    >     }

  >    >     ret = nvs_flash_read_security_cfg_v2(sec_scheme_handle, &cfg);
  >    >     if (ret != ESP_OK) {
  >    >         if (ret == ESP_ERR_NVS_SEC_HMAC_KEY_NOT_FOUND) {
  >    >             ret = nvs_flash_generate_keys_v2(&sec_scheme_handle, &cfg);
  >    >             if (ret != ESP_OK) {
  >    >             ESP_LOGE(TAG, "Failed to generate NVS encr-keys!");
  >    >                 return ret;
  >    >             }
  >    >         }
  >    >         ESP_LOGE(TAG, "Failed to read NVS security cfg!");
  >    >         return ret;
  >    >     }

  > 2. 使用 API 函数 `nvs_flash_secure_init()` 或 `nvs_flash_secure_init_partition()` 初始化 NVS flash 分区。

  > 3. 使用 API 函数 `nvs_open()` 或 `nvs_open_from_partition()` 打开一个命名空间。

  > 4. 使用 `nvs_get_*` 或 `nvs_set_*` 执行 NVS 读/写操作。

  > 5. 使用 `nvs_flash_deinit()` 取消初始化 NVS 分区。

备注

在采用基于 HMAC 的方案时，可以在不启用任何 NVS 加密的配置选项的情况下开始上述工作流：CONFIG_NVS_ENCRYPTION，CONFIG_NVS_SEC_KEY_PROTECTION_SCHEME -> CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC 和 CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID，以使用 `nvs_flash_secure_init()` API 加密默认分区及自定义的 NVS 分区。

## NVS Security Provider

组件 nvs_sec_provider 存储了 NVS 加密方案的所有特定实现代码，并且适用于未来的方案。此组件充当 nvs_flash 组件处理加密密钥的接口。组件 nvs_sec_provider 有自己的配置菜单，选定的安全方案和相应设置基于这一菜单注册到 nvs_flash 组件。

该组件通过工厂函数注册了特殊的安全框架，可以实现出厂即用的安全方案。在该方案中，无需使用 API 来生成、读取加密密钥（如 `nvs_sec_provider_register_hmac()`）。要了解 API 的使用，参考示例 security/nvs_encryption_hmac。

备注

如果不希望使用 :component: nvs_sec_provider 组件的默认实现，而使用自定义方式生成或者保护 NVS 加密密钥，请选择 CONFIG_NVS_SEC_KEY_PROTECTION_SCHEME -> `CONFIG_NVS_SEC_KEY_PROTECT_NONE` 配置项。

## API 参考

### Header File

- components/nvs_sec_provider/include/nvs_sec_provider.h

- This header file can be included with:

  >     #include "nvs_sec_provider.h"

- This header file is a part of the API provided by the `nvs_sec_provider` component. To declare that your component depends on `nvs_sec_provider`, add the following to your CMakeLists.txt:

  >     REQUIRES nvs_sec_provider

  > or

  >     PRIV_REQUIRES nvs_sec_provider

### Functions

esp_err_t nvs_sec_provider_register_flash_enc(const nvs_sec_config_flash_enc_t *sec_scheme_cfg, nvs_sec_scheme_t **sec_scheme_handle_out)

  Register the Flash-Encryption based scheme for NVS Encryption.

  参数:

    - sec_scheme_cfg -- [in] Security scheme specific configuration data

    - sec_scheme_handle_out -- [out] Security scheme specific configuration handle

  返回:

    - ESP_OK, if `sec_scheme_handle_out` was populated successfully with the scheme configuration;

    - ESP_ERR_INVALID_ARG, if `scheme_cfg_hmac` is NULL;

    - ESP_ERR_NO_MEM, No memory for the scheme-specific handle `sec_scheme_handle_out`

    - ESP_ERR_NOT_FOUND, if no `nvs_keys` partition is found

esp_err_t nvs_sec_provider_register_hmac(const nvs_sec_config_hmac_t *sec_scheme_cfg, nvs_sec_scheme_t **sec_scheme_handle_out)

  Register the HMAC-based scheme for NVS Encryption.

  参数:

    - sec_scheme_cfg -- [in] Security scheme specific configuration data

    - sec_scheme_handle_out -- [out] Security scheme specific configuration handle

  返回:

    - ESP_OK, if `sec_scheme_handle_out` was populated successfully with the scheme configuration;

    - ESP_ERR_INVALID_ARG, if `scheme_cfg_hmac` is NULL;

    - ESP_ERR_NO_MEM, No memory for the scheme-specific handle `sec_scheme_handle_out`

esp_err_t nvs_sec_provider_deregister(nvs_sec_scheme_t *sec_scheme_handle)

  Deregister the NVS encryption scheme registered with the given handle.

  参数:

    sec_scheme_handle -- [in] Security scheme specific configuration handle

  返回:

    - ESP_OK, if the scheme registered with `sec_scheme_handle` was deregistered successfully

    - ESP_ERR_INVALID_ARG, if `sec_scheme_handle` is NULL;

### Structures

struct nvs_sec_config_flash_enc_t

  Flash encryption-based scheme specific configuration data.

  Public Members

  const esp_partition_t *nvs_keys_part

    Partition of subtype `nvs_keys` holding the NVS encryption keys

struct nvs_sec_config_hmac_t

  HMAC-based scheme specific configuration data.

  Public Members

  hmac_key_id_t hmac_key_id

    HMAC Key ID used for generating the NVS encryption keys

### Macros

ESP_ERR_NVS_SEC_BASE

  Starting number of error codes

ESP_ERR_NVS_SEC_HMAC_KEY_NOT_FOUND

  HMAC Key required to generate the NVS encryption keys not found

ESP_ERR_NVS_SEC_HMAC_KEY_BLK_ALREADY_USED

  Provided eFuse block for HMAC key generation is already in use

ESP_ERR_NVS_SEC_HMAC_KEY_GENERATION_FAILED

  Failed to generate/write the HMAC key to eFuse

ESP_ERR_NVS_SEC_HMAC_XTS_KEYS_DERIV_FAILED

  Failed to derive the NVS encryption keys based on the HMAC-based scheme

NVS_SEC_PROVIDER_CFG_FLASH_ENC_DEFAULT()

  Helper for populating the Flash encryption-based scheme specific configuration data.

NVS_SEC_PROVIDER_CFG_HMAC_DEFAULT()

  Helper for populating the HMAC-based scheme specific configuration data.

### Enumerations

enum nvs_sec_scheme_id_t

  NVS Encryption Keys Protection Scheme.

  Values:

  enumerator NVS_SEC_SCHEME_FLASH_ENC

    Protect NVS encryption keys using Flash Encryption

  enumerator NVS_SEC_SCHEME_HMAC

    Protect NVS encryption keys using HMAC peripheral

  enumerator NVS_SEC_SCHEME_MAX

此文档对您有帮助吗？

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

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