38 个版本
0.4.5 | 2023年12月12日 |
---|---|
0.4.4 | 2023年12月8日 |
0.4.3 | 2023年11月15日 |
0.3.4 | 2023年10月16日 |
0.1.14 | 2023年3月30日 |
#266 in 加密
每月 64 次下载
1.5MB
2.5K SLoC
ArunaReadWriter
关于自定义组件 ArunaReadWriter
的使用简要说明。有关正式文件规范,请点击 此处。
为 Aruna 对象存储(AOS)定制的数据转换组件的通用版本。思路简单,您只需实现这些简单的基 trait 并使用您自定义的数据转换逻辑
#[async_trait::async_trait]
pub trait Transformer {
async fn process_bytes(&mut self, buf: &mut bytes::BytesMut, finished: bool) -> Result<bool>;
}
之后,实现 Transformer
的结构可以注册到 ArunaReadWriter
中,以在 ReadWriter 的 Read
和 Write
部分之间插入。
示例
let file = b"This is a very very important test".to_vec();
let mut file2 = Vec::new();
// Create a new ArunaReadWriter
ArunaReadWriter::new_with_writer(file.as_ref(), &mut file2)
.add_transformer(ZstdEnc::new(1, false))
.add_transformer(ZstdEnc::new(2, false)) // Double compression because we can
.add_transformer(
ChaCha20Enc::new(false, b"wvwj3485nxgyq5ub9zd3e7jsrq7a92ea".to_vec()).unwrap(),
)
.add_transformer(
ChaCha20Enc::new(false, b"99wj3485nxgyq5ub9zd3e7jsrq7a92ea".to_vec()).unwrap(),
)
.add_transformer(
ChaCha20Dec::new(Some(b"99wj3485nxgyq5ub9zd3e7jsrq7a92ea".to_vec())).unwrap(),
)
.add_transformer(
ChaCha20Dec::new(Some(b"wvwj3485nxgyq5ub9zd3e7jsrq7a92ea".to_vec())).unwrap(),
)
.add_transformer(ZstdDec::new())
.add_transformer(ZstdDec::new())
.add_transformer(Filter::new(Range { from: 0, to: 3 }))
.process()
.await
.unwrap();
assert_eq!(file2, b"Thi".to_vec());
此示例创建了一个从字节数组(实现 AsyncRead)到 Vec<u8>
的转换(实现 AsynWrite),并将其沉入另一个 Vec<u8>
中。在之间,可以进行自定义数据转换。
示例将向量使用自定义填充的 Zstandard 压缩组件压缩两次,然后使用 ChaCha20-Poly1305 加密结果两次。之后,所有步骤都逆转,得到原始数据。
关于自定义实现的说明
主要逻辑是围绕 process_bytes 函数构建的。
async fn process_bytes(&mut self, buf: &mut bytes::Bytes, finished: bool, should_flush: bool) -> Result<bool>;
思路是,您的 Transformer 会接收到一个包含您必须转换的字节的可变缓冲区。如果您已经转换了数据(全部或通过内部缓冲区),则将数据放回缓冲区,以便下一个 Transformer 的 process_bytes
方法。如果 should_flush
为 true
,则应立即刷新和清除所有内部缓冲区。
ARUNA 文件格式
本文档包含了 aruna 文件格式(对应于 .zst.c4gh
)的正式描述。这种文件格式在保持对大型多吉字节文件进行合理性能索引解决方案的同时,实现了压缩和加密。针对对象存储解决方案,如 S3 进行优化。
规范
aruna 文件格式的核心是 GA4GH 的 crypt4gh 加密格式与 zstandard 压缩算法(《RFC8878》)的结合。这通过一个可选的自定义脚块进行扩展,该脚块包含用于在较大文件中解密和解压缩块的定位信息。
结构
Aruna 文件由三个不同的部分组成。一个头部部分,随后是压缩和加密的数据块,以及一个可选的脚部部分,其中包含间接索引信息和块大小。
数据结构
对于压缩,数据首先应分割成大小为 5 MiB 的原始数据块(最后一个块除外)。这些块必须使用 zstandard 算法进行压缩,并选择一个压缩级别,可以可选地以 MAC 结尾。如果压缩后的尺寸不是 65536 字节的倍数(64 KiB),并且原始文件大小超过 5 MiB,则每个压缩帧之后必须跟随一个定义为《RFC8878》的 skippable frame
。跳过的帧应使用 0x184D2A50
作为 Magic_Number,并应避免使用 0x184D2A51
和 0x184D2A52
以避免与自定义脚部部分混淆。如果存在多个块,则必须使用跳过的帧来将总压缩大小对齐到加密块大小的倍数(除最后一个块外)。由于跳过的帧最小大小为 8 字节,它们在最坏情况下会扩展数据 65536 + 7 = 65543 字节。小于 5 MiB 的原始文件不应包含任何跳过的帧,并且出于性能考虑应省略任何索引。
由压缩数据组成的最终块必须使用 ChaCha20-Poly1305_ietf 加密块进行加密,加密块大小为 65536 字节,使用安全生成的随机加密密钥。所有块都应前导一个针对每个块的随机生成的 12 字节 Nonce,并以 16 字节的消息认证码(MAC)结尾。这导致总块大小为 65562 字节。如果文件未压缩的大小小于 5 MiB,则文件的最后加密块可以小于这个大小。
如果文件大小超过 5 MiB,则构建 5 MiB 原始数据的块数量总和应汇总为一个介于 1 到 81(最后一个块 +2 = 83)之间的无符号整数。81 是最大值,因为 5 MiB 完全等于 80 个 65536 字节的块,并且在最坏的情况下,如果没有压缩,跳过的帧可能会扩展这个值最多一个块。这个索引数字存储在文件最后的一个或两个加密跳过帧块中,作为按顺序快速访问数据的索引。
头部
主要头部与 crypt4gh 标准指定的头部相同,包含特定接收者的块和加密信息。此头部是临时生成的,并且不会与数据本身一起存储,以避免多次重新加密第一个部分。
脚部
页脚由一个或两个加密的65536字节大小的块组成,这些块包含可跳过的帧,其中包含1字节无符号整数,用于按顺序存储5MiB原始未压缩数据的每个块的索引信息。这些块以下列结构存在于 小端 格式。
Header
包含Magic_Number
0x184D2A51
用于一个块0x184D2A52
如果附加了两个块。(4字节)Frame_Size
= 65536,作为无符号32位整数Block_Total
= 32位无符号整数,表示64Kib + 28字节块的总数。Block_List
= 每个段的大小为5MiB,以64Kib + 28字节块为单位,按顺序以无符号8位整数表示Padding
= 0x00字节,用于填充64 Kib块。
如果页脚包含两个块(通过Magic_Number 0x184D2A52
指示),则两个块应重复包含相同信息的 Header
/ Frame_Size
/ Block_number
部分。
实际指南
本节包含关于构建符合此格式的加密逻辑的实际建议。
压缩和加密
- 将文件分割成5 MiB块
- 如果结果只有一个块
- 压缩整个块
- 将压缩数据分割成64Kib部分
- 使用随机nonce加密64 Kib部分
- 将
Nonce
+Encrypted data
+MAC
连接到一个块中 - 按顺序连接所有块
- 如果结果有多个块
- 压缩每个块
- 计算填充到每个块64 Kib部分“缺失”字节的公式:
Compressed size % 65536
如果结果result
小于8,则可跳过的块将为result + 64 Kib
否则,可跳过的帧大小应为result
- 创建一个大小为
result
的可跳过帧,具有魔法头0x184D2A50
以将结果对齐到64Kib - 将生成的压缩部分分割成64 Kib部分,并记住部分数量
- 使用随机nonce加密64 Kib部分
- 将
Nonce
+Encrypted data
+MAC
连接到一个块中 - (如果共享)以包含加密信息的crypt4gh头部开始
- 按顺序连接所有块和所有块
- 如 Footer 部分所述,创建一个可跳过的帧,其中包含每个压缩块的已知部分数量
- 将一个或两个页脚附加到文件中
解密和解压缩
此过程有两个选项:一个简单的单线程选项和一个更可并行化的多线程选项。多线程仅在文件大于10-20 MiB时才提供显著优势。
选项A(单线程):
- 此过程应使用常规的crypt4gh和zstd工具工作。
- 从文件开头读取,通过头部部分获取加密信息
- 按顺序使用头部中的加密密钥和每个块中前置的12字节nonce解密每个64 Kib
- 结果数据可以直接输入到zstd解压缩器中。
- 应跳过所有填充信息以及页脚部分作为可跳过帧。
选项B(多线程):
- 获取压缩和加密文件的长度
- 如果文件明显小于5 MiB -> 继续使用选项A
- 读取并解密最后两个加密块2x(64 Kib + 28字节)并将它们存储在单独的变量中。
- 检查最后一个块的
Magic_Number
,如果它是0x184D2A51
,则丢弃倒数第二个块;如果数字是0x184D2A52
,则从倒数第二个块开始。 - 决定应该启动多少个并行解密和解压缩线程。
- 按照在页脚部分描述的,将
Block_Total
大致分成与您并行线程的数量相等的部分。这将导致每个线程应处理64 Kib的块数量。 - 开始迭代页脚中的
Block_List
部分,并为块列表中的每个条目累加块数,记住以0开始的初始块。如果总和超过上一步中确定的块数,则启动一个单独的线程来处理从您的初始块到当前块末尾的部分。将初始块设置为下一个块的开始,并重复此过程。- 每个线程都得到一个“初始”和一个“到”块索引以进行处理。
- 这些数字与文件中的字节偏移有关,公式如下:
blocknumber * (65536 + 28)
- 示例1:处理从0到222的所有块 -> 范围:0 - 14554764字节
- 示例2:处理从222到444的所有块 -> 范围:14554765 (14554764 + 1) - 29109528字节
- 由于数据对齐,可以等效地处理为选项A
- 之后,将每个线程中解密/解压缩的所有部分按正确顺序连接起来,以获得完整的文件。
选项C(特定范围):
如果您只想从文件中获取特定范围,则按以下步骤操作
- 按选项B中描述的方式获取页脚,如果文件小于5 Mib,则解密/解压缩整个文件,并从生成的原始数据中提取所需的范围。
- 根据您的范围请求确定所需的节。数据是以5 Mib的块压缩的,因此范围必须首先转换为5 Mib块的索引。这可以通过将索引整数除以5Mib(5242880)来完成。
- 示例:范围:开始:5242111 - 结束:20971320 -> 开始 // 5 Mib = 0, 结束 // 5 Mib = 3 -> 需要索引从0到3的5 Mib块。
- 从块列表的开始迭代,将变量A中的计数累加到开始索引,并将从开始到结束索引的计数单独累加到变量B中。
- 变量A和B表示需要解密和压缩的压缩字节的范围,以获取所需的范围的所有数据。计算范围的公式是:从
Variable A * (65536 + 28)
到Variable A * (65536 + 28) + Variable B * (65536 + 28)
这个范围应只包含所需的5 Mib大小的完整加密/压缩块。 - 区块可以像在选项A或选项B中一样进行解密和解压缩。
- 之后获取“真实”请求范围,即第一个
开始范围 % 5 Mib
字节和最后一个5 Mib - 结束范围 % 5 Mib
字节,或者必须被丢弃。这也可以通过首先丢弃开始部分,然后仅返回请求范围的“大小”(开始范围 - 结束范围
)来实现。
讨论
Aruna文件格式考虑了多个方面,如压缩率、访问速度等,并试图创建一个平衡的中庸之道,最适合广泛的文件类型。通过利用现有的标准算法和程序,生成的文件可以使用现有的工具读取,无需特定软件来处理。然而,这种文件格式的全部潜力只能通过使用存储在可跳过帧中的附加信息的定制软件来实现。
依赖项
~13–24MB
~352K SLoC