核心算法原理:从状态矩阵到密钥流
好,咱们直接进入正题。ChaCha20 的核心,说白了就是四个操作:初始化状态矩阵、Quarter Round 函数、Double Round 与 10 轮迭代,以及最后的密钥流生成。这四步串起来,就是整个算法的骨架。
我个人习惯把 ChaCha20 看作一台「搅碎机」—— 你把密钥、随机数、计数器扔进去,它转 20 圈(其实是 10 轮 Double Round),出来一堆看似随机的字节流。嗯,就这么简单。
2.1 初始化状态矩阵:16 个 32 位字的排列
ChaCha20 的状态是一个 4×4 的矩阵,总共 16 个 32 位字(64 字节)。初始化时,这 16 个字按固定顺序填入:
| 位置 | 内容 | 说明 |
|---|---|---|
| 0-3 | 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574 |
常量 "expand 32-byte k" 的 ASCII 小端编码 |
| 4-11 | 密钥(256 位,8 个字) | 用户提供的对称密钥 |
| 12 | 计数器(32 位) | 从 0 开始,每生成 64 字节递增 1 |
| 13-15 | 随机数(96 位,3 个字) | 每次加密必须唯一,通常由发送方生成 |
你想想看,这 16 个字里,只有密钥和随机数是真正「秘密」的。常量是公开的,计数器也是公开的。那安全性从哪来?
答案就在后面的搅动过程里。
重要提醒: 初始化时,所有字都按小端序存储。比如常量 0x61707865 在内存里是 65 78 70 61。这个细节在移植到不同平台时特别容易踩坑,我当年在 ARM Cortex-M4 上就吃过这个亏。
2.2 Quarter Round 函数:ChaCha 的最小搅动单元
Quarter Round(四分之一轮)是 ChaCha20 的基本操作单元。它接收 4 个 32 位字(a, b, c, d),做以下操作:
a += b; d ^= a; d <<<= 16;
c += d; b ^= c; b <<<= 12;
a += b; d ^= a; d <<<= 8;
c += d; b ^= c; b <<<= 7;
说白了就是「加、异或、循环左移」三件套,重复 4 次。每次的移位量不同:16、12、8、7。为什么选这几个数?
我记得在设计 ChaCha 的原始论文里,DJ Bernstein 做过大量测试,这组移位量能在扩散性和性能之间取得最佳平衡。我在项目中实测过,换成别的移位量,要么扩散不够,要么跑起来慢一截。
我的经验: 如果你要在硬件里实现 Quarter Round,注意那 4 个加法是模 2^32 加法,不是普通整数加法。C 语言里直接用 uint32_t 就行,溢出自动截断。但如果你用 Python 做原型,记得手动 & 0xffffffff。
2.3 Double Round 与 10 轮迭代
一个 Double Round 包含 8 次 Quarter Round:先做 4 次「列轮」,再做 4 次「对角线轮」。
列轮操作的是矩阵的列:
- Quarter Round(0, 4, 8, 12)
- Quarter Round(1, 5, 9, 13)
- Quarter Round(2, 6, 10, 14)
- Quarter Round(3, 7, 11, 15)
对角线轮操作的是矩阵的对角线:
- Quarter Round(0, 5, 10, 15)
- Quarter Round(1, 6, 11, 12)
- Quarter Round(2, 7, 8, 13)
- Quarter Round(3, 4, 9, 14)
为什么叫「Double Round」?因为一次列轮加一次对角线轮,正好把矩阵里的每个字都搅动了两次。ChaCha20 总共做 10 次 Double Round,也就是 20 次 Quarter Round 的「层」—— 这也是 ChaCha20 名字的由来。
避坑指南: 我曾经在优化时试图减少轮数到 8 轮(ChaCha16),结果被安全审计打了回来。10 轮是经过密码分析验证的安全下限,别自作聪明去减。反过来,加轮数对安全提升微乎其微,只会拖慢速度。
下面是整个迭代过程的 SVG 流程图,你可以直观看到数据是怎么流动的:
2.4 密钥流生成机制
10 轮 Double Round 跑完之后,我们得到了一组「搅动后」的 16 个字。但这还不是密钥流。
ChaCha20 的密钥流生成有一个关键步骤:将搅动后的状态与初始状态相加(模 2^32 加法)。
for i in 0..15:
keystream[i] = initial_state[i] + final_state[i]
为什么要加这一步?
我刚开始学的时候也纳闷:直接拿搅动后的状态当密钥流不行吗?后来在实现中才理解 —— 如果不加初始状态,整个算法就是可逆的。攻击者只要拿到一段密钥流,就能反推出内部状态,进而预测后续的密钥流。加上初始状态后,这个反推就变得不可能了。
最终,这 16 个字(64 字节)就是当前计数器值对应的密钥流块。加密时,把明文和密钥流逐字节异或;解密时,再做一次同样的异或。
核心要点: 每生成 64 字节密钥流,计数器就加 1。计数器从 0 开始,最大到 2^32 - 1。这意味着一个密钥和随机数组合最多可以加密 2^32 × 64 字节 ≈ 256 GB 的数据。对于绝大多数物联网场景,这个量级绰绰有余。
最后,我用一个表格总结一下这四步的关系:
| 步骤 | 输入 | 输出 | 关键点 |
|---|---|---|---|
| 初始化 | 常量、密钥、计数器、随机数 | 4×4 状态矩阵 | 小端序存储,计数器从 0 开始 |
| Quarter Round | 4 个 32 位字 | 4 个搅动后的字 | 加、异或、循环左移 |
| Double Round × 10 | 状态矩阵 | 搅动后的状态 | 列轮 + 对角线轮,共 20 层 QR |
| 密钥流生成 | 初始状态 + 最终状态 | 64 字节密钥流 | 模加后输出,计数器递增 |
嗯,到这里,ChaCha20 的核心算法原理就讲完了。你可能会觉得:就这么点东西?对,就这么点。ChaCha20 的魅力就在于它的简洁 —— 没有复杂的 S 盒,没有费时的查表操作,全是基础的算术和逻辑运算。这也是它能在轻量级设备上跑得飞快的原因。