Google 致力于为黑人社区推动种族平等。查看具体举措

硬件封装密钥

与大多数磁盘和文件加密软件一样,Android 的存储加密传统上依赖于系统内存中存在的原始加密密钥,以便可以执行加密。即使通过专用硬件(而非软件)执行加密,软件通常仍需管理原始加密密钥。

传统上,这并不被认为是个问题,因为密钥在离线攻击期间不存在。离线攻击是存储加密旨在防范的主要攻击类型。但是,人们希望能够针对其他类型的攻击增强保护,例如冷启动攻击和在线攻击。在在线攻击中,攻击者可能会泄露系统内存,而不会全面入侵设备。

为解决此问题,Android 11 引入了对硬件封装密钥的支持,其中存在硬件支持。硬件封装密钥是仅专用硬件知晓的原始形式的存储密钥;软件只能查看并使用封装(加密)形式的密钥。此硬件必须能够生成和导入存储密钥、以临时形式和长期形式封装存储密钥、派生子密钥、直接将一个子密钥编程到内嵌加密引擎,以及向软件返回单独的子密钥。

注意:内嵌加密引擎(或内嵌加密硬件)是指对正在传入/传出存储设备的数据进行加密/解密的硬件。通常情况下,这是实现了相应 JEDEC 规范所定义的加密扩展项的 UFS 或 eMMC 主机控制器。

设计

本部分介绍了硬件封装密钥功能的设计,包括此功能所需的硬件支持。这里讨论的重点是文件级加密 (FBE),但解决方案同样适用于元数据加密

为避免在系统内存中需要用到原始加密密钥,一种方法是将其仅保留在内嵌加密引擎的密钥槽中。不过,这种方法存在一些问题:

  • 加密密钥的数量可能会超过密钥槽数。
  • 内嵌加密引擎只能用于加密/解密磁盘上的完整数据块。不过,对于 FBE 而言,软件仍需要能够执行其他加密工作,例如加密文件名和派生密钥标识符。软件仍需访问原始 FBE 密钥,才能执行这类其他工作。

为避免这些问题,存储密钥会变成硬件封装密钥,硬件封装密钥只能由专用硬件解封和使用。这样可以支持不限数量的密钥。此外,系统会修改密钥层次结构,并将其部分移至此硬件,从而可以将子密钥返回给软件,用于处理无法使用内嵌加密引擎的任务。

密钥层次结构

可以利用 HKDFKDF(密钥派生函数)从其他密钥派生密钥,从而生成密钥层次结构。

下图展示了不使用硬件封装密钥时 FBE 的典型密钥层次结构:

FBE 密钥层次结构(标准)
图 1. FBE 密钥层次结构(标准)

FBE 类密钥是 Android 传递给 Linux 内核以解锁一组特定加密目录(例如针对特定 Android 用户的凭据加密存储空间)的原始加密密钥。(在此内核中,这种密钥称为 fscrypt 主密钥)。内核会根据该密钥派生以下子密钥:

  • 密钥标识符。此标识符不用于加密,而是作为一个值用于标识保护特定文件或目录的密钥。
  • 文件内容加密密钥
  • 文件名加密密钥

下图则展示了使用硬件封装密钥时 FBE 的密钥层次结构:

FBE 密钥层次结构(使用硬件封装密钥)
图 2. FBE 密钥层次结构(使用硬件封装密钥)

与前一种情况相比,此密钥层次结构新增了一个额外级别,并且改变了文件内容加密密钥的位置。根节点仍代表由 Android 传递到 Linux 以解锁一组加密目录的密钥。但是,该密钥现在采用临时封装形式,并且必须传给专用硬件才能使用。该硬件必须实现两个获取一个临时封装密钥的接口:

  • 第一个接口用于派生 inline_encryption_key 并将其直接编程到内嵌加密引擎的密钥槽。这样,软件无需访问原始密钥,即可加密/解密文件内容。在 Android 通用内核中,此接口与 blk_ksm_ll_ops::keyslot_program 操作相对应,此操作必须由存储驱动程序实现。
  • 第二个接口用于派生并返回 sw_secret(“软件 Secret”,在某些地方也称为“原始 Secret”),后者作为一个密钥由 Linux 用于为文件内容加密之外的所有加密派生子密钥。在 Android 通用内核中,此接口与 blk_ksm_ll_ops::derive_raw_secret 操作相对应,此操作必须由存储驱动程序实现。

要从原始存储密钥派生 inline_encryption_keysw_secret,硬件必须使用强加密 KDF。此 KDF 必须遵循加密最佳做法;至少具有 256 位的安全性,也就是说,足以应对以后使用的任何算法。在派生每种类型的子密钥时,此 KDF 还必须使用不同的标签、上下文和/或应用特定信息字符串,以确保生成的子密钥经过加密隔离,也就是说,知道其中一个子密钥并不会泄露任何其他子密钥。原始存储密钥已是均匀随机密钥,因此不需要延伸密钥。

从技术上讲,可以使用任何满足安全要求的 KDF。但是,出于测试目的,需要在测试代码中重新实现相同的 KDF。目前,已审核并实现了一个 KDF,可以在 vts_kernel_encryption_test 的源代码中找到该 KDF。建议硬件使用此 KDF,此 KDF 使用 NIST SP 800-108“计数器模式的 KDF”,并将 AES-256-CMAC 作为 PRF。请注意,为确保兼容性,该算法的所有部分都必须完全相同,包括为每个子密钥选择的 KDF 上下文和标签。

密钥封装

为了满足硬件封装密钥的安全目标,定义了两种密钥封装:

  • 临时封装:硬件会使用一个在每次启动时随机生成的、不在硬件外部直接公开的密钥来加密原始密钥。
  • 长期封装:硬件使用一个不在硬件外部直接公开的、内置于硬件的唯一永久性密钥来加密原始密钥。

传递到 Linux 内核以解锁存储空间的所有密钥都是临时封装类型。这样,即便攻击者能够从系统内存中提取使用中的密钥,我们仍能确保该密钥在设备外部以及在重新启动后的设备上都无法使用。

同时,Android 仍然需要在磁盘上存储加密版本的密钥,以便一开始能够解锁密钥。原始密钥可达到此目的。不过,最好始终不让原始密钥出现在系统内存中,这样,即便在启动时提取了原始密钥,也永远无法提取到设备外使用。因此,定义了长期封装的概念。

为了支持对以这两种不同方式封装的密钥进行管理,硬件必须实现以下接口:

  • 用于生成和导入存储密钥的接口,会以长期封装形式返回存储密钥。这些接口是通过 KeyMint 间接访问的,并且与 TAG_STORAGE_KEY KeyMint 标记相对应。 vold 使用“生成”功能来生成新的存储密钥以供 Android 使用,而 vts_kernel_encryption_test 则使用“导入”功能来导入测试密钥。
  • 将长期封装的存储密钥转换为临时封装的存储密钥的接口。这与 convertStorageKeyToEphemeral KeyMint 方法相对应。voldvts_kernel_encryption_test 都使用此方法解锁存储空间。

密钥封装算法是一种实现细节,但它应使用强 AEAD,例如采用随机 IV 的 AES-256-GCM。

需要的软件变更

AOSP 已有一个支持硬件封装密钥的基本框架。这包括用户空间组件中的支持(例如 vold)以及 blk-crypto、fscrypt 和 dm-default-key 中的 Linux 内核支持。

不过,需要做出一些特定于实现的变更。

KeyMint 变更

必须修改设备的 KeyMint 实现,以支持 TAG_STORAGE_KEY 并实现 convertStorageKeyToEphemeral 方法。

在 Keymaster 中,使用 exportKey,而不是 convertStorageKeyToEphemeral

Linux 内核变更

必须修改设备内嵌加密引擎的 Linux 内核驱动程序,以设置 BLK_CRYPTO_FEATURE_WRAPPED_KEYS、使 keyslot_program()keyslot_evict() 操作支持编程/逐出硬件封装密钥,以及实现 derive_raw_secret() 操作。

测试

虽然使用硬件封装密钥的加密比使用标准密钥的加密更难测试,但通过导入测试密钥并重新实现硬件所完成的密钥派生,还是可以进行测试的。这是在 vts_kernel_encryption_test 中实现的。要运行此测试,请运行以下命令:

atest -v vts_kernel_encryption_test

阅读测试日志,并确认硬件封装密钥测试用例(例如 FBEPolicyTest.TestAesInlineCryptOptimizedHwWrappedKeyPolicyDmDefaultKeyTest.TestHwWrappedKey)没有因为未检测到对硬件封装密钥的支持而被跳过,因为在这种情况下测试结果仍然为“通过”。

启用

设备能正常支持硬件封装密钥后,可以对设备的 fstab 文件进行以下更改,使 Android 将其用于 FBE 和元数据加密:

  • FBE:将 wrappedkey_v0 标记添加到 fileencryption 参数中。例如,使用 fileencryption=::inlinecrypt_optimized+wrappedkey_v0。如需了解更多详情,请参阅 FBE 文档
  • 元数据加密:将 wrappedkey_v0 标记添加到 metadata_encryption 参数中。例如,使用 metadata_encryption=:wrappedkey_v0。如需了解更多详情,请参阅元数据加密文档