文件系统级加密 (fscrypt)¶
简介¶
fscrypt 是一个库,文件系统可以挂钩该库以支持对文件和目录的透明加密。
注意:本文档中的“fscrypt”指的是内核级别的部分,在 fs/crypto/
中实现,而不是用户空间工具 fscrypt。本文档仅涵盖内核级别的部分。有关如何使用加密的命令行示例,请参阅用户空间工具 fscrypt 的文档。此外,建议使用 fscrypt 用户空间工具或其他现有的用户空间工具,如 fscryptctl 或 Android 的密钥管理系统,而不是直接使用内核的 API。使用现有工具可以减少引入您自己的安全漏洞的机会。(尽管如此,为了完整起见,本文档还是涵盖了内核的 API。)
与 dm-crypt 不同,fscrypt 在文件系统级别而不是在块设备级别运行。这允许它使用不同的密钥加密不同的文件,并在同一文件系统上拥有未加密的文件。这对于多用户系统很有用,在多用户系统中,每个用户的静态数据需要与其他用户进行加密隔离。但是,除了文件名之外,fscrypt 不加密文件系统元数据。
与堆叠文件系统 eCryptfs 不同,fscrypt 直接集成到受支持的文件系统中——目前是 ext4、F2FS、UBIFS 和 CephFS。这允许读取和写入加密文件,而无需在页面缓存中缓存解密和加密的页面,从而使使用的内存几乎减少一半,并与未加密的文件保持一致。同样,只需要一半的目录项和索引节点。eCryptfs 还将加密文件名限制为 143 字节,导致应用程序兼容性问题;fscrypt 允许完整的 255 字节 (NAME_MAX)。最后,与 eCryptfs 不同,非特权用户可以使用 fscrypt API,而无需挂载任何内容。
fscrypt 不支持就地加密文件。相反,它支持将一个空目录标记为加密。然后,在用户空间提供密钥后,在该目录树中创建的所有常规文件、目录和符号链接都会被透明地加密。
威胁模型¶
离线攻击¶
如果用户空间选择强加密密钥,则在块设备内容发生单点时间永久离线泄露的情况下,fscrypt 可保护文件内容和文件名的机密性。fscrypt 不保护非文件名元数据的机密性,例如文件大小、文件权限、文件时间戳和扩展属性。此外,文件中空洞(逻辑上包含所有零的未分配块)的存在和位置不受保护。
如果攻击者能够在授权用户稍后访问文件系统之前离线操作文件系统,则 fscrypt 不能保证保护机密性或真实性。
在线攻击¶
fscrypt(以及一般的存储加密)只能对在线攻击提供有限的保护,如果有的话。详细来说
侧信道攻击¶
fscrypt 仅在底层 Linux 加密 API 算法或内联加密硬件的范围内抵抗侧信道攻击,例如时序或电磁攻击。如果使用了易受攻击的算法,例如基于表的 AES 实现,则攻击者可能会对在线系统发起侧信道攻击。侧信道攻击也可能针对使用解密数据的应用程序发起。
内核内存泄露¶
如果攻击者通过发起物理攻击或利用内核安全漏洞,泄露系统到足以从任意内存读取,则可以泄露当前正在使用的所有加密密钥。
但是,fscrypt 允许从内核中删除加密密钥,这可能会保护它们免受以后的泄露。
更详细地说,FS_IOC_REMOVE_ENCRYPTION_KEY ioctl(或 FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS ioctl)可以从内核内存中擦除主加密密钥。如果它这样做,它还会尝试逐出所有使用该密钥“解锁”的缓存索引节点,从而擦除它们的每个文件密钥,并使它们再次显示为“锁定”,即以密文或加密形式显示。
但是,这些 ioctl 有一些限制
对于正在使用的文件,每个文件的密钥都不会被删除或擦除。因此,为了获得最大效果,用户空间在删除主密钥之前应关闭相关的加密文件和目录,并终止任何工作目录位于受影响的加密目录中的进程。
内核无法神奇地擦除用户空间也可能拥有的主密钥副本。因此,用户空间还必须擦除它制作的所有主密钥副本;通常,这应在 FS_IOC_ADD_ENCRYPTION_KEY 后立即完成,而无需等待 FS_IOC_REMOVE_ENCRYPTION_KEY。自然,同样的道理也适用于密钥层次结构中的所有更高级别。用户空间还应遵循其他安全预防措施,例如 mlock() 内存中包含的密钥以防止其被换出。
一般来说,内核 VFS 缓存中解密的内容和文件名会被释放,但不会被擦除。因此,即使在擦除相应的密钥后,也可能会从释放的内存中恢复其中的一部分。为了部分解决这个问题,您可以在内核配置中设置 CONFIG_PAGE_POISONING=y,并将 page_poison=1 添加到内核命令行中。但是,这会带来性能成本。
密钥可能仍然存在于 CPU 寄存器、加密加速器硬件(如果加密 API 使用它来实现任何算法)或此处未明确考虑的其他位置。
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,这不太可能适合文件系统的 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 密钥从主密钥派生)进行哈希处理,并添加到文件数据单元索引模 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 加速是诸如 CAAM 或 CESA 之类的非 CPU 加密加速器,这些加速器不支持 XTS 的系统。
其余的模式对是“民族自豪感密码”
(SM4-XTS, SM4-CBC-CTS)
一般来说,这些密码本身并不“差”,但与 AES 和 ChaCha 等常用选择相比,它们接受的安全审查有限。它们也没有带来太多新的东西。建议仅在强制要求使用这些密码时才使用它们。
内核配置选项¶
启用 fscrypt 支持 (CONFIG_FS_ENCRYPTION) 会自动从加密 API 中提取使用 AES-256-XTS 和 AES-256-CBC-CTS 加密所需的基本支持。为了获得最佳性能,强烈建议您还启用任何可用的特定于平台的 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_CHACHA20_NEON
arm32: CONFIG_CRYPTO_NHPOLY1305_NEON
arm64: CONFIG_CRYPTO_CHACHA20_NEON
arm64: CONFIG_CRYPTO_NHPOLY1305_NEON
x86: CONFIG_CRYPTO_CHACHA20_X86_64
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。
但是,每个加密的目录仍然使用唯一的密钥,或者可选地将文件的 nonce(对于 DIRECT_KEY 策略)或 inode 号(对于 IV_INO_LBLK_64 策略)包含在 IV 中。因此,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_mode
和filenames_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(包括 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 加密策略,但该目录启用了 casefold 标志(大小写折叠与 v1 策略不兼容)。ENOKEY
:指定了 v2 加密策略,但尚未添加具有指定master_key_identifier
的密钥,或者进程在初始用户命名空间中没有 CAP_FOWNER 功能ENOTDIR
:该文件未加密,并且是一个普通文件,而不是目录ENOTEMPTY
:该文件未加密,并且是一个非空目录ENOTTY
:此类型的文件系统未实现加密EOPNOTSUPP
:内核未配置文件系统加密支持,或者文件系统超级块上未启用加密。(例如,要在 ext4 文件系统上使用加密,必须在内核配置中启用 CONFIG_FS_ENCRYPTION,并且必须使用tune2fs -O encrypt
或mkfs.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
。
获取每个文件系统的盐值¶
某些文件系统(如 ext4 和 F2FS)还支持已弃用的 ioctl FS_IOC_GET_ENCRYPTION_PWSALT。此 ioctl 检索存储在文件系统超级块中的随机生成的 16 字节值。此值旨在用作从密码或其他低熵用户凭据派生加密密钥时的盐值。
FS_IOC_GET_ENCRYPTION_PWSALT 已弃用。相反,更倾向于在用户空间中生成和管理任何需要的盐值。
获取文件的加密随机数¶
自 Linux v5.7 起,支持 ioctl FS_IOC_GET_ENCRYPTION_NONCE。在加密的文件和目录上,它会获取 inode 的 16 字节随机数。在未加密的文件和目录上,它会因 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;
__u32 __reserved[8];
__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 __reserved;
__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
匹配。由于raw
是可变长度的,因此此密钥的有效负载的总大小必须是sizeof(struct fscrypt_provisioning_key_payload)
加上原始密钥大小。进程必须具有对此密钥的搜索权限。大多数用户应将此值保留为 0 并直接指定原始密钥。对指定 Linux 密钥环密钥的支持主要旨在允许在卸载并重新挂载文件系统后重新添加密钥,而无需将原始密钥存储在用户空间内存中。
raw
是一个可变长度的字段,必须包含实际密钥,长度为raw_size
字节。或者,如果key_id
非零,则此字段未使用。
对于 v2 策略密钥,内核会跟踪哪个用户(由有效的用户 ID 标识)添加了密钥,并且只允许该用户或“root”删除密钥(如果他们使用 FS_IOC_REMOVE_ENCRYPTION_KEY_ALL_USERS)。
但是,如果另一个用户已添加密钥,可能需要阻止该其他用户意外删除密钥。因此,即使其他用户已经添加了 v2 策略密钥,也可以再次使用 FS_IOC_ADD_ENCRYPTION_KEY 来添加该密钥。在这种情况下,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 指定,但该进程缺乏对密钥的搜索权限。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添加的密钥
只有在非 root 用户添加或删除 v2 策略密钥的情况下,这两个 ioctl 才会有所不同。
这些 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_KEY 和 FS_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() 像往常一样删除空目录。因此,
rm
和rm -r
将按预期工作。可以读取和跟随符号链接的目标,但它们将以加密形式呈现,类似于目录中的文件名。因此,它们不太可能指向任何有用的地方。
在没有密钥的情况下,无法打开或截断常规文件。尝试这样做将失败并返回 ENOKEY。这意味着任何需要文件描述符的常规文件操作,例如 read()、write()、mmap()、fallocate() 和 ioctl(),也都被禁止。
同样在没有密钥的情况下,任何类型的文件(包括目录)都无法创建或链接到加密目录中,加密目录中的名称也不能成为重命名的源或目标,也不能在加密目录中创建 O_TMPFILE 临时文件。所有这些操作都将失败并返回 ENOKEY。
目前,在没有加密密钥的情况下,无法备份和还原加密文件。这将需要特殊的 API,但尚未实现。
加密策略强制执行¶
在目录上设置加密策略后,在该目录(递归地)中创建的所有常规文件、目录和符号链接都将继承该加密策略。特殊文件(即命名管道、设备节点和 UNIX 域套接字)不会被加密。
除了这些特殊文件外,禁止在加密目录树中存在未加密的文件或使用不同加密策略加密的文件。尝试将此类文件链接或重命名到加密目录中将失败并返回 EXDEV。在 ->lookup() 期间也会强制执行此操作,以提供有限的保护,防止离线攻击尝试禁用或降级应用程序可能在以后写入敏感数据的已知位置的加密。建议实施“验证启动”形式的系统在访问之前验证所有顶级加密策略,从而利用此功能。
内联加密支持¶
默认情况下,fscrypt 对所有加密操作都使用内核加密 API(HKDF 除外,fscrypt 部分实现了 HKDF)。内核加密 API 支持硬件加密加速器,但仅限于那些以传统方式工作且所有输入和输出(例如,明文和密文)都在内存中的加速器。fscrypt 可以利用此类硬件,但传统的加速模型效率不高,并且 fscrypt 尚未针对其进行优化。
相反,许多较新的系统(尤其是移动 SoC)都具有内联加密硬件,该硬件可以在数据往返存储设备的过程中对数据进行加密/解密。Linux 通过一组对块层的扩展(称为 blk-crypto)支持内联加密。blk-crypto 允许文件系统将加密上下文附加到 bios(I/O 请求),以指定如何以内联方式加密或解密数据。有关 blk-crypto 的更多信息,请参阅 Documentation/block/inline-encryption.rst。
在支持的文件系统(当前为 ext4 和 f2fs)上,fscrypt 可以使用 blk-crypto 而不是内核加密 API 来加密/解密文件内容。要启用此功能,请在内核配置中设置 CONFIG_FS_ENCRYPTION_INLINE_CRYPT=y,并在挂载文件系统时指定 “inlinecrypt” 挂载选项。
请注意,“inlinecrypt” 挂载选项仅指定在可能的情况下使用内联加密;它不会强制使用。如果内联加密硬件不具备所需的加密功能(例如,支持所需的加密算法和数据单元大小)并且 blk-crypto-fallback 不可用,则 fscrypt 仍然会回退到使用内核加密 API。(要使 blk-crypto-fallback 可用,必须在内核配置中使用 CONFIG_BLK_INLINE_ENCRYPTION_FALLBACK=y 启用它。)
目前,fscrypt 始终使用文件系统块大小(通常为 4096 字节)作为数据单元大小。因此,它只能使用支持该数据单元大小的内联加密硬件。
内联加密不会影响密文或磁盘格式的其他方面,因此用户可以自由地在使用 “inlinecrypt” 和不使用 “inlinecrypt” 之间切换。
直接 I/O 支持¶
要使加密文件上的直接 I/O 工作,必须满足以下条件(除了未加密文件上直接 I/O 的条件):
该文件必须使用内联加密。通常这意味着文件系统必须使用
-o inlinecrypt
挂载,并且必须存在内联加密硬件。但是,软件回退也可用。有关详细信息,请参阅内联加密支持。I/O 请求必须完全对齐到文件系统块大小。这意味着 I/O 目标的文件位置、所有 I/O 段的长度以及所有 I/O 缓冲区的内存地址都必须是此值的倍数。请注意,文件系统块大小可能大于块设备的逻辑块大小。
如果未满足上述任何条件,则加密文件上的直接 I/O 将回退到缓冲 I/O。
实现细节¶
加密上下文¶
加密策略在磁盘上由结构体 fscrypt_context_v1 或结构体 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];
};
上下文结构体包含与相应策略结构体相同的信息(请参阅设置加密策略),但上下文结构体还包含一个 nonce。nonce 由内核随机生成,并用作 KDF 输入或调整,以使不同的文件以不同的方式加密;请参阅每个文件的加密密钥和DIRECT_KEY 策略。
数据路径更改¶
当使用内联加密时,文件系统只需要将加密上下文与 bios 关联,以指定块层或内联加密硬件将如何加密/解密文件内容。
当不使用内联加密时,文件系统必须自行加密/解密文件内容,如下所述:
对于常规文件的读取路径 (->read_folio()),文件系统可以将密文读取到页缓存中并就地解密。必须保持 folio 锁,直到解密完成,以防止 folio 过早地对用户空间可见。
对于常规文件的写入路径 (->writepage()),文件系统无法在页缓存中就地加密数据,因为必须保留缓存的明文。相反,文件系统必须加密到临时缓冲区或“反弹页”中,然后写出临时缓冲区。某些文件系统(例如 UBIFS)已经使用临时缓冲区,而无需加密。其他文件系统(例如 ext4 和 F2FS)必须专门为加密分配反弹页。
文件名哈希和编码¶
现代文件系统通过使用索引目录来加速目录查找。索引目录组织为以文件名哈希为键的树。当请求 ->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
不应该有测试失败。但是,如果所需的算法没有内置到内核的加密 API 中,则会跳过使用非默认加密模式的测试(例如 generic/549 和 generic/550)。此外,访问原始块设备的测试(例如 generic/399、generic/548、generic/549、generic/550)将在 UBIFS 上跳过。
除了运行“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