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

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

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

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)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 流程之前,此设计已分发给存储团队和上述审核人员并已通过审核。

设计

RFC-0136 介绍了 Fxfs。

要求

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

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

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

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

  5. 应支持密钥轮替,而无需进行巨大的迁移工作。

  6. 卷的大小应受到限制(不过,此设计将留待日后完成)。

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

  8. 应针对基于块计数的指纹攻击提供保护。这种攻击方式是,知道一组文件的加密大小(向上取整到最近的块)后,就可以确定该组文件是否存在于文件系统中。

不在范围内

此设计未涵盖以下领域:

  1. 在设备(包括主机和目标设备)之间任意方向传输加密映像 - 虽然能够执行此操作有助于调试,但在正式版设备上实现此操作的可能性不大,并且没有先例。

  2. 加密服务的实现。此设计在其他部分有所介绍。

  3. 该设计应支持密钥滚动,但在组件之间使用的精确 API 超出了本文档的讨论范围。

概览

Fxfs 加密

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

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

  1. 该日志(位于根父级存储区中)将包含针对使用流加密算法加密的子存储区元数据的更改。此加密算法的密钥将按存储区分配。

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

根父级存储空间和根存储空间中的其他对象不会被加密,即超块、与分配器相关的所有对象、用于支持根对象存储空间的层文件(其中包含根存储空间对象的元数据),以及包含子存储空间基本信息的对象。这些都不是用户数据。

最终结果是:

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

部分信息不会加密:

  • 卷中的文件数。
  • 分配给卷的一组 extent。

对象 ID

在 Fxfs 中,对象 ID 目前以单调递增的方式分配,可用作边信道。为解决此问题,系统会在分配时使用 ff1 加密对对象 ID 的最低 32 位进行加密。在循环处理 32 位对象 ID 后,密钥将旋转。密钥将经过封装存储,并与未加密的其他存储信息一起存储。每次轮替键时,对象 ID 的 32 位高字节都会单调递增。

卷中使用的对象数量和空间也提供了一个侧信道(基本上是通过 statvfs 提供的所有信息)。解决此问题超出了此设计的范围(这对 Fxfs 来说并不新)。

密钥管理

密钥封装和解封装由单独的“加密”服务负责,该服务将提供类似于以下协议的服务:

/// 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 等分块加密算法不适用)Chacha20 来加密这些更改。此类密钥将被封装并与其他未加密的存储数据一起存储。

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

密钥可用后,系统就可以解密并应用可能位于内存中或存在于上述文件中的加密更改。如需解密更改,需要密码流中的偏移量;该偏移量会与商店的未加密信息一起存储。

以下是添加到 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 步可以合并 - 应该可以同时执行主要压缩并重新封装键。

与安全擦除功能一样,我们最初不打算实现密钥轮替支持。

Fsck

没有密钥的 fsck 将执行一组有限的检查。显然无法检查加密存储空间的一致性,但可以检查所有其他未加密存储空间的 extent 和一致性。

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

多卷支持

fshost 将导出一个新目录:volumes。目录中的节点将代表由 fshost 导出的卷,并将支持新的卷协议(节点上不支持任何其他协议,即它们不支持 fuchsia.io 节点协议,因此无法克隆)。该协议将如下所示:

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;
}

如果提供错误的加密服务,挂载将会失败,但这应该是例外情况;用户调用 Mount 时应该希望 Mount 成功;不应将其用作测试凭据的手段。

导出根目录现在看起来就像文件系统公开的根目录一样。fs.Admin 服务将公开,并且可以使用 Shutdown 方法锁定/关闭卷。如果关闭与卷的所有连接,该卷也会锁定。锁定卷时,系统会小心确保丢弃所有已展开的密钥。

枚举和移除将通过卷目录上的 fuchsia.io 目录协议完成。移除操作可能是异步的,也可能是同步的,但除非系统保证最终会移除相应卷,否则不会返回成功状态。名称可以立即重复使用,但存储空间可能需要一段时间才能供您使用。

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

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 已通过隐私权团队的审核。

主要隐私保护注意事项是,以下数据不会被加密:

  • 卷中的文件数。
  • 分配给卷的一组 extent(其中包括分配给卷的空间量)。
  • 卷的名称。

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

对实现进行安全审核时,应验证设计是否满足这些要求。

测试

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

文档

在某个阶段,文档将有助于系统设计师在给定产品的不同存储选项之间进行选择。不过,配置方式尚未确定,不在本 RFC 的讨论范围之内。

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

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

本 RFC 中所述的加密设计比 Zxcrypt 使用的基于分区的加密要复杂得多。

系统设计人员仍然可以使用基于分区的加密,但这会限制卷之间的空间共享:基于分区的方案需要使用卷管理器,以便灵活使用空间(因此会因额外的间接而导致性能下降),并且可能会出现碎片问题(在分区之间移动空间可能需要进行碎片整理)。此外,在基于分区的方案中支持安全擦除和密钥滚动也要困难得多。

在先技术和参考文档

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

此处的设计在大多数方面都符合 Android 12 的加密要求。一个显著的区别是,通过 Fxfs 日志的元数据需要使用流加密算法进行加密,但该算法并未明确提及为可接受的加密算法。