一个用 Rust 构建的多线程 AEAD 加密引擎。使用三重缓冲的 io_uring 流水线、通过 Rayon 实现的并行分块处理以及通过 ring 实现的汇编优化密码,以每秒千兆字节的吞吐量加密和解密文件。
⚠️ 免责声明:实验性软件 ⚠️
该项目极其新,目前不建议用于生产或关键任务场景。 虽然加密原语(通过 ring 的 AES-256-GCM、ChaCha20-Poly1305)和格式设计是可靠的,但代码库尚未经过正式安全审计或广泛的实际测试。使用风险自负。如需保护敏感数据,请考虑使用经过实战检验的工具(如 GnuPG、age 或 OpenSSL),直到该项目成熟。
ring(汇编优化)实现 AES-256-GCM(硬件 AES-NI)和 ChaCha20-Poly1305--memory 配置)ring 的 seal_in_place_separate_tag / open_in_place 最小化热循环中的内存分配O_DIRECT I/O,绕过内核页面缓存,实现 NVMe 上的 DMA 速度读写。缓冲区池使用 std::alloc,对齐为 4096 字节使用 cargo bench(Criterion,每个测量 10 个样本)进行基准测试。排除密钥派生——数字仅反映纯加密吞吐量。
硬件:
关于 I/O 的说明: Criterion 将临时文件写入 /tmp,在该系统上为 tmpfs(内存支持)。使用 O_DIRECT 时,内核无法在 tmpfs 上使用真正的异步 DMA,因此这些数字反映了密码吞吐量 + io_uring 开销 而没有 DMA 绕过的好处。在真正的 Gen4 NVMe 驱动器上,O_DIRECT 消除了页面缓存双缓冲,并允许直接进入对齐缓冲区池的 DMA,这应该会带来显著更高的吞吐量。
块大小扫描(AES-256-GCM,64 MiB 文件):
该引擎使用 ring(汇编优化的 AES-NI / NEON / ARMv8-CE)进行密码操作,并使用三重缓冲的 io_uring 流水线进行 I/O。三个预分配的缓冲区池在流水线中轮换:当池 A 的写入操作在内核中完成时,池 B 正在被 CPU 上的 Rayon 加密,而池 C 的读取操作正在提交到内核。这实现了 I/O 延迟与加密计算的重叠。
为什么小文件上 AES-256-GCM 比 ChaCha20-Poly1305 更快:
ring 的 AES-GCM 后端利用 x86-64 上可用的 AES-NI + CLMUL 硬件指令,使其比 ChaCha20(一种软件密码)具有硬件优势。在较大文件上,两种密码收敛到约 1.0 GiB/s,表明瓶颈从密码吞吐量转移到 I/O 提交开销。
为什么峰值吞吐量在 1-16 MiB 而不是 256 MiB: 小文件(1-16 MiB)的块数少,因此 Rayon 并行性高效,工作集适合缓存。在 64-256 MiB 时,io_uring 流水线完全活跃(三批在途),但每次 SQE 提交和 CQE 完成的开销随块数缩放。三重缓冲设计确保 I/O 和加密重叠,部分隐藏了此成本。
为什么约 1.0 GiB/s 而不是 10+ GiB/s: 现代 AES-NI 每核心可推送 2-4 GiB/s。使用 12 个线程,原始密码吞吐量可能超过 10 GiB/s。三个因素解释了差异:
PIPELINE_DEPTH=3 时,任何时候只有三批在流水线中轮换。真正的稳态重叠需要至少三批;适合一批或两批的文件无法从流水线中受益。缓冲区生命周期和安全性:
缓冲区池在 io_uring 环创建之前通过 std::alloc::alloc_zeroed 和 Layout::from_size_align(size, 4096) 一次性分配,并在所有流水线迭代中重用而不重新分配。每个加密块在 O_DIRECT 写入之前零填充到扇区对齐。环在缓冲区池之前显式丢弃,确保内核从不引用已释放的内存(无 UAF)。
git clone https://github.com/frogsnot/concryptor.git
cd concryptor
cargo build --release
二进制文件位于 target/release/concryptor。
# AES-256-GCM(默认),输出到 myfile.dat.enc
concryptor encrypt myfile.dat
# ChaCha20-Poly1305,自定义输出路径
concryptor encrypt myfile.dat --cipher chacha -o encrypted.enc
# 自定义块大小(以 MiB 为单位)
concryptor encrypt largefile.iso --chunk-size 8
# 更强的 KDF(512 MiB 内存成本)
concryptor encrypt secrets.tar --memory 512
# 非交互式(跳过密码提示)
concryptor encrypt myfile.dat -p "password"
安全说明:
--password/-p将密码作为 CLI 参数传递,这在ps输出和 shell 历史记录中可见。对于交互式使用,请省略它以获得安全的隐藏提示。对于脚本编写,建议清理历史记录或使用从文件描述符读取的包装器。
# 加密目录(自动检测,生成 mydir.tar.enc)
concryptor encrypt mydir/
# 使用自定义密码和输出
concryptor encrypt mydir/ --cipher chacha -o secrets.enc
目录加密创建一个临时 tar 归档(.concryptor-*.tar,权限 0600,CSPRNG 命名),对其进行加密,然后自动删除临时文件。文件名、目录结构、权限和时间戳都在加密负载内。
# 自动去除 .enc 扩展名
concryptor decrypt myfile.dat.enc
# 自定义输出路径
concryptor decrypt encrypted.enc -o restored.dat
# 非交互式
concryptor decrypt myfile.dat.enc -p "password"
# 一步解密并提取(自动去除 .tar.enc -> 目录名)
concryptor decrypt mydir.tar.enc --extract
# 短标志,自定义输出目录
concryptor decrypt mydir.tar.enc -x -o restored_dir/
不使用 --extract 时,解密目录归档会生成中间的 .tar 文件,您可以检查或手动提取。
concryptor --help
concryptor encrypt --help
concryptor decrypt --help
所有值为小端序。头占用完整的 4 KiB 扇区;每个加密块槽填充到下一个 4 KiB 边界。这确保每个偏移量和 I/O 大小都是扇区对齐的,以支持 O_DIRECT。
偏移量 大小 字段
------ ----- ---------------------
0 10 魔数 "CONCRYPTOR"
10 1 格式版本 (4)
11 1 密码类型 (0 = AES-256-GCM, 1 = ChaCha20-Poly1305)
12 4 块大小(字节,LE)
16 8 原始文件大小(字节,LE)
24 16 Argon2 盐(加密随机,每文件唯一)
40 12 基础 nonce(加密随机,每文件唯一)
52 4 Argon2 m_cost(KiB,LE,0 = 旧版 64 MiB)
56 4 Argon2 t_cost / 迭代次数(LE,0 = 旧版 3)
60 4 Argon2 p_cost / 并行度(LE,0 = 旧版 4)
64 4032 保留(零填充至 4096 字节)
4096 ... [块 0: 密文 + 16 字节标签 + 零填充至扇区边界]
[块 1: 密文 + 16 字节标签 + 零填充至扇区边界]
...
对于 4 MiB 块:每个磁盘槽为 ceil((4194304 + 16) / 4096) * 4096 = 4198400 字节(每块 4080 字节填充)。头中的 4032 个保留字节可用于将来特性(非对称密钥槽、元数据等)。
每次加密时,盐和基础 nonce 都从 rand::rng()(由操作系统 CSPRNG 支持)新鲜生成。跨文件重用密码是安全的,因为不同的盐产生不同的 Argon2id 密钥,不同的基础 nonce 产生不同的每块 nonce。
chunk_nonce = base_nonce XOR chunk_index(TLS 1.3 风格)。交换块会导致解密失败,因为位置 N 的 nonce 与用于加密原始位置 M 的块的 nonce 不匹配。注意:基于 XOR 的 nonce 派生在跨多个流使用 相同密钥 时存在理论弱点(不同的基础 nonce 可能产生重叠的 nonce 空间)。这不适用于 Concryptor,因为每次加密生成一个新的 128 位随机盐,为每个文件产生唯一的 Argon2id 密钥。Nonce 唯一性仅在相同密钥下重要,而每对文件密钥重用概率约为 2^-128。AAD = full_aligned_header (4096) || chunk_index (8 LE) || is_final (1)(共 4105 字节)。完整的 4 KiB 头扇区(核心字段、KDF 参数和保留填充)绑定到每个块的认证标签中。修改 任何 头字节(密码类型、块大小、原始大小、盐、nonce、KDF 参数或保留填充)会使所有块无效。这防止了攻击者编辑 original_size 并删除尾部块的截断攻击,也防止了在保留填充区域中走私数据。旧版 v3 文件使用 52 字节 AAD 解密以保持向后兼容;无法从 v4 降级到 v3,因为版本字节本身在认证的 AAD 内。0x01,对于所有其他块为 0x00。这防止了两种攻击:
is_final = 0x00 加密,但解密期望 0x01。is_final = 0x01 生成有效标签。rand::rng())中抽取 16 字节盐和 12 字节基础 nonce。相同文件使用相同密码的两次加密产生完全不同的密文。Nonce 重用(对 AES-GCM 是灾难性的)通过构造避免。# 运行完整测试套件(67 个测试)
cargo test
# 运行基准测试(HTML 报告在 target/criterion/ 中)
cargo bench
# 过滤基准测试
cargo bench -- "encrypt/AES"
cargo bench -- "chunk_sweep"
测试套件涵盖:
original_size + 移除的块)chunk_size)# 从 crates.io(推荐)
cargo install concryptor
# 从源码
git clone https://github.com/FrogSnot/Concryptor
cd Concryptor
cargo build --release
# 二进制文件位于 target/release/concryptor
本项目采用 GNU Affero General Public License v3.0 许可。
| 文件大小 | AES-256-GCM 加密 | ChaCha20 加密 | AES-256-GCM 解密 | ChaCha20 解密 |
|---|
| 64 KiB | 244 MiB/s | 233 MiB/s | 233 MiB/s | 234 MiB/s |
| 1 MiB | 1.08 GiB/s | 882 MiB/s | 1010 MiB/s | 876 MiB/s |
| 16 MiB | 1.10 GiB/s | 923 MiB/s | 1.06 GiB/s | 988 MiB/s |
| 64 MiB | 984 MiB/s | 935 MiB/s | 988 MiB/s | 973 MiB/s |
| 256 MiB | 1.00 GiB/s | 1015 MiB/s | 1.01 GiB/s | 1.02 GiB/s |
| 块大小 | 吞吐量 |
|---|
| 64 KiB | 1.01 GiB/s |
| 256 KiB | 1.05 GiB/s |
| 1 MiB | 1.07 GiB/s |
| 4 MiB | 988 MiB/s |
| 8 MiB | 988 MiB/s |
| 16 MiB | 1.00 GiB/s |
--memory| Crate | 用途 |
|---|
ring | 汇编优化的 AES-256-GCM 和 ChaCha20-Poly1305 AEAD |
io-uring | 用于异步读/写 I/O 的 Linux io_uring 接口 |
libc | 用于头 I/O 的 O_DIRECT 标志和对齐的 pread/pwrite |
argon2 | Argon2id 密钥派生 |
rayon | 数据并行块处理 |
clap | CLI 参数解析 |
indicatif | 终端进度条 |
rand | 加密随机数生成 |
zeroize | 安全内存擦除 |
anyhow | 错误处理 |
rpassword | 隐藏密码输入 |
tar | 目录归档和提取 |