文件系统级别加密 (fscrypt)

简介

fscrypt 是一个库,文件系统可以挂钩到该库中以支持文件和目录的透明加密。

注意:本文档中的“fscrypt”指的是内核级别的部分,在 fs/crypto/ 中实现,而不是用户空间工具 fscrypt。本文档仅涵盖内核级别的部分。有关如何使用加密的命令行示例,请参阅用户空间工具 fscrypt 的文档。此外,建议使用 fscrypt 用户空间工具或其他现有的用户空间工具,如 fscryptctlAndroid 的密钥管理系统,而不是直接使用内核的 API。使用现有工具可以减少引入您自己的安全漏洞的机会。(不过,为了完整起见,本文档仍然涵盖了内核的 API。)

与 dm-crypt 不同,fscrypt 在文件系统级别运行,而不是在块设备级别运行。这使得它可以使用不同的密钥加密不同的文件,并且在同一文件系统上拥有未加密的文件。这对于多用户系统非常有用,在多用户系统中,每个用户的静态数据都需要与其他用户进行加密隔离。但是,除了文件名之外,fscrypt 不加密文件系统元数据。

与 eCryptfs(一种堆叠文件系统)不同,fscrypt 直接集成到受支持的文件系统中——目前是 ext4、F2FS、UBIFS 和 CephFS。这允许读取和写入加密文件,而无需在页缓存中缓存解密和加密的页面,从而几乎减少了一半的内存使用量,并使其与未加密文件保持一致。同样,只需要一半数量的 dentry 和 inode。eCryptfs 还将加密文件名限制为 143 字节,从而导致应用程序兼容性问题;fscrypt 允许完整的 255 字节 (NAME_MAX)。最后,与 eCryptfs 不同,fscrypt API 可以被非特权用户使用,而无需挂载任何东西。

fscrypt 不支持就地加密文件。相反,它支持将空目录标记为加密。然后,在用户空间提供密钥后,在该目录树中创建的所有常规文件、目录和符号链接都会被透明地加密。

威胁模型

离线攻击

如果用户空间选择了一个强加密密钥,那么在块设备内容在单个时间点永久离线泄露的情况下,fscrypt 可以保护文件内容和文件名的机密性。fscrypt 不保护非文件名元数据的机密性,例如文件大小、文件权限、文件时间戳和扩展属性。此外,文件中空洞(逻辑上包含所有零的未分配块)的存在和位置不受保护。

如果攻击者能够在授权用户稍后访问文件系统之前离线操纵文件系统,则 fscrypt 不能保证保护机密性或真实性。

在线攻击

fscrypt(以及一般的存储加密)只能提供有限的在线攻击保护。详细信息:

旁道攻击

fscrypt 对旁道攻击(如定时或电磁攻击)的抵抗力仅限于底层 Linux 加密 API 算法或内联加密硬件的抵抗力。如果使用了易受攻击的算法,如基于表的 AES 实现,则攻击者可能会对在线系统发起旁道攻击。旁道攻击也可能针对使用解密数据的应用程序发起。

未经授权的文件访问

添加加密密钥后,fscrypt 不会向同一系统上的其他用户隐藏纯文本文件内容或文件名。相反,应为此目的使用现有的访问控制机制,如文件模式位、POSIX ACL、LSM 或命名空间。

(对于此背后的原因,请理解,在添加密钥时,从系统本身的角度来看,数据的机密性不是由加密的数学属性来保护的,而是仅由内核的正确性来保护的。因此,任何加密特定的访问控制检查都只会由内核代码强制执行,因此与已经存在的各种访问控制机制在很大程度上是多余的。)

只读内核内存泄露

除非使用 硬件封装的密钥,否则能够读取任意内核内存的攻击者(例如,通过发起物理攻击或利用内核安全漏洞)可能会泄露所有当前正在使用的 fscrypt 密钥。这也会扩展到冷启动攻击;如果系统突然断电,系统使用的密钥可能会在内存中保留一小段时间。

但是,如果使用硬件封装的密钥,则 fscrypt 主密钥和文件内容加密密钥(而不是其他类型的 fscrypt 子密钥,如文件名加密密钥)将受到保护,免受任意内核内存泄露的影响。

此外,fscrypt 允许从内核中删除加密密钥,这可以保护它们免受以后的泄露。

更详细地说,FS_IOC_REMOVE_ENCRYPTION_KEY ioctl(或 FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS ioctl)可以从内核内存中擦除主加密密钥。如果这样做,它还会尝试驱逐所有已使用该密钥“解锁”的缓存 inode,从而擦除其每个文件的密钥,并使它们再次显示为“已锁定”,即以密文或加密形式显示。

但是,这些 ioctl 有一些限制:

  • 对于正在使用的文件,每个文件的密钥将不会被删除或擦除。因此,为了获得最大效果,用户空间应在删除主密钥之前关闭相关的加密文件和目录,并终止任何工作目录位于受影响的加密目录中的进程。

  • 内核无法神奇地擦除用户空间可能也拥有的主密钥副本。因此,用户空间还必须擦除它制作的所有主密钥副本;通常应该在 FS_IOC_ADD_ENCRYPTION_KEY 后立即执行此操作,而无需等待 FS_IOC_REMOVE_ENCRYPTION_KEY。当然,这同样适用于密钥层次结构中的所有更高级别。用户空间还应遵循其他安全预防措施,例如 mlock() 内存包含密钥,以防止其被换出。

  • 一般来说,内核 VFS 缓存中解密的内容和文件名会被释放,但不会被擦除。因此,即使在擦除相应的密钥之后,也可以从释放的内存中恢复其中的一部分。为了部分解决这个问题,您可以在内核命令行中添加 init_on_free=1。但是,这会带来性能成本。

  • 密钥可能仍然存在于 CPU 寄存器、加密加速器硬件(如果加密 API 用于实现任何算法)或其他未在此处明确考虑的地方。

完全系统泄露

获得“root”访问权限和/或执行任意内核代码能力的攻击者可以自由地泄露受任何正在使用的 fscrypt 密钥保护的数据。因此,通常 fscrypt 在这种情况下不提供有意义的保护。(受在整个攻击过程中不存在的密钥保护的数据仍然受到保护,但受上述密钥删除的限制影响,如果密钥在攻击之前被删除。)

但是,如果使用 硬件封装的密钥,则此类攻击者将无法以在系统断电后可用的形式泄露主密钥或文件内容密钥。如果攻击者受到严重的时间限制和/或带宽限制,因此他们只能泄露一些数据,并且需要依赖以后的离线攻击来泄露其余数据,这可能会很有用。

v1 策略的限制

v1 加密策略在在线攻击方面存在一些弱点:

  • 无法验证提供的主密钥是否正确。因此,恶意用户可以暂时将错误的密钥与他们具有只读访问权限的另一个用户的加密文件相关联。由于文件系统缓存,即使其他用户的密钥链中有正确的密钥,该错误密钥也会被其他用户访问这些文件时使用。这违反了“只读访问”的含义。

  • 每个文件的密钥泄露也会泄露从中派生的主密钥。

  • 非 root 用户无法安全地删除加密密钥。

以上所有问题都通过 v2 加密策略得到修复。由于此原因和其他原因,建议在所有新的加密目录中使用 v2 加密策略。

密钥层次结构

注意:本节假设使用原始密钥而不是硬件封装的密钥。使用硬件封装的密钥会稍微修改密钥层次结构。有关详细信息,请参阅 硬件封装的密钥

主密钥

每个加密目录树都由主密钥保护。主密钥的长度可达 64 字节,并且必须至少与所使用的内容和文件名加密模式的安全性强度一样长。例如,如果使用任何 AES-256 模式,则主密钥必须至少为 256 位,即 32 字节。如果密钥被 v1 加密策略使用并且使用了 AES-256-XTS,则适用更严格的要求;此类密钥必须为 64 字节。

要“解锁”加密目录树,用户空间必须提供适当的主密钥。主密钥的数量可以是任意数量,每个主密钥保护任意数量的文件系统上的任意数量的目录树。

主密钥必须是真正的加密密钥,即与相同长度的随机字节串无法区分。这意味着用户不得直接使用密码作为主密钥、零填充较短的密钥或重复较短的密钥。如果用户空间犯了任何此类错误,则无法保证安全性,因为加密证明和分析将不再适用。

相反,用户应使用加密安全的随机数生成器或使用 KDF(密钥派生函数)生成主密钥。内核不执行任何密钥拉伸;因此,如果用户空间从低熵机密(如密码)派生密钥,则必须使用为此目的设计的 KDF,如 scrypt、PBKDF2 或 Argon2。

密钥派生函数

除了一个例外,fscrypt 永远不会直接使用主密钥进行加密。相反,它们仅用作 KDF(密钥派生函数)的输入,以派生实际密钥。

用于特定主密钥的 KDF 因密钥是用于 v1 加密策略还是用于 v2 加密策略而异。用户不得将同一密钥用于 v1 和 v2 加密策略。(目前尚不清楚针对此特定密钥重用案例的真实攻击,但无法保证其安全性,因为加密证明和分析将不再适用。)

对于 v1 加密策略,KDF 仅支持派生每个文件的加密密钥。它的工作原理是使用 AES-128-ECB 加密主密钥,使用文件的 16 字节 nonce 作为 AES 密钥。生成的密文用作派生密钥。如果密文比需要的长,则将其截断到需要的长度。

对于 v2 加密策略,KDF 是 HKDF-SHA512。主密钥作为“输入密钥材料”传递,不使用 salt,并且为每个要派生的不同密钥使用不同的“特定于应用程序的信息字符串”。例如,当派生每个文件的加密密钥时,特定于应用程序的信息字符串是文件的 nonce,前缀为“fscrypt\0”和一个上下文字节。不同的上下文字节用于其他类型的派生密钥。

HKDF-SHA512 优于原始的基于 AES-128-ECB 的 KDF,因为 HKDF 更灵活、不可逆,并且均匀分布来自主密钥的熵。HKDF 也已标准化并被其他软件广泛使用,而基于 AES-128-ECB 的 KDF 是临时的。

每个文件的加密密钥

由于每个主密钥可以保护许多文件,因此有必要“调整”每个文件的加密,以使两个文件中的相同纯文本不会映射到相同的密文,反之亦然。在大多数情况下,fscrypt 通过派生每个文件的密钥来做到这一点。当创建一个新的加密 inode(常规文件、目录或符号链接)时,fscrypt 随机生成一个 16 字节的 nonce 并将其存储在 inode 的加密 xattr 中。然后,它使用 KDF(如 密钥派生函数 中所述)从主密钥和 nonce 派生文件的密钥。

选择密钥派生而不是密钥包装,因为包装密钥需要更大的 xattr,这些 xattr 更不可能内联地容纳在文件系统的 inode 表中,并且密钥包装似乎没有任何明显的优势。特别是,目前没有要求支持使用多个备用主密钥解锁文件或支持轮换主密钥。相反,主密钥可以在用户空间中包装,例如 fscrypt 工具所做的那样。

DIRECT_KEY 策略

Adiantum 加密模式(请参阅 加密模式和用法)适用于内容和文件名加密,并且它接受长 IV——足够长以容纳 8 字节的数据单元索引和 16 字节的每个文件 nonce。此外,每个 Adiantum 密钥的开销大于 AES-256-XTS 密钥的开销。

因此,为了提高性能并节省内存,对于 Adiantum,支持“直接密钥”配置。当用户通过在 fscrypt 策略中设置 FSCRYPT_POLICY_FLAG_DIRECT_KEY 来启用此功能时,不使用每个文件的加密密钥。相反,每当加密任何数据(内容或文件名)时,文件的 16 字节 nonce 都包含在 IV 中。而且:

  • 对于 v1 加密策略,加密直接使用主密钥完成。因此,用户不得将同一主密钥用于任何其他目的,甚至用于其他 v1 策略。

  • 对于 v2 加密策略,加密使用使用 KDF 派生的每个模式密钥完成。用户可以将同一主密钥用于其他 v2 加密策略。

IV_INO_LBLK_64 策略

当在 fscrypt 策略中设置 FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64 时,加密密钥从主密钥、加密模式编号和文件系统 UUID 派生。这通常会导致受同一主密钥保护的所有文件共享单个内容加密密钥和单个文件名加密密钥。为了仍然以不同的方式加密不同文件的数据,inode 编号包含在 IV 中。因此,可能不允许缩小文件系统。

此格式针对与符合 UFS 标准的内联加密硬件一起使用进行了优化,该标准仅支持每个 I/O 请求 64 位 IV,并且可能只有少量密钥槽。

IV_INO_LBLK_32 策略

IV_INO_LBLK_32 策略的工作方式与 IV_INO_LBLK_64 类似,只是对于 IV_INO_LBLK_32,inode 编号使用 SipHash-2-4(其中 SipHash 密钥从主密钥派生)进行哈希处理,并添加到文件数据单元索引 mod 2^32 以生成 32 位 IV。

此格式针对与符合 eMMC v5.2 标准的内联加密硬件一起使用进行了优化,该标准仅支持每个 I/O 请求 32 位 IV,并且可能只有少量密钥槽。此格式会导致一定程度的 IV 重用,因此仅应在由于硬件限制而必要时使用。

密钥标识符

对于用于 v2 加密策略的主密钥,还会使用 KDF 派生一个唯一的 16 字节“密钥标识符”。此值以明文形式存储,因为它需要可靠地标识密钥本身。

Dirhash 密钥

对于使用纯文本文件名上的秘密密钥 dirhash 索引的目录,KDF 还用于为每个目录派生一个 128 位 SipHash-2-4 密钥,以便对文件名进行哈希处理。这与派生每个文件的加密密钥的工作方式相同,只是使用了不同的 KDF 上下文。目前,只有区分大小写(“不区分大小写”)的加密目录使用这种哈希风格。

加密模式和用法

fscrypt 允许为文件内容指定一种加密模式,并为文件名指定一种加密模式。允许不同的目录树使用不同的加密模式。

支持的模式

目前,支持以下加密模式对:

  • 用于内容的 AES-256-XTS 和用于文件名的 AES-256-CBC-CTS

  • 用于内容的 AES-256-XTS 和用于文件名的 AES-256-HCTR2

  • Adiantum 用于内容和文件名

  • 用于内容的 AES-128-CBC-ESSIV 和用于文件名的 AES-128-CBC-CTS

  • 用于内容的 SM4-XTS 和用于文件名的 SM4-CBC-CTS

注意:在 API 中,“CBC”表示 CBC-ESSIV,“CTS”表示 CBC-CTS。因此,例如,FSCRYPT_MODE_AES_256_CTS 表示 AES-256-CBC-CTS。

由于难以处理密文扩展,因此目前不支持经过身份验证的加密模式。因此,内容加密在 XTS 模式CBC-ESSIV 模式中使用块密码,或者使用宽块密码。文件名加密在 CBC-CTS 模式中使用块密码或宽块密码。

(AES-256-XTS, AES-256-CBC-CTS)对是推荐的默认设置。它也是保证如果内核完全支持 fscrypt 则始终支持的唯一选项;请参阅 内核配置选项

(AES-256-XTS, AES-256-HCTR2)对也是一个不错的选择,可以将文件名加密升级为使用宽块密码。(宽块密码,也称为可调整的超伪随机置换,具有更改一位会扰乱整个结果的属性。)如 文件名加密 中所述,宽块密码是问题域的理想模式,尽管 CBC-CTS 是替代方案中“最不坏”的选择。有关 HCTR2 的更多信息,请参阅 HCTR2 论文

建议在由于 AES 缺乏 AES 硬件加速而速度太慢的系统上使用 Adiantum。Adiantum 是一种宽块密码,它使用 XChaCha12 和 AES-256 作为其底层组件。大部分工作由 XChaCha12 完成,当 AES 加速不可用时,XChaCha12 比 AES 快得多。有关 Adiantum 的更多信息,请参阅 Adiantum 论文

(AES-128-CBC-ESSIV, AES-128-CBC-CTS)对的存在只是为了支持唯一的 AES 加速形式是非 CPU 加密加速器(如 CAAM 或 CESA)且不支持 XTS 的系统。

其余的模式对是“国家自豪密码”:

  • (SM4-XTS, SM4-CBC-CTS)

一般来说,这些密码本身并不是“坏”的,但与 AES 和 ChaCha 等常用选择相比,它们的安全审查有限。它们也没有带来太多新东西。建议仅在强制要求使用这些密码时才使用它们。

内核配置选项

启用 fscrypt 支持 (CONFIG_FS_ENCRYPTION) 会自动拉入使用 AES-256-XTS 和 AES-256-CBC-CTS 加密所需的基础加密 API 支持。为了获得最佳性能,强烈建议启用任何可用的平台特定的 kconfig 选项,这些选项为希望使用的算法提供加速。对任何“非默认”加密模式的支持通常也需要额外的 kconfig 选项。

下面,一些相关的选项按加密模式列出。请注意,未在下面列出的加速选项可能适用于您的平台;请参阅 kconfig 菜单。还可以将文件内容加密配置为使用内联加密硬件而不是内核加密 API(请参阅 内联加密支持);在这种情况下,文件内容模式不需要在内核加密 API 中支持,但文件名模式仍然需要。

  • AES-256-XTS 和 AES-256-CBC-CTS
    • 推荐:
      • arm64:CONFIG_CRYPTO_AES_ARM64_CE_BLK

      • x86:CONFIG_CRYPTO_AES_NI_INTEL

  • AES-256-HCTR2
    • 必需:
      • CONFIG_CRYPTO_HCTR2

    • 推荐:
      • arm64:CONFIG_CRYPTO_AES_ARM64_CE_BLK

      • arm64:CONFIG_CRYPTO_POLYVAL_ARM64_CE

      • x86:CONFIG_CRYPTO_AES_NI_INTEL

      • x86:CONFIG_CRYPTO_POLYVAL_CLMUL_NI

  • Adiantum
    • 必需:
      • CONFIG_CRYPTO_ADIANTUM

    • 推荐:
      • arm32:CONFIG_CRYPTO_NHPOLY1305_NEON

      • arm64:CONFIG_CRYPTO_NHPOLY1305_NEON

      • x86:CONFIG_CRYPTO_NHPOLY1305_SSE2

      • x86:CONFIG_CRYPTO_NHPOLY1305_AVX2

  • AES-128-CBC-ESSIV 和 AES-128-CBC-CTS
    • 必需:
      • CONFIG_CRYPTO_ESSIV

      • CONFIG_CRYPTO_SHA256 或另一个 SHA-256 实现

    • 推荐:
      • AES-CBC 加速

fscrypt 还使用 HMAC-SHA512 进行密钥派生,因此建议启用 SHA-512 加速:

  • SHA-512
    • 推荐:
      • arm64:CONFIG_CRYPTO_SHA512_ARM64_CE

      • x86:CONFIG_CRYPTO_SHA512_SSSE3

内容加密

对于内容加密,每个文件的内容被分成“数据单元”。每个数据单元都是独立加密的。每个数据单元的 IV 包含数据单元在文件中的从零开始的索引。这确保了文件中的每个数据单元都被不同地加密,这对于防止信息泄露至关重要。

注意:依赖于文件偏移量的加密意味着不支持像“折叠范围”和“插入范围”这样重新排列文件盘区映射的操作加密文件。

数据单元的大小有两种情况:

  • 固定大小的数据单元。这是除 UBIFS 之外的所有文件系统的工作方式。文件的所有数据单元大小相同;如果需要,最后一个数据单元会进行零填充。默认情况下,数据单元大小等于文件系统块大小。在某些文件系统上,用户可以通过加密策略的 log2_data_unit_size 字段选择子块数据单元大小;请参阅 FS_IOC_SET_ENCRYPTION_POLICY

  • 可变大小的数据单元。这是 UBIFS 的工作方式。每个“UBIFS 数据节点”都被视为一个加密数据单元。每个数据单元包含可变长度的可能压缩的数据,零填充到下一个 16 字节边界。用户无法在 UBIFS 上选择子块数据单元大小。

在压缩 + 加密的情况下,压缩的数据会被加密。UBIFS 压缩如上所述工作。f2fs 压缩的工作方式略有不同;它将许多文件系统块压缩为较少的文件系统块。因此,f2fs 压缩的文件仍然使用固定大小的数据单元,并且它的加密方式与包含空洞的文件类似。

密钥层次结构 中所述,默认加密设置使用每个文件的密钥。在这种情况下,每个数据单元的 IV 只是数据单元在文件中的索引。但是,用户可以选择不使用每个文件的密钥的加密设置。对于这些设置,某种文件标识符按如下方式包含在 IV 中:

  • 使用 DIRECT_KEY 策略,数据单元索引放置在 IV 的 0-63 位中,文件的 nonce 放置在 64-191 位中。

  • 使用 IV_INO_LBLK_64 策略,数据单元索引放置在 IV 的 0-31 位中,文件的 inode 编号放置在 32-63 位中。仅当数据单元索引和 inode 编号适合 32 位时,才允许此设置。

  • 使用 IV_INO_LBLK_32 策略,文件的 inode 编号经过哈希处理并添加到数据单元索引中。结果值被截断为 32 位并放置在 IV 的 0-31 位中。仅当数据单元索引和 inode 编号适合 32 位时,才允许此设置。

IV 的字节顺序始终为小端序。

如果用户为内容模式选择 FSCRYPT_MODE_AES_128_CBC,则会自动包含 ESSIV 层。在这种情况下,在将 IV 传递给 AES-128-CBC 之前,它会使用 AES-256 进行加密,其中 AES-256 密钥是文件内容加密密钥的 SHA-256 哈希。

文件名加密

对于文件名,每个完整的文件名一次加密。由于需要保留对高效目录查找和最大 255 字节的文件名的支持,因此同一 IV 用于目录中的每个文件名。

但是,每个加密目录仍然使用唯一的密钥,或者可替代地,IV 中包含文件的 nonce(对于 DIRECT_KEY 策略)或 inode 编号(对于 IV_INO_LBLK_64 策略)。因此,IV 重用仅限于单个目录内。

对于 CBC-CTS,IV 重用意味着当纯文本文件名共享至少与密码块大小(对于 AES 为 16 字节)一样长的公共前缀时,相应的加密文件名也将共享一个公共前缀。这是不可取的。Adiantum 和 HCTR2 没有这种弱点,因为它们是宽块加密模式。

所有支持的文件名加密模式都接受任何明文长度 >= 16 字节;不需要密码块对齐。但是,短于 16 字节的文件名在加密之前会被 NUL 填充到 16 字节。此外,为了减少通过文件名密文泄漏文件名长度,所有文件名都会被 NUL 填充到下一个 4、8、16 或 32 字节边界(可配置)。建议使用 32,因为它提供最佳的机密性,但代价是目录条目会占用稍微更多的空间。请注意,由于 NUL (\0) 在文件名中不是有效字符,因此填充永远不会产生重复的明文。

符号链接目标被视为一种文件名,并以与目录条目中的文件名相同的方式加密,但 IV 重用不是问题,因为每个符号链接都有自己的 inode。

用户 API

设置加密策略

FS_IOC_SET_ENCRYPTION_POLICY

FS_IOC_SET_ENCRYPTION_POLICY ioctl 在一个空目录上设置加密策略,或者验证目录或常规文件是否已具有指定的加密策略。它接受一个指向 struct fscrypt_policy_v1 或 struct fscrypt_policy_v2 的指针,定义如下

#define FSCRYPT_POLICY_V1               0
#define FSCRYPT_KEY_DESCRIPTOR_SIZE     8
struct fscrypt_policy_v1 {
        __u8 version;
        __u8 contents_encryption_mode;
        __u8 filenames_encryption_mode;
        __u8 flags;
        __u8 master_key_descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE];
};
#define fscrypt_policy  fscrypt_policy_v1

#define FSCRYPT_POLICY_V2               2
#define FSCRYPT_KEY_IDENTIFIER_SIZE     16
struct fscrypt_policy_v2 {
        __u8 version;
        __u8 contents_encryption_mode;
        __u8 filenames_encryption_mode;
        __u8 flags;
        __u8 log2_data_unit_size;
        __u8 __reserved[3];
        __u8 master_key_identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
};

必须按如下方式初始化此结构

  • 如果使用 struct fscrypt_policy_v1,则 version 必须为 FSCRYPT_POLICY_V1 (0);如果使用 struct fscrypt_policy_v2,则必须为 FSCRYPT_POLICY_V2 (2)。(注意:我们将原始策略版本称为“v1”,尽管它的版本代码实际上是 0。)对于新的加密目录,请使用 v2 策略。

  • contents_encryption_modefilenames_encryption_mode 必须设置为来自 <linux/fscrypt.h> 的常量,这些常量标识要使用的加密模式。如果不确定,请为 contents_encryption_mode 使用 FSCRYPT_MODE_AES_256_XTS (1),为 filenames_encryption_mode 使用 FSCRYPT_MODE_AES_256_CTS (4)。有关详细信息,请参阅加密模式和用法

    v1 加密策略仅支持三种模式组合:(FSCRYPT_MODE_AES_256_XTS, FSCRYPT_MODE_AES_256_CTS)、(FSCRYPT_MODE_AES_128_CBC, FSCRYPT_MODE_AES_128_CTS) 和 (FSCRYPT_MODE_ADIANTUM, FSCRYPT_MODE_ADIANTUM)。v2 策略支持支持的模式中记录的所有组合。

  • flags 包含来自 <linux/fscrypt.h> 的可选标志

    • FSCRYPT_POLICY_FLAGS_PAD_*: 加密文件名时要使用的 NUL 填充量。如果不确定,请使用 FSCRYPT_POLICY_FLAGS_PAD_32 (0x3)。

    • FSCRYPT_POLICY_FLAG_DIRECT_KEY:请参阅DIRECT_KEY 策略

    • FSCRYPT_POLICY_FLAG_IV_INO_LBLK_64:请参阅IV_INO_LBLK_64 策略

    • FSCRYPT_POLICY_FLAG_IV_INO_LBLK_32:请参阅IV_INO_LBLK_32 策略

    v1 加密策略仅支持 PAD_* 和 DIRECT_KEY 标志。其他标志仅受 v2 加密策略支持。

    DIRECT_KEY、IV_INO_LBLK_64 和 IV_INO_LBLK_32 标志是互斥的。

  • log2_data_unit_size 是数据单元大小的 log2(以字节为单位),或者为 0 以选择默认数据单元大小。数据单元大小是文件内容加密的粒度。例如,将 log2_data_unit_size 设置为 12 会导致文件内容以 4096 字节的数据单元传递给底层加密算法(例如 AES-256-XTS),每个数据单元都有自己的 IV。

    并非所有文件系统都支持设置 log2_data_unit_size。ext4 和 f2fs 自 Linux v6.7 起支持它。在支持它的文件系统上,支持的非零值包括 9 到文件系统块大小的 log2(包括边界值)。默认值 0 选择文件系统块大小。

    log2_data_unit_size 的主要用例是选择小于文件系统块大小的数据单元大小,以便与仅支持较小数据单元大小的内联加密硬件兼容。/sys/block/$disk/queue/crypto/ 可能有助于检查特定系统的内联加密硬件支持哪些数据单元大小。

    除非您确定需要它,否则将此字段留空。使用不必要的小数据单元大小会降低性能。

  • 对于 v2 加密策略,__reserved 必须为零。

  • 对于 v1 加密策略,master_key_descriptor 指定如何在密钥环中查找主密钥;请参阅添加密钥。由用户空间为每个主密钥选择唯一的 master_key_descriptor。e4crypt 和 fscrypt 工具使用 SHA-512(SHA-512(master_key)) 的前 8 个字节,但不需要此特定方案。此外,执行 FS_IOC_SET_ENCRYPTION_POLICY 时,主密钥不必已经在密钥环中。但是,必须先添加它,然后才能在加密目录中创建任何文件。

    对于 v2 加密策略,master_key_descriptor 已被 master_key_identifier 替换,后者更长并且不能任意选择。相反,必须首先使用FS_IOC_ADD_ENCRYPTION_KEY添加密钥。然后,内核在 struct fscrypt_add_key_arg 中返回的 key_spec.u.identifier 必须用作 struct fscrypt_policy_v2 中的 master_key_identifier

如果文件尚未加密,则 FS_IOC_SET_ENCRYPTION_POLICY 验证该文件是否为空目录。如果是,则将指定的加密策略分配给目录,将其转换为加密目录。之后,在提供添加密钥中描述的相应主密钥之后,在该目录中创建的所有常规文件、目录(递归)和符号链接都将被加密,并继承相同的加密策略。目录条目中的文件名也将被加密。

或者,如果文件已经被加密,则 FS_IOC_SET_ENCRYPTION_POLICY 验证指定的加密策略是否与实际的加密策略完全匹配。如果它们匹配,则 ioctl 返回 0。否则,它将失败并显示 EEXIST。这适用于常规文件和目录,包括非空目录。

当 v2 加密策略被分配给目录时,还需要当前用户已添加指定的密钥,或者调用者在初始用户命名空间中具有 CAP_FOWNER。(这是为了防止用户使用其他用户的密钥加密他们的数据。)密钥必须在 FS_IOC_SET_ENCRYPTION_POLICY 执行期间保持添加状态。但是,如果新的加密目录不需要立即访问,则可以在之后立即删除密钥。

请注意,即使根目录为空,ext4 文件系统也不允许加密根目录。想要使用一个密钥加密整个文件系统的用户应该考虑使用 dm-crypt。

FS_IOC_SET_ENCRYPTION_POLICY 可能会因以下错误而失败

  • EACCES: 该文件不属于进程的 uid,并且该进程在具有文件所有者的 uid 映射的命名空间中也没有 CAP_FOWNER 能力

  • EEXIST: 该文件已经使用与指定的加密策略不同的加密策略进行了加密

  • EINVAL: 指定了无效的加密策略(无效的版本、模式或标志;或者设置了保留位);或者指定了 v1 加密策略,但目录启用了不区分大小写标志(不区分大小写与 v1 策略不兼容)。

  • ENOKEY: 指定了 v2 加密策略,但是具有指定的 master_key_identifier 的密钥尚未添加,并且该进程在初始用户命名空间中也没有 CAP_FOWNER 能力

  • ENOTDIR: 该文件未加密,并且是常规文件,而不是目录

  • ENOTEMPTY: 该文件未加密,并且是非空目录

  • ENOTTY: 此类型的文件系统未实现加密

  • EOPNOTSUPP: 内核未配置对文件系统的加密支持,或者文件系统超级块未启用加密。(例如,要在 ext4 文件系统上使用加密,必须在内核配置中启用 CONFIG_FS_ENCRYPTION,并且必须使用 tune2fs -O encryptmkfs.ext4 -O encrypt 在超级块上启用“encrypt”功能标志。)

  • EPERM: 此目录可能无法加密,例如,因为它是一个 ext4 文件系统的根目录

  • EROFS: 文件系统是只读的

获取加密策略

有两个 ioctl 可用于获取文件的加密策略

ioctl 的扩展 (_EX) 版本更通用,建议尽可能使用。但是,在较旧的内核上,只有原始 ioctl 可用。应用程序应尝试扩展版本,如果它失败并显示 ENOTTY,则回退到原始版本。

FS_IOC_GET_ENCRYPTION_POLICY_EX

FS_IOC_GET_ENCRYPTION_POLICY_EX ioctl 检索目录或常规文件的加密策略(如果有)。除了打开文件的能力之外,不需要额外的权限。它接受一个指向 struct fscrypt_get_policy_ex_arg 的指针,定义如下

struct fscrypt_get_policy_ex_arg {
        __u64 policy_size; /* input/output */
        union {
                __u8 version;
                struct fscrypt_policy_v1 v1;
                struct fscrypt_policy_v2 v2;
        } policy; /* output */
};

调用者必须将 policy_size 初始化为策略结构可用的空间大小,即 sizeof(arg.policy)

成功后,策略结构将在 policy 中返回,其实际大小将在 policy_size 中返回。应检查 policy.version 以确定返回的策略版本。请注意,“v1”策略的版本代码实际上是 0 (FSCRYPT_POLICY_V1)。

FS_IOC_GET_ENCRYPTION_POLICY_EX 可能会因以下错误而失败

  • EINVAL: 文件已加密,但它使用无法识别的加密策略版本

  • ENODATA: 文件未加密

  • ENOTTY: 此类型的文件系统未实现加密,或者此内核太旧而无法支持 FS_IOC_GET_ENCRYPTION_POLICY_EX(请尝试 FS_IOC_GET_ENCRYPTION_POLICY)

  • EOPNOTSUPP: 内核未配置对此文件系统的加密支持,或者文件系统超级块未启用加密

  • EOVERFLOW: 文件已加密并使用可识别的加密策略版本,但策略结构不适合提供的缓冲区

注意:如果您只需要知道文件是否已加密,则在大多数文件系统上,也可以使用 FS_IOC_GETFLAGS ioctl 并检查 FS_ENCRYPT_FL,或者使用 statx() 系统调用并在 stx_attributes 中检查 STATX_ATTR_ENCRYPTED。

FS_IOC_GET_ENCRYPTION_POLICY

FS_IOC_GET_ENCRYPTION_POLICY ioctl 也可以检索目录或常规文件的加密策略(如果有)。但是,与FS_IOC_GET_ENCRYPTION_POLICY_EX不同,FS_IOC_GET_ENCRYPTION_POLICY 仅支持原始策略版本。它直接接受指向 struct fscrypt_policy_v1 的指针,而不是 struct fscrypt_get_policy_ex_arg。

FS_IOC_GET_ENCRYPTION_POLICY 的错误代码与 FS_IOC_GET_ENCRYPTION_POLICY_EX 的错误代码相同,但如果文件使用较新的加密策略版本加密,则 FS_IOC_GET_ENCRYPTION_POLICY 也会返回 EINVAL

获取每个文件系统的 salt

某些文件系统(例如 ext4 和 F2FS)也支持已弃用的 ioctl FS_IOC_GET_ENCRYPTION_PWSALT。此 ioctl 检索存储在文件系统超级块中的随机生成的 16 字节值。此值旨在用作从密码或其他低熵用户凭据派生加密密钥时的 salt。

FS_IOC_GET_ENCRYPTION_PWSALT 已弃用。相反,更喜欢在用户空间中生成和管理任何所需的 salt。

获取文件的加密 nonce

自 Linux v5.7 起,支持 ioctl FS_IOC_GET_ENCRYPTION_NONCE。在加密的文件和目录上,它获取 inode 的 16 字节 nonce。在未加密的文件和目录上,它会失败并显示 ENODATA。

此 ioctl 对于验证加密是否正确完成的自动化测试很有用。fscrypt 的正常使用不需要它。

添加密钥

FS_IOC_ADD_ENCRYPTION_KEY

FS_IOC_ADD_ENCRYPTION_KEY ioctl 向文件系统添加主加密密钥,使文件系统上所有使用该密钥加密的文件都显示为“已解锁”,即以纯文本形式显示。它可以在目标文件系统上的任何文件或目录上执行,但建议使用文件系统的根目录。它接受一个指向 struct fscrypt_add_key_arg 的指针,定义如下

struct fscrypt_add_key_arg {
        struct fscrypt_key_specifier key_spec;
        __u32 raw_size;
        __u32 key_id;
#define FSCRYPT_ADD_KEY_FLAG_HW_WRAPPED 0x00000001
        __u32 flags;
        __u32 __reserved[7];
        __u8 raw[];
};

#define FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR        1
#define FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER        2

struct fscrypt_key_specifier {
        __u32 type;     /* one of FSCRYPT_KEY_SPEC_TYPE_* */
        __u32 __reserved;
        union {
                __u8 __reserved[32]; /* reserve some extra space */
                __u8 descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE];
                __u8 identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
        } u;
};

struct fscrypt_provisioning_key_payload {
        __u32 type;
        __u32 flags;
        __u8 raw[];
};

struct fscrypt_add_key_arg 必须归零,然后按如下方式初始化

  • 如果要添加密钥以供 v1 加密策略使用,则 key_spec.type 必须包含 FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR,并且 key_spec.u.descriptor 必须包含要添加的密钥的描述符,对应于 struct fscrypt_policy_v1 的 master_key_descriptor 字段中的值。要添加此类型的密钥,调用进程必须在初始用户命名空间中具有 CAP_SYS_ADMIN 能力。

    或者,如果要添加密钥以供 v2 加密策略使用,则 key_spec.type 必须包含 FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER,并且 key_spec.u.identifier 是一个输出字段,内核使用密钥的加密哈希填充它。要添加此类型的密钥,调用进程不需要任何权限。但是,可以添加的密钥数量受到用户密钥环服务配额的限制(请参阅 Documentation/security/keys/core.rst)。

  • raw_size 必须是以字节为单位的 raw 密钥的大小。或者,如果 key_id 非零,则此字段必须为 0,因为在这种情况下,大小由指定的 Linux 密钥环密钥暗示。

  • 如果密钥直接在 raw 字段中给出,则 key_id 为 0。否则,key_id 是类型为“fscrypt-provisioning”的 Linux 密钥环密钥的 ID,其有效负载是 struct fscrypt_provisioning_key_payload,其 raw 字段包含密钥,其 type 字段与 key_spec.type 匹配,并且其 flags 字段与 flags 匹配。由于 raw 是可变长度的,因此此密钥有效负载的总大小必须是 sizeof(struct fscrypt_provisioning_key_payload) 加上密钥字节数。进程必须对此密钥具有搜索权限。

    大多数用户应将其保留为 0 并直接指定密钥。指定 Linux 密钥环密钥的支持主要旨在允许在卸载和重新挂载文件系统后重新添加密钥,而无需将密钥存储在用户空间内存中。

  • flags 包含来自 <linux/fscrypt.h> 的可选标志

    • FSCRYPT_ADD_KEY_FLAG_HW_WRAPPED:这表示密钥是硬件包装的密钥。请参阅硬件包装的密钥。如果使用了 FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR,则不能使用此标志。

  • raw 是一个可变长度的字段,必须包含实际密钥,长度为 raw_size 字节。或者,如果 key_id 非零,则此字段未使用。请注意,尽管命名为 raw,但如果指定了 FSCRYPT_ADD_KEY_FLAG_HW_WRAPPED,则它将包含包装的密钥,而不是原始密钥。

对于 v2 策略密钥,内核会跟踪哪个用户(由有效用户 ID 标识)添加了密钥,并且仅允许该用户删除密钥 --- 或者“root”,如果他们使用FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS

但是,如果另一个用户添加了密钥,则可能需要防止该其他用户意外删除密钥。因此,即使密钥已经由其他用户添加,FS_IOC_ADD_ENCRYPTION_KEY 也可以用于再次添加 v2 策略密钥。在这种情况下,FS_IOC_ADD_ENCRYPTION_KEY 将仅为当前用户安装对密钥的声明,而不是再次实际添加密钥(但密钥仍然必须作为知识证明提供)。

如果密钥或对密钥的声明被添加或已经存在,则 FS_IOC_ADD_ENCRYPTION_KEY 返回 0。

FS_IOC_ADD_ENCRYPTION_KEY 可能会因以下错误而失败

  • EACCES: 指定了 FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR,但调用者在初始用户命名空间中没有 CAP_SYS_ADMIN 能力;或者密钥由 Linux 密钥 ID 指定,但进程缺少密钥的搜索权限。

  • EBADMSG: 无效的硬件包装密钥

  • EDQUOT: 添加密钥将超出此用户的密钥配额

  • EINVAL: 无效的密钥大小或密钥说明符类型,或者设置了保留位

  • EKEYREJECTED: 密钥由 Linux 密钥 ID 指定,但密钥类型错误

  • ENOKEY: 密钥由 Linux 密钥 ID 指定,但不存在具有该 ID 的密钥

  • ENOTTY: 此类型的文件系统未实现加密

  • EOPNOTSUPP: 内核未配置对此文件系统的加密支持,或者文件系统超级块未启用加密;或者指定了硬件包装密钥,但文件系统不支持内联加密或硬件不支持硬件包装密钥

旧方法

对于 v1 加密策略,也可以通过将主加密密钥添加到进程订阅的密钥环(例如,会话密钥环,或者如果用户密钥环链接到会话密钥环,则添加到用户密钥环)来提供主加密密钥。

由于多种原因,此方法已被弃用(并且不支持 v2 加密策略)。首先,它不能与 FS_IOC_REMOVE_ENCRYPTION_KEY 结合使用(请参阅删除密钥),因此对于删除密钥,必须使用诸如 keyctl_unlink() 与 sync; echo 2 > /proc/sys/vm/drop_caches 结合使用的解决方法。其次,它与加密文件的锁定/未锁定状态(即它们显示为纯文本形式还是密文形式)是全局的事实不符。这种不匹配导致了许多混乱以及当以不同 UID 运行的进程(例如 sudo 命令)需要访问加密文件时出现的实际问题。

但是,要将密钥添加到进程订阅的密钥环之一,可以使用 add_key() 系统调用(请参阅:Documentation/security/keys/core.rst)。密钥类型必须为“logon”;此类型的密钥保存在内核内存中,用户空间无法读取。密钥描述必须为“fscrypt:”,后跟加密策略中设置的 master_key_descriptor 的 16 个字符的小写十六进制表示形式。密钥有效负载必须符合以下结构

#define FSCRYPT_MAX_KEY_SIZE            64

struct fscrypt_key {
        __u32 mode;
        __u8 raw[FSCRYPT_MAX_KEY_SIZE];
        __u32 size;
};

mode 被忽略;只需将其设置为 0。实际密钥在 raw 中提供,size 指示其大小(以字节为单位)。也就是说,字节 raw[0..size-1](包括边界值)是实际密钥。

密钥描述前缀“fscrypt:”也可以替换为文件系统特定的前缀,例如“ext4:”。但是,文件系统特定的前缀已弃用,不应在新程序中使用。

删除密钥

有两个 ioctl 可用于删除通过FS_IOC_ADD_ENCRYPTION_KEY添加的密钥

这两个 ioctl 仅在非 root 用户添加或删除 v2 策略密钥的情况下有所不同。

这些 ioctl 不适用于通过旧的进程订阅密钥环机制添加的密钥。

在使用这些 ioctl 之前,请阅读在线攻击部分,以了解这些 ioctl 的安全目标和限制。

FS_IOC_REMOVE_ENCRYPTION_KEY

FS_IOC_REMOVE_ENCRYPTION_KEY ioctl 从文件系统中删除对主加密密钥的声明,并可能删除密钥本身。它可以在目标文件系统上的任何文件或目录上执行,但建议使用文件系统的根目录。它接受一个指向 struct fscrypt_remove_key_arg 的指针,定义如下

struct fscrypt_remove_key_arg {
        struct fscrypt_key_specifier key_spec;
#define FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY      0x00000001
#define FSCRYPT_KEY_REMOVAL_STATUS_FLAG_OTHER_USERS     0x00000002
        __u32 removal_status_flags;     /* output */
        __u32 __reserved[5];
};

此结构必须归零,然后按如下方式初始化

  • 要删除的密钥由 key_spec 指定

    • 要删除 v1 加密策略使用的密钥,请将 key_spec.type 设置为 FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR 并填写 key_spec.u.descriptor。要删除此类型的密钥,调用进程必须在初始用户命名空间中具有 CAP_SYS_ADMIN 能力。

    • 要删除 v2 加密策略使用的密钥,请将 key_spec.type 设置为 FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER 并填写 key_spec.u.identifier

对于 v2 策略密钥,非 root 用户可以使用此 ioctl。但是,为了使这成为可能,它实际上只是删除了当前用户对密钥的声明,从而撤消了对 FS_IOC_ADD_ENCRYPTION_KEY 的单个调用。只有在删除所有声明后,密钥才会被真正删除。

例如,如果使用 uid 1000 调用了 FS_IOC_ADD_ENCRYPTION_KEY,则密钥将被 uid 1000“声明”,并且 FS_IOC_REMOVE_ENCRYPTION_KEY 只能以 uid 1000 的身份成功。或者,如果 uid 1000 和 2000 都添加了密钥,则对于每个 uid,FS_IOC_REMOVE_ENCRYPTION_KEY 将仅删除他们自己的声明。只有在两者都被删除后,密钥才会被真正删除。(可以将其视为取消链接可能具有硬链接的文件。)

如果 FS_IOC_REMOVE_ENCRYPTION_KEY 真正删除了密钥,它还会尝试“锁定”所有已使用密钥解锁的文件。它不会锁定仍在使用的文件,因此预计此 ioctl 将与用户空间合作使用,以确保没有文件仍处于打开状态。但是,如有必要,可以稍后再次执行此 ioctl 以重试锁定任何剩余文件。

如果密钥被删除(但可能仍有文件需要锁定)、用户对密钥的声明被删除或密钥已被删除但仍有文件需要锁定,因此 ioctl 重试锁定它们,则 FS_IOC_REMOVE_ENCRYPTION_KEY 返回 0。在任何这些情况下,removal_status_flags 都将填充以下信息状态标志

  • FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY:如果某些文件仍在运行,则设置。不能保证在仅删除用户对密钥的声明的情况下设置。

  • FSCRYPT_KEY_REMOVAL_STATUS_FLAG_OTHER_USERS:如果仅删除用户对密钥的声明,而不是密钥本身,则设置

FS_IOC_REMOVE_ENCRYPTION_KEY 可能会因以下错误而失败

  • EACCES: 指定了 FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR 密钥说明符类型,但调用者在初始用户命名空间中没有 CAP_SYS_ADMIN 能力

  • EINVAL: 无效的密钥说明符类型,或者设置了保留位

  • ENOKEY: 根本找不到密钥对象,即它从未被添加过,或者已经被完全删除,包括所有锁定的文件;或者,用户没有对密钥的声明(但其他人有)。

  • ENOTTY: 此类型的文件系统未实现加密

  • EOPNOTSUPP: 内核未配置对此文件系统的加密支持,或者文件系统超级块未启用加密

FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS

FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS 与FS_IOC_REMOVE_ENCRYPTION_KEY完全相同,只是对于 v2 策略密钥,ioctl 的 ALL_USERS 版本将删除所有用户对密钥的声明,而不仅仅是当前用户的声明。也就是说,密钥本身将始终被删除,无论有多少用户添加了它。仅当非 root 用户添加和删除密钥时,此差异才有意义。

因此,FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS 也需要“root”,即初始用户命名空间中的 CAP_SYS_ADMIN 能力。否则,它将失败并显示 EACCES。

获取密钥状态

FS_IOC_GET_ENCRYPTION_KEY_STATUS

FS_IOC_GET_ENCRYPTION_KEY_STATUS ioctl 检索主加密密钥的状态。它可以在目标文件系统上的任何文件或目录上执行,但建议使用文件系统的根目录。它接受一个指向 struct fscrypt_get_key_status_arg 的指针,定义如下

struct fscrypt_get_key_status_arg {
        /* input */
        struct fscrypt_key_specifier key_spec;
        __u32 __reserved[6];

        /* output */
#define FSCRYPT_KEY_STATUS_ABSENT               1
#define FSCRYPT_KEY_STATUS_PRESENT              2
#define FSCRYPT_KEY_STATUS_INCOMPLETELY_REMOVED 3
        __u32 status;
#define FSCRYPT_KEY_STATUS_FLAG_ADDED_BY_SELF   0x00000001
        __u32 status_flags;
        __u32 user_count;
        __u32 __out_reserved[13];
};

调用者必须将所有输入字段归零,然后填写 key_spec

  • 要获取 v1 加密策略的密钥状态,请将 key_spec.type 设置为 FSCRYPT_KEY_SPEC_TYPE_DESCRIPTOR 并填写 key_spec.u.descriptor

  • 要获取 v2 加密策略的密钥状态,请将 key_spec.type 设置为 FSCRYPT_KEY_SPEC_TYPE_IDENTIFIER 并填写 key_spec.u.identifier

成功后,返回 0,并且内核将填写输出字段

  • status 指示密钥是不存在、存在还是未完全删除。未完全删除意味着已启动删除,但某些文件仍在被使用;即,FS_IOC_REMOVE_ENCRYPTION_KEY 返回 0 但设置了信息状态标志 FSCRYPT_KEY_REMOVAL_STATUS_FLAG_FILES_BUSY。

  • status_flags 可以包含以下标志

    • FSCRYPT_KEY_STATUS_FLAG_ADDED_BY_SELF 指示密钥已由当前用户添加。这仅针对由 identifier 而不是 descriptor 标识的密钥设置。

  • user_count 指定添加密钥的用户数。这仅针对由 identifier 而不是 descriptor 标识的密钥设置。

FS_IOC_GET_ENCRYPTION_KEY_STATUS 可能会因以下错误而失败

  • EINVAL: 无效的密钥说明符类型,或者设置了保留位

  • ENOTTY: 此类型的文件系统未实现加密

  • EOPNOTSUPP: 内核未配置对此文件系统的加密支持,或者文件系统超级块未启用加密

在其他用例中,FS_IOC_GET_ENCRYPTION_KEY_STATUS 可用于确定在提示用户输入派生密钥所需的密码之前,是否需要为给定的加密目录添加密钥。

FS_IOC_GET_ENCRYPTION_KEY_STATUS 只能获取文件系统级别密钥环中的密钥状态,即由FS_IOC_ADD_ENCRYPTION_KEYFS_IOC_REMOVE_ENCRYPTION_KEY管理的密钥环。它无法获取仅使用旧机制(涉及进程订阅密钥环)为 v1 加密策略添加的密钥的状态。

访问语义

使用密钥

使用加密密钥,加密的常规文件、目录和符号链接的行为与其未加密的对应物非常相似 --- 毕竟,加密旨在是透明的。但是,精明的用户可能会注意到行为上的一些差异

  • 未加密的文件或使用不同加密策略(即不同的密钥、模式或标志)加密的文件无法重命名或链接到加密目录中;请参阅加密策略执行。尝试这样做将失败并显示 EXDEV。但是,可以在加密目录中或移动到未加密目录中重命名加密文件。

    注意:将未加密文件“移动”到加密目录中(例如,使用 mv 程序)是在用户空间中通过复制然后删除来实现的。请注意,原始未加密数据可能仍然可以从磁盘上的可用空间中恢复;最好从一开始就保持所有文件加密。shred 程序可用于覆盖源文件,但不保证在所有文件系统和存储设备上都有效。

  • 仅在某些情况下支持加密文件的直接 I/O。有关详细信息,请参阅直接 I/O 支持

  • fallocate 操作 FALLOC_FL_COLLAPSE_RANGE 和 FALLOC_FL_INSERT_RANGE 在加密文件上不受支持,并且将失败并显示 EOPNOTSUPP。

  • 不支持加密文件的在线碎片整理。EXT4_IOC_MOVE_EXT 和 F2FS_IOC_MOVE_RANGE ioctl 将失败并显示 EOPNOTSUPP。

  • ext4 文件系统不支持使用加密的常规文件进行数据日志记录。它将回退到有序数据模式。

  • 不支持在加密文件上使用 DAX(直接访问)。

  • 加密符号链接的最大长度比未加密符号链接的最大长度短 2 个字节。例如,在块大小为 4K 的 EXT4 文件系统上,未加密的符号链接最大长度可达 4095 字节,而加密的符号链接的最大长度仅可达 4093 字节(两种长度均不包括终止空字符)。

请注意,支持 mmap。这是可能的,因为加密文件的页面缓存包含纯文本,而不是密文。

没有密钥

即使在文件系统的加密密钥添加之前,或者加密密钥移除之后,某些文件系统操作也可以在加密的常规文件、目录和符号链接上执行。

  • 文件元数据可以被读取,例如使用 stat()。

  • 目录可以被列出,在这种情况下,文件名将以一种从其密文导出的编码形式列出。当前的编码算法在文件名哈希和编码中描述。该算法可能会更改,但保证呈现的文件名不会超过 NAME_MAX 字节,不会包含 /\0 字符,并且将唯一标识目录条目。

    ... 目录条目是特殊的。它们始终存在,并且未加密或编码。

  • 文件可以被删除。也就是说,非目录文件可以像往常一样使用 unlink() 删除,空目录可以像往常一样使用 rmdir() 删除。因此,rmrm -r 将按预期工作。

  • 符号链接目标可以被读取和跟随,但它们将以加密形式呈现,类似于目录中的文件名。因此,它们不太可能指向任何有用的位置。

在没有密钥的情况下,常规文件无法打开或截断。尝试这样做将会失败并返回 ENOKEY。这意味着任何需要文件描述符的常规文件操作,例如 read()、write()、mmap()、fallocate() 和 ioctl(),也是被禁止的。

同样,在没有密钥的情况下,任何类型的文件(包括目录)都无法在加密目录中创建或链接,加密目录中的名称也不能作为重命名的源或目标,也不能在加密目录中创建 O_TMPFILE 临时文件。所有此类操作都将失败并返回 ENOKEY。

目前,在没有加密密钥的情况下,无法备份和恢复加密文件。这将需要尚未实现的特殊 API。

加密策略强制执行

在目录上设置加密策略后,在该目录中创建的所有常规文件、目录和符号链接(递归地)都将继承该加密策略。特殊文件 --- 即命名管道、设备节点和 UNIX 域套接字 --- 不会被加密。

除了这些特殊文件外,不允许在加密目录树中存在未加密的文件,或使用不同加密策略加密的文件。尝试将此类文件链接或重命名到加密目录将会失败并返回 EXDEV。这也在 ->lookup() 期间强制执行,以提供有限的保护,防止离线攻击尝试禁用或降级应用程序稍后可能写入敏感数据的已知位置中的加密。建议实施某种形式的“验证启动”的系统通过验证所有顶级加密策略来利用这一点,然后再进行访问。

内联加密支持

默认情况下,fscrypt 使用内核 crypto API 进行所有加密操作(HKDF 除外,fscrypt 部分实现了它)。内核 crypto API 支持硬件加密加速器,但仅支持以传统方式工作的加速器,其中所有输入和输出(例如,明文和密文)都在内存中。 fscrypt 可以利用这种硬件,但传统的加速模型效率不高,并且 fscrypt 尚未针对它进行优化。

相反,许多较新的系统(尤其是移动 SoC)具有内联加密硬件,可以在数据传输到/从存储设备的过程中加密/解密数据。 Linux 通过一组对块层的扩展(称为 blk-crypto)支持内联加密。 blk-crypto 允许文件系统将加密上下文附加到 bios(I/O 请求),以指定数据将如何在线加密或解密。有关 blk-crypto 的更多信息,请参见 Documentation/block/inline-encryption.rst

在受支持的文件系统(目前为 ext4 和 f2fs)上,fscrypt 可以使用 blk-crypto 而不是内核 crypto API 来加密/解密文件内容。要启用此功能,请在内核配置中设置 CONFIG_FS_ENCRYPTION_INLINE_CRYPT=y,并在挂载文件系统时指定“inlinecrypt”挂载选项。

请注意,“inlinecrypt”挂载选项仅指定在可能的情况下使用内联加密;它不会强制使用。对于内联加密硬件不具备所需加密功能(例如,对所需加密算法和数据单元大小的支持)并且 blk-crypto-fallback 不可用的文件,fscrypt 仍然会回退到使用内核 crypto API。(要使 blk-crypto-fallback 可用,必须在内核配置中使用 CONFIG_BLK_INLINE_ENCRYPTION_FALLBACK=y 启用它,并且该文件必须受到原始密钥而不是硬件封装密钥的保护。)

目前,fscrypt 始终使用文件系统块大小(通常为 4096 字节)作为数据单元大小。因此,它只能使用支持该数据单元大小的内联加密硬件。

内联加密不会影响磁盘格式的密文或其他方面,因此用户可以自由地在使用“inlinecrypt”和不使用“inlinecrypt”之间来回切换。一个例外是,受硬件封装密钥保护的文件只能由内联加密硬件加密/解密,因此只能在使用“inlinecrypt”挂载选项时访问。有关硬件封装密钥的更多信息,请参见下文。

硬件封装密钥

当内联加密硬件支持时,fscrypt 支持使用硬件封装密钥。此类密钥仅以封装(加密)形式存在于内核内存中;它们只能由内联加密硬件解封装(解密),并且在时间上绑定到当前启动。这可以防止密钥在内核内存泄漏时遭到泄露。这是在不限制可以使用的密钥数量的情况下完成的,同时仍然允许执行绑定到同一密钥但无法使用内联加密硬件的加密任务,例如文件名加密。

请注意,硬件封装密钥并非 fscrypt 特有;它们是块层功能(blk-crypto 的一部分)。有关硬件封装密钥的更多详细信息,请参见块层文档 Documentation/block/inline-encryption.rst。本节的其余部分仅关注 fscrypt 如何使用硬件封装密钥的详细信息。

fscrypt 支持硬件封装密钥,允许 fscrypt 主密钥作为硬件封装密钥,以替代原始密钥。要使用 FS_IOC_ADD_ENCRYPTION_KEY 添加硬件封装密钥,用户空间必须在 struct fscrypt_add_key_arg 的 flags 字段中以及适用的 struct fscrypt_provisioning_key_payload 的 flags 字段中指定 FSCRYPT_ADD_KEY_FLAG_HW_WRAPPED。密钥必须采用临时封装形式,而不是长期封装形式。

一些限制适用。首先,受硬件封装密钥保护的文件绑定到系统的内联加密硬件。因此,它们只能在使用“inlinecrypt”挂载选项时访问,并且不能包含在可移植文件系统映像中。其次,目前硬件封装密钥支持仅与 IV_INO_LBLK_64 策略IV_INO_LBLK_32 策略兼容,因为它假定每个 fscrypt 主密钥只有一个文件内容加密密钥,而不是每个文件一个。未来的工作可能会通过将每个文件的随机数传递到存储堆栈来解决此限制,以允许硬件导出每个文件的密钥。

在实现方面,为了加密/解密受硬件封装密钥保护的文件的内容,fscrypt 使用 blk-crypto,将硬件封装密钥附加到 bio 加密上下文。与原始密钥一样,当块层密钥不在密钥槽中时,块层会将密钥编程到密钥槽中。但是,在编程硬件封装密钥时,硬件不会将给定的密钥直接编程到密钥槽中,而是会解封装它(使用硬件的临时封装密钥)并从中导出内联加密密钥。内联加密密钥是实际被编程到密钥槽中的密钥,并且永远不会暴露给软件。

但是,fscrypt 不仅仅进行文件内容加密;它还使用其主密钥来导出文件名加密密钥、密钥标识符,有时还会导出一些更模糊的子密钥类型,例如 dirhash 密钥。因此,即使没有文件内容加密,fscrypt 仍然需要一个原始密钥才能工作。为了从硬件封装密钥中获取这样的密钥,fscrypt 要求内联加密硬件从硬件封装密钥中导出一个加密隔离的“软件密钥”。 fscrypt 使用此“软件密钥”来为 KDF 设置密钥,以导出除文件内容密钥之外的所有子密钥。

请注意,这意味着硬件封装密钥功能仅保护文件内容加密密钥。它不保护其他 fscrypt 子密钥,例如文件名加密密钥。

直接 I/O 支持

要使加密文件的直接 I/O 工作,必须满足以下条件(除了未加密文件的直接 I/O 的条件):

  • 该文件必须使用内联加密。通常,这意味着文件系统必须使用 -o inlinecrypt 挂载,并且必须存在内联加密硬件。但是,软件回退也可用。有关详细信息,请参见内联加密支持

  • I/O 请求必须完全对齐到文件系统块大小。这意味着 I/O 目标的文件位置、所有 I/O 段的长度以及所有 I/O 缓冲区的内存地址必须是该值的倍数。请注意,文件系统块大小可能大于块设备的逻辑块大小。

如果上述任一条件未满足,则加密文件的直接 I/O 将回退到缓冲 I/O。

实现细节

加密上下文

加密策略在磁盘上由 struct fscrypt_context_v1 或 struct fscrypt_context_v2 表示。由各个文件系统决定将其存储在何处,但通常将其存储在隐藏的扩展属性中。由于加密 xattr 的特殊语义,不应通过 xattr 相关的系统调用(例如 getxattr() 和 setxattr())公开它。(特别是,如果将加密策略添加到除空目录之外的任何内容或从中删除,将会造成很大的混乱。)这些结构定义如下:

#define FSCRYPT_FILE_NONCE_SIZE 16

#define FSCRYPT_KEY_DESCRIPTOR_SIZE  8
struct fscrypt_context_v1 {
        u8 version;
        u8 contents_encryption_mode;
        u8 filenames_encryption_mode;
        u8 flags;
        u8 master_key_descriptor[FSCRYPT_KEY_DESCRIPTOR_SIZE];
        u8 nonce[FSCRYPT_FILE_NONCE_SIZE];
};

#define FSCRYPT_KEY_IDENTIFIER_SIZE  16
struct fscrypt_context_v2 {
        u8 version;
        u8 contents_encryption_mode;
        u8 filenames_encryption_mode;
        u8 flags;
        u8 log2_data_unit_size;
        u8 __reserved[3];
        u8 master_key_identifier[FSCRYPT_KEY_IDENTIFIER_SIZE];
        u8 nonce[FSCRYPT_FILE_NONCE_SIZE];
};

上下文结构包含与相应策略结构相同的信息(请参见设置加密策略),不同之处在于上下文结构还包含一个随机数。随机数由内核随机生成,并用作 KDF 输入或作为调整,以使不同的文件以不同的方式加密;请参见每个文件的加密密钥DIRECT_KEY 策略

数据路径更改

使用内联加密时,文件系统只需将加密上下文与 bios 相关联,以指定块层或内联加密硬件如何加密/解密文件内容。

不使用内联加密时,文件系统必须自行加密/解密文件内容,如下所述:

对于常规文件的读取路径 (->read_folio()),文件系统可以将密文读取到页面缓存中,并就地解密。必须保持 folio 锁,直到解密完成,以防止 folio 过早地对用户空间可见。

对于常规文件的写入路径 (->writepages()),文件系统不能就地加密页面缓存中的数据,因为必须保留缓存的明文。相反,文件系统必须加密到临时缓冲区或“bounce page”中,然后写出临时缓冲区。一些文件系统(例如 UBIFS)已经使用临时缓冲区,而不管加密如何。其他文件系统(例如 ext4 和 F2FS)必须专门为加密分配 bounce page。

文件名哈希和编码

现代文件系统通过使用索引目录来加速目录查找。索引目录组织为由文件名哈希键控的树。当请求 ->lookup() 时,文件系统通常会哈希正在查找的文件名,以便它可以快速找到相应的目录条目(如果有)。

使用加密时,无论有无加密密钥,都必须支持查找并且效率很高。显然,哈希明文文件名是行不通的,因为没有密钥就无法获得明文文件名。(哈希明文文件名也会使文件系统的 fsck 工具无法优化加密目录。)相反,文件系统会哈希密文文件名,即实际存储在目录条目中的磁盘上的字节。当被要求使用密钥执行 ->lookup() 时,文件系统只需加密用户提供的名称即可获得密文。

没有密钥的查找更为复杂。原始密文可能包含 \0/ 字符,这在文件名中是非法的。因此,readdir() 必须对密文进行 base64url 编码以进行呈现。对于大多数文件名,这都可以正常工作;在 ->lookup() 上,文件系统只需对用户提供的名称进行 base64url 解码即可返回原始密文。

但是,对于很长的文件名,base64url 编码会导致文件名长度超过 NAME_MAX。为了防止这种情况,readdir() 实际上以缩写形式呈现长文件名,该缩写形式编码了密文文件名的强“哈希”,以及目录查找所需的可选文件系统特定哈希。这允许文件系统仍然以很高的置信度将 ->lookup() 中给出的文件名映射回先前由 readdir() 列出的特定目录条目。有关更多详细信息,请参见源代码中的 struct fscrypt_nokey_name。

请注意,在没有密钥的情况下向用户空间呈现文件名的确切方式可能会在将来更改。这只是作为一种临时呈现有效文件名的方式,以便像 rm -r 这样的命令可以按预期在加密目录上工作。

测试

要测试 fscrypt,请使用 xfstests,它是 Linux 的事实上的标准文件系统测试套件。首先,在相关文件系统上运行“encrypt”组中的所有测试。也可以使用“inlinecrypt”挂载选项运行测试,以测试内联加密支持的实现。例如,要使用 kvm-xfstests 测试 ext4 和 f2fs 加密

kvm-xfstests -c ext4,f2fs -g encrypt
kvm-xfstests -c ext4,f2fs -g encrypt -m inlinecrypt

也可以通过这种方式测试 UBIFS 加密,但应在单独的命令中完成,并且 kvm-xfstests 需要一些时间来设置模拟的 UBI 卷

kvm-xfstests -c ubifs -g encrypt

不应有任何测试失败。但是,如果所需的算法未构建到内核的 crypto API 中,则将跳过使用非默认加密模式的测试(例如 generic/549 和 generic/550)。此外,将在 UBIFS 上跳过访问原始块设备的测试(例如 generic/399、generic/548、generic/549、generic/550)。

除了运行“encrypt”组测试之外,对于 ext4 和 f2fs,还可以使用“test_dummy_encryption”挂载选项运行大多数 xfstests。此选项会导致自动使用虚拟密钥加密所有新文件,而无需进行任何 API 调用。这更彻底地测试了加密的 I/O 路径。要使用 kvm-xfstests 执行此操作,请使用“encrypt”文件系统配置

kvm-xfstests -c ext4/encrypt,f2fs/encrypt -g auto
kvm-xfstests -c ext4/encrypt,f2fs/encrypt -g auto -m inlinecrypt

因为这运行的测试比“-g encrypt”多得多,所以运行时间要长得多;因此,还请考虑使用 gce-xfstests 而不是 kvm-xfstests

gce-xfstests -c ext4/encrypt,f2fs/encrypt -g auto
gce-xfstests -c ext4/encrypt,f2fs/encrypt -g auto -m inlinecrypt