RFC-0157:Fxfs 加密和多卷支持

RFC-0157:Fxfs 加密和多卷支持
状态已接受
领域
  • 存储
说明

介绍 Fxfs 中的加密和多卷支持。

问题
  • 92275
Gerrit 更改
  • 666884
作者
审核人
提交日期(年-月-日)2022-04-07
审核日期(年-月-日)2022-04-28

总结

此 RFC 说明了 Fxfs 中的加密和多卷支持。

设计初衷

Fuchsia 需要能够支持多个卷,这些卷使用与不同用户密码绑定的不同密钥进行加密。

利益相关方

审核者:abarth@google.com、enoharemaien@google.com、palmer@google.com、jfsulliv@google.com、jsankey@google.com、zarvox@google.com、

咨询人员:Fuchsia 的安全、存储和隐私权团队。

社交:在开始此 RFC 流程之前,该设计已由 Storage 团队和上述审核人员传播和审核。

设计

RFC-0136 中对 Fxfs 进行了介绍。

要求

  1. 应该能够创建、枚举和删除卷。

  2. 每个卷应使用不同的密钥进行加密。

  3. 对象元数据(例如文件名、大小和时间戳)应进行加密。

  4. 必须能够在没有密钥的情况下删除卷。

  5. 应能够支持按键滚动,而无需费力的迁移工作。

  6. 卷的大小应该有限制(尽管此项的设计留待日后使用)。

  7. 应该可以在不访问密钥的情况下查询卷的大小。

  8. 系统应该根据分块数提供防护,以防范指纹攻击。在这种攻击中,只有知道文件集中若干文件的加密大小(向上舍入到最近的分块),才能确定文件系统中是否存在这组文件。

职责范围以外

此设计未涵盖以下领域:

  1. 在设备(包括主机和目标设备)之间向任一方向传输加密映像,虽然能够这样做来帮助调试会很有帮助,但这在正式版设备上是不可能的,而且没有先例。

  2. 加密服务的实现。此设计会在其他地方介绍。

  3. 设计应支持按键滚动,但要在组件之间使用的精确 API 超出了范围。

概览

Fxfs 加密

Fxfs 内置加密功能将支持为每个文件使用单独的密钥。文件通常只有一个加密密钥,但为了支持密钥滚动和文件克隆,格式会支持每个文件使用多个密钥。密钥将由 Fxfs 将与之通信的加密服务进行封装和解封装。

根父级和根存储区中的以下对象将采用某种形式的加密:

  1. 日志(位于根父存储区中)将包含针对子存储区中元数据的变更,这些变更使用流加密技术加密。此加密方式的密钥按存储区设置。

  2. 子存储区的图层文件(位于根存储区中)将被加密。这些文件的加密机制与所有其他文件相同。该密钥将使用基于存储区的密钥进行封装,并与其他未加密的存储区信息一起存储。

根父级和根存储区中的其他对象(即超级块)、与分配器相关的所有对象、支持根对象存储区的图层文件(其中包含根存储区对象的元数据)以及包含子级存储区基本信息的对象。上述数据均不构成用户数据。

这样做的最终结果是:

  • 用户数据将被加密
  • 所有元数据(包括文件名、文件大小、范围信息和目录信息)都将加密。

部分信息不会加密:

  • 卷中的文件数量。
  • 分配给卷的范围集。

对象 ID

在 Fxfs 中,对象 ID 目前以单调递增的方式分配,可用作边信道。为了解决此问题,对象 ID 的最低有效 32 位将使用 ff1 加密(在分配时)进行加密。在循环遍历 32 位的对象 ID 后,密钥将进行轮替。该密钥将以封装形式存储,并与其余未加密的存储信息一起存储。每次滚动密钥时,对象 ID 的高 32 位将单调递增。

卷内使用的对象数和空间还提供边信道(基本上是通过 statvfs 获得的所有信息)。如何解决此问题不在本设计的范围之内(对 Fxfs 来说并不是新问题)。

密钥管理

密钥封装和解封装由一项单独的“crypt”服务负责,该服务会提供类似于以下协议的功能:

/// Designates the purpose of a key.
type KeyPurpose = flexible enum {
    /// The key will be used to encrypt metadata.
    METADATA = 1;
    /// The key will be used to encrypt data.
    DATA = 2;
};

protocol Crypt {
    /// Creates a new wrapped key.  `owner`, effectively a nonce, identifies the
    /// owner (an object ID) of the key and must be supplied to `UnwrapKey`.
    /// `metadata` indicates that the key will be used to encrypt metadata which
    /// might influence the choice of wrapping key used by the service.  Returns
    /// the wrapping key ID used to wrap this key along with the wrapped and
    /// unwrapped key.  Errors:
    ///   ZX_ERR_INVALID_ARGS: purpose is not recognised.
    ///   ZX_ERR_BAD_STATE: the crypt service has not been correctly initialized
    ///     with a wrapping key for the specified purpose.
    CreateKey(struct {
        owner uint64;
        purpose KeyPurpose;
    }) -> (struct {
        wrapping_key_id uint64;
        wrapped_key bytes:48;
        unwrapped_key bytes:32;
    }) error zx.status;

    /// Unwraps keys that are wrapped by the key identified by `wrapping_key_id`.
    /// `owner` must be the same as that passed to `CreateKey`.  This can fail due
    /// to permission reasons or if an incorrect key is supplied.
    UnwrapKey(struct {
        wrapping_key_id uint64;
        owner uint64;
        key bytes:48;
    }) -> (struct {
        unwrapped_key bytes:32;
    }) error zx.status;
};
  • wrapping_key_id 的含义由加密服务的实现者决定。Fxfs 的值不会对其值产生影响。

  • 每个加密 Fxfs 卷都应有单独的连接,这样一来,如果需要的话,服务器可以托管在不同的进程中。

  • 目前,将支持 256 位密钥(与 Zxcrypt 一致)。

  • 预计加密服务将使用 AEAD 来封装密钥,这意味着封装密钥的大小可能是 48 个字节。

  • purpose 用于分隔用于元数据的键和用于数据的键,并且旨在促进按键滚动(见下文)。

加密服务的精确实现以及密钥管理政策不在该设计的讨论范围内。

磁盘格式

每个文件都包含如下内容:

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum EncryptionKeys {
    None,
    AES256XTS(WrappedKeys),
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct WrappedKeys {
    /// The keys (wrapped).  To support key rolling and clones, there can be more
    /// than one key.  Each of the keys is given an identifier.  The identifier is
    /// unique to the object.  AES256-XTS requires a 512 bit key, which is made
    /// of two 256 bit keys, one for the data and one for the tweak.  Both those
    /// keys are derived from the single 256 bit key we have here.
    pub keys: Vec<(/* wrapping_key_id= */ u64, /* id= */ u64, [u8; 48])>,
}

每个范围都大致如下所示:

pub enum ExtentValue {
    /// Indicates a deleted extent; that is, the logical range described by the
    /// extent key is considered to be deleted.
    None,
    /// The location of the extent and other related information.  `key_id`
    /// identifies which of the object's keys should be used.  `checksums` hold
    /// post-encryption checksums.
    Some { device_offset: u64, checksums: Checksums, key_id: u64 },
}

数据块将使用 AES-XTS-256(与 Zxcrypt 使用的方法相同)进行加密。该文件中的逻辑偏移量将用于调整。

元数据

对象存储会维护日志结构合并 (LSM) 树来保存其元数据,这些元数据将按照与存储区中包含的文件相同的方式进行加密。系统将创建用于图层文件的键,并将用途设置为“元数据”。

事务提交时,变更会写入日志。这些变更会应用于 LSM 树的内存层。一段时间后,内存中的层会刷新为持久性层。对象元数据树的任何变更在写入日志时都需要进行加密。为了支持这一点,Fxfs 使用新的变更:

EncryptedObjectStore(Box<[u8]>),

系统将使用流密码(AES-XTS-256 等分块加密不合适)Ccha20 来加密这些变更。此密钥将与其他未加密的存储数据一起进行封装和存储。

在重放时,密钥可能不可用,在这种情况下,这些变更将以加密形式保留在内存中。如果需要刷新内存中的数据(以释放日志中的空间),系统会将这些加密的更改写入根存储区中的对象。

当密钥可用时,可以解密并应用加密的变更,这些变更可能位于内存中或存在于上述文件中。如需对变更进行解密,则需要加密流中的偏移量;该偏移量与存储区的未加密信息一起存储。

以下是添加到 StoreInfo 的新字段(商店的未加密信息):

    // The (wrapped) key that encrypted mutations should use.
    mutations_key: Option<Box<[u8]>>,

    // Mutations for the store are encrypted using a stream cipher.  To decrypt the
    // mutations, we need to know the offset in the cipher stream to start it.
    mutations_cipher_offset: u64,

    // If we have to flush the store whilst we do not have the key, we need to
    // write the encrypted mutations to an object. This is the object ID of that
    // file if it exists.
    encrypted_mutations_object_id: u64,

删除卷

为了支持删除卷,分配元数据将包含拥有元数据的商店 ID 的对象 ID:

pub struct AllocatorKey {
    pub device_range: Range<u64>,
}

pub enum AllocationRefCount {
    // Tombstone variant indicating an extent is no longer allocated.
    None,
    // Used when we know there are no possible allocations below us in the stack.
    // (e.g. on the first allocation of an extent.)
    // This variant also tracks the owning ObjectStore for the extent.
    Abs { count: u64, owner_object_id: u64 },
}

pub struct AllocatorValue {
     /// Reference count for the allocated range.
     /// (A zero reference count is treated as a 'tombstone', indicating that older
     /// values in the LSM Tree for this range should be ignored).
     pub refs: AllocationRefCount,
}

删除卷涉及更改分配器,使其将属于已删除卷的所有记录视为可用记录。在进行重大压缩后,我们可以确定这些记录不再存在,然后就可以忘记该卷的存在。分配器会保留有关已删除卷的列表的信息及其元数据。

内嵌数据

Fxfs 目前不支持内嵌数据,但在支持内嵌数据时,系统会使用加密元数据所用的相同密钥对其进行加密。这些键会在写入新图层文件时有效地滚动。

安全清空

安全清除文件需要文件仍然不可恢复,即使密钥(硬件中存在的密钥除外)随后遭到破解。鉴于文件系统通常在闪存设备上运行(使用垃圾回收),因此擦除所有出现的数据非常重要。唯一实际的解决方案是滚动封装密钥(不应存储在闪存中)。

安全擦除功能并非计划实现的功能,但应该可以通过以下步骤实现:

  1. 开始使用新的封装密钥封装新的元数据键。

  2. Fxfs 使用旧的元数据键重写所有对象。

  3. 旧的元数据封装密钥将被销毁(通常直接或间接使用 TPM 功能 - 这超出了本设计的讨论范围)。

通过执行主要压缩,可以在 Fxfs 中相对轻松地完成第 2 步。由于这是自然发生的事情,因此此流程可以根据需要定期执行。可以安排 Fxfs 保证一次(例如每周)进行一次重大压缩,但具体取决于可用的键。

此过程要求使用不同的数据封装键封装元数据键(因为否则,它将强制重写所有数据,这是不允许的),因此需要为 create_key 方法提供元数据参数。

请注意,该过程需要重写卷的所有元数据,因此,不应经常执行这些操作。

滚动按键

上文中介绍了元数据封装密钥的滚动密钥,以确保安全清除。可以通过以下步骤滚动用于封装数据密钥的密钥:

  1. 开始使用新的封装密钥封装新密钥。为以这种方式封装的密钥返回新的 wrapping_key_id。

  2. 要求 Fxfs 重新封装与指定 wrapping_key_id 匹配的所有密钥。此 API 留给后续设计使用:它应该不需要更改磁盘格式。

  3. 按照安全擦除程序封装元数据密钥。由于密钥只写入元数据文件,因此它应确保在旧封装密钥销毁后成功回滚数据封装密钥。

请注意,第 2 步和第 3 步可以合并 - 应该可以同时执行主要压缩和重新封装密钥。

与 Secure Erase 一样,我们最初并未计划实现密钥滚动支持。

Fsck

没有该密钥的 Fsck 将执行有限的一组检查。加密存储区的一致性显然不可能,但可以检查所有其他未加密存储区的范围和一致性。

有了该密钥,您就可以执行完整的一致性检查,也可以仅检查单个卷的元数据。

多卷支持

fshost 将导出一个新目录:Volume。目录中的节点将表示 fshost 导出的卷,并且将支持新的卷协议(节点不支持其他协议,即它们不支持 fuchsia.io Node 协议,因此无法克隆)。协议大致如下所示:

type MountOptions = table {
    // A handle to the crypt service. Unencrypted volumes will ignore this option.
    crypt client_end:Crypt;
}

protocol Volume {
    // Mounts the volume and starts serving the filesystem. An error will be
    // returned if the volume is currently being served from a previous call
    // to Mount.
    Mount(resource struct {
        export_root server_end:Directory;
        options MountOptions;
    }) -> () error zx.status;
}

如果提供的密钥服务有误,装载将会失败,但这种情况属于异常情况;用户应在调用装载时预期装载会成功;不应将其用作测试凭据的方式。

导出根目录与文件系统现在公开的根目录相似。系统将公开 fs.Admin 服务,并使用关闭方法锁定/关闭卷。如果与卷的所有连接都已关闭,则卷也会锁定。锁定卷时,应小心谨慎,确保舍弃所有解封装的密钥。

枚举和移除将通过卷目录上的 fuchsia.io 目录协议完成。移除操作不一定是异步进行的,但在我们保证最终会移除卷之前,不会返回成功结果。该名称可以立即重复使用,但可能需要过一段时间才能使用。

新卷无法使用目录协议的 open 方法,因为我们需要提供加密服务和其他选项。而是会添加新的协议:

type CreateVolumeOptions = table {
    // Reserved for future use.
}

protocol Manager {
    // Creates a new data volume.
    CreateVolume(resource struct {
        name string:MAX_FILENAME;
        crypt client_end:Crypt;
        export_root server_end: Directory;
        options CreateVolumeOptions;
    }) -> () error zx.status;;
}

最初,实现将假定 Fxfs 将管理所有这些卷,但将来应该能够提供可与 Zxcrypt 支持的文件系统配合使用的组件。

卷上可能存在的名称和确切卷集将基于产品决策,不在此 RFC 的讨论范围内。

实现

对多个卷和加密的支持将以典型方式实现。

目前没有支持密钥滚动、安全清除和内嵌数据方面的计划。

AES-XTS-256、ChaCha20 和 ff1 加密将由第三方 crate 提供。

将需要进行安全审核。

性能

加密会影响性能。系统将使用现有的文件系统基准来评估性能。实现后,可以消除 Zxcrypt,这样做应该可以抵消因这种实现而导致的损失。我们会调查当前性能导致的所有性能下降问题。

向后兼容性

这是一项对 Fxfs 的破坏性更改,需要重新格式化。

安全注意事项

此 RFC 已由安全团队审核。

以下要求由安全性驱动:

  1. 每个卷应使用不同的密钥进行加密。

  2. 文件数据和元数据应加密。

  3. 使用对象 ID 作为旁路应该比较困难。

实现将需要安全审核。

隐私注意事项

此 RFC 已由隐私权团队进行审核。

为保护隐私而主要考虑的因素是,以下数据不会加密:

  • 卷中的文件数量。
  • 分配给卷的范围集(其中包括分配给卷的空间量)。
  • 卷的名称。

所有其他数据和元数据都应加密。这应该足以满足包括指纹攻击在内的要求。

实现的安全审核应验证设计是否满足这些要求。

测试

系统将使用单元测试、集成和端到端测试的常规组合对此进行测试。对于在 CQ 下运行的许多测试,Fxfs 将用于数据分区,因此会通过这种方式获得数据。

文档

在某些阶段,您需要提供文档来帮助系统设计人员为给定产品选择不同的存储方案。但是,发生该配置的方式尚未确定,并且不在此 RFC 的讨论范围之内。

此 RFC 中引入的 API 最初(可能是无限期)位于树内,目前将作为 FIDL 的一部分进行记录。

缺点、替代方案和未知情况

此 RFC 中列出的加密设计比 Zxcrypt 采用的基于分区的加密复杂得多。

系统设计人员仍然可以使用基于分区的加密,但这在卷之间的空间共享方面存在限制:可以在空间方面灵活使用的基于分区的方案需要卷管理器(由于额外的间接,从而导致性能受到影响),并且可能会受到碎片化问题(在分区之间移动空间可能需要碎片整理)。基于分区的方案支持安全清空和密钥滚动也相当困难。

早期技术和参考资料

Zxcrypt 使用 AES-256 XTS 以与此 RFC 类似的方式加密分块,但所用的调整方式有所不同。

此处的设计在很大程度上符合 Android 12 的加密要求。一个显著区别是,通过 Fxfs 日志传递的元数据需要使用未明确说明的可接受流加密进行加密。