RC2 的 π 表究竟怎么造出来的:一个迟到三十年的洗牌答案
一、事件概述
RC2 是 Ron Rivest 在 1980 年代末设计的块密码。它的密钥扩展阶段会把用户密钥的每个字节送进一张固定的 256 字节置换表(RFC 2268 中称为 PITABLE),把短密钥“摊”成更长的内部密钥。所有 RC2 实现里,这 256 个数字都是同一份。
问题在于:这类常量表是理论上可以藏后门的地方。密码学界的惯例是从 π 这类公共常量的数字中推导出表,并把推导方法公开,让任何人都能重新算一遍、确认表中没有任何“手工挑选”的成分——这就是所谓的 “nothing-up-my-sleeve” 数字。但 RFC 2268 只写了 PITABLE 是“基于 π 的数字生成的‘随机’字节数组”,从未给出生成方法;同一份文档里还有第二张表(作者的叫法是 version table,即 RFC 中的 Table[]),连“来自 π”这句话都没有。
2026 年 10 月 18 日,Khalid Hamadeh 发布研究笔记《How RC2’s π table was made》,给出了一个能逐位精确复现两张表的构造方法。按作者的说法,这“很可能就是当年制表的原始方式”,但 Rivest 究竟怎么想到这条规则,原文未说明。作者同时强调:这个结果对 RC2 的安全性没有任何影响。
二、关键技术点
1. 构造:π 当骰子的 Fisher–Yates 洗牌
方法本身是密码学里最标准的公平洗牌算法 Fisher–Yates,并不保密。真正缺失的一直是“随机性从哪来”。作者的答案是:
- 把 0–255 共 256 张“牌”按序排开;
- 从第 2 张到最后一张依次处理,把当前牌与它及它之前位置中的一张随机牌交换;
- “随机”取自 π 的十进制数字,每次抽样固定读 3 位,从最开头的数字 3 开始;
- 若这一次抽样会造成偏差(即
x ≥ n·⌊1000/n⌋),则丢弃这 3 位、重新读,保证均匀性。
算法伪代码(对应原文 Algorithm 1):
1 | rand(n): |
开头几步是:读到 314,第 1 张牌与位置 314 mod 2 = 0 交换;读到 159,第 2 张牌与 159 mod 3 = 0 交换;读到 265,第 3 张牌与 265 mod 4 = 1 交换……整个洗牌共丢弃 21 次抽样(其中两次正好落在 π 第 762 位小数处那串连续六个 9,即“费曼点”),合计用掉 828 位 π 的十进制数字。
2. 第二张表:换个起点而已
RFC 2268 第 6 节印出的第二张 0–255 置换表,用于把 1–255 位的有效密钥长度编码为 RC2-CBC 的 RC2Version 参数(PKCS #5 等标准会用到)。RFC 只说明它“不是密钥扩展阶段的那张表”,没有给出来源。
作者发现:把同一个算法从 π 的第 5 位数字(5926…)开始跑,即可 256/256 完全复现,同样用了 828 位数字、同样丢弃 21 次。也就是说,RFC 里两张表可以由同一个生成器、只是换个起始偏移产生。
3. 为什么这个线索被埋没了三十年
2014 年 Rivest 已确认,他的 MD2 哈希也是用同一个 Fisher–Yates 洗牌造出 π 表的。但 MD2 的取数规则是按需变长的:候选位置不超过 10 个时读 1 位,不超过 100 个时读 2 位,再往上才读 3 位。把这套规则直接套到 RC2 上,只有 1/256 个位置吻合,与随机猜测无异——于是最自然的那次验证反而会把人引向“两者无关”的结论。RC2 需要的改动只有一个:每次都读 3 位。
4. 验证强度
- 作者做了一轮约 10 万候选的搜索(4 种数字来源 × 7 种取数方式 × 6 种洗牌顺序 × 300 个起点 × 表与其逆),只有一种完整匹配,且是最简单的那种。π 的数字、e 的数字、改变每次读取位宽等变体都掉回随机水平。
- 去掉丢弃步骤,前 112 次抽样不变,只有 29/256 个位置还能对上。
- 结果被独立复现两次:一次是仅凭 RFC 加一段规则文字重写的第二份实现;一次使用自行计算的 π 和一份全新的 RFC 副本。
- 顺带记录了一个反面案例:2026 年 10 月 17 日,Google 的 AI Overview 在被问到这个问题时给出了一套“π 的十六进制数字两两配对”的方法,其示例
