文章

不可破解的一次一密

介绍了一次一密(One-Time Pad, OTP)的原理、历史和安全性

不可破解的一次一密

epigraph “我已经把这件事反复想了一千遍,”沃特豪斯说,“我唯一能想到的解释是,他们把信息转换成巨大的二进制数字,然后再和另外一些巨大的二进制数字结合——很可能是一次一密。”

“如果是这样的话,你的项目就完蛋了,”艾伦说,“因为一次一密是无法破解的。””

—— Neal Stephenson,《编码宝典》

密码学中,不少的加密算法在计算复杂度的保护下难以破解:

  • RSA 依赖大整数分解的困难性
  • AES 依赖密钥空间的庞大

而它们的安全性都只是“目前没人能在合理时间内破解”。如果哪天量子计算机量产了或者某位天才证明了 $P=NP$,这些算法就会变得不安全。

但有一种加密方案,它不管任何计算困难性,其安全性是经过数学证明的——即使攻击者有 $\infty$ 的计算能力,也不能从密文中还原出任何明文信息。

它就是一次一密!(One-Time Pad,也被翻译为“一次性密码本”)

A Famous Story?

假设你和你的朋友小明要在机房里传纸条,但是机房里还有一个很烦人的人叫小刚,他会偷看你们的纸条。

最朴素的想法:把每个字母往后移 3 位,比如 A 变成 D,B 变成 E。这就是凯撒密码。

但小刚不傻,他数了一下字母频率,发现出现最多的那个字母八成就是 E(英文中 E 的出现频率最高),于是轻松破解了你的密码。

(是的,你大概会发现以这篇故事是我用网上的故事改的……)

网上的一大堆介绍非对称加密的文章,你大概会看到下面这个故事:

你把一个盒子和一把锁寄给小明,小明把盒子锁上寄回给你,你再用钥匙打开盒子取出里面的东西。 小刚就偷看不了力! (THE END)

这个故事虽然形象,但它其实是对非对称加密的一个过于简化的比喻。非对称加密的核心在于公钥和私钥,而不是物理锁和钥匙。更重要的是,非对称加密的安全性依赖于数学问题的计算困难性,而不是不可能。

那么,如果我们想要一个真正意义上无法破解的加密方案,我们需要什么样的设计呢?

那怎么办?你灵机一动:如果每个字母用不同的偏移量来加密呢?而且这些偏移量是完全随机的、只用一次?

恭喜你,你刚刚发明了一次一密。

怎么做?

一次一密的操作极其简单。我们用最常见的 XOR(异或,$\oplus$)版本来讲解。

首先,异或运算有几个性质:

  • $a \oplus 0 = a$
  • $a \oplus a = 0$
  • $a \oplus b = b \oplus a$
  • $(a \oplus b) \oplus c = a \oplus (b \oplus c)$
  • $a \oplus b \oplus b = a \oplus (b \oplus b) = a \oplus 0 = a$

假设明文为 $M$,密钥为 $K$,密文为 $C$,那么:

  • 加密:$C = M \oplus K$
  • 解密:$M = C \oplus K$

其中 $\oplus$ 是按位异或运算。

举个例子,假设明文是 OI,我们先把它转成 ASCII:

字符ASCII (十进制)二进制
O7901001111
I7301001001

现在我们生成一个真随机的密钥(长度和明文一样):

密钥字节十进制二进制
$K_1$4200101010
$K_2$21711011001

加密过程就是逐位异或:

\[\begin{aligned} C_1 &= \texttt{01001111} \oplus \texttt{00101010} = \texttt{01100101} = 101 \\ C_2 &= \texttt{01001001} \oplus \texttt{11011001} = \texttt{10010000} = 144 \end{aligned}\]

所以密文是 {101, 144},看起来和原文毫无关系。

解密时,对方拿着同样的密钥,再异或一次:

\[\begin{aligned} M_1 &= \texttt{01100101} \oplus \texttt{00101010} = \texttt{01001111} = 79 = \texttt{O} \\ M_2 &= \texttt{10010000} \oplus \texttt{11011001} = \texttt{01001001} = 73 = \texttt{I} \end{aligned}\]

完美还原了!

这就是 XOR 自反性的好处:$a \oplus b \oplus b = a$。

用代码来写就更直白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <random>
#include <chrono>
// 需要 C++ 11 以上
using namespace std;

int main()
{
    string msg = "Hello, OI!";
    int n = msg.size();

    // 生成随机密钥
    mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); // 其实是伪随机,但为了演示方便
    vector<uint8_t> key(n);
    for (auto& k : key) k = rng() % 256;

    // 加密
    string cipher(n, 0);
    for (int i = 0; i < n; i++)
        cipher[i] = msg[i] ^ key[i];

    // 解密
    string plain(n, 0);
    for (int i = 0; i < n; i++)
        plain[i] = cipher[i] ^ key[i];

    cout << "明文: " << msg << "\n";
    cout << "密文(hex): ";
    for (int i = 0; i < n; i++) printf("%02x ", (uint8_t)cipher[i]);
    cout << "\n";
    cout << "解密: " << plain << "\n";

    return 0;
}

运行:

1
2
3
明文: Hello, OI!
密文(hex): f0 e8 3b 86 7e ff a6 6c cf 2e
解密: Hello, OI!

三条铁律

一次一密要想做到“无法破解”,必须满足三个条件:

  1. 密钥必须是真随机的。不能用伪随机生成器(上面代码里的 mt19937 严格来说不合格,只是为了演示)。

  2. 密钥长度必须等于明文长度。一个字节的明文对应一个字节的密钥,不能少。

  3. 密钥只能使用一次。“一次一密”的名字就是这么来的。密钥用完就销毁,下次通信必须换一把全新的密钥。

看起来都是废话?但每一条都是致命的,违反任何一条都会让整个系统瞬间崩塌。后面我们会讲为什么。

为什么不可破解?

1949 年,信息论之父 Claude Shannon 在他的论文 Communication Theory of Secrecy Systems 中,严格证明了一次一密具有完美保密性(Perfect Secrecy)

完美保密性的定义是:对于任意明文 $m$ 和任意密文 $c$,有

\[P(M = m \mid C = c) = P(M = m)\]

看到密文之后,你对明文的猜测不会比看到密文之前更准确。也就是密文没有泄露关于明文的任何信息。

直觉上这很好理解。因为密钥是完全随机的,对于同一段密文 $c$,任何一段等长的明文都有可能产生它。具体来说,对于任意明文 $m$,总存在唯一的密钥 $k = m \oplus c$ 使得 $m$ 加密后恰好得到 $c$。而由于所有密钥等概率出现,所有明文从密文反推也就是等概率的。

攻击者拿到密文 {101, 144} 之后?

  • 如果密钥是 {42, 217},明文就是 OI
  • 如果密钥是 {55, 232},明文就是 NO
  • 如果密钥是 {81, 241},明文就是 21

每种可能性都是等概率的。攻击者完全不知道原文到底是什么,哪怕他打表暴搜尝试了所有密钥也没用——因为他会得到所有可能的明文,却无法判断哪个是正确的。

这就是一次一密和其他加密算法最本质的区别:它的安全性不是计算上的困难,而是信息论意义上的不可能。

数学证明

[!NOTE] 本段内容由 DeepSeek-V3.1 Thinking 模型生成。

我们简单证明一下。设明文空间、密文空间和密钥空间都是 ${0,1}^n$,密钥 $K$ 在 ${0,1}^n$ 上均匀分布。

\[\begin{aligned} P(C=c \mid M=m) &= P(M \oplus K = c \mid M = m) \\ &= P(K = m \oplus c) \\ &= 2^{-n} \end{aligned}\]

对所有 $m$ 的条件概率都是 $2^{-n}$。由贝叶斯公式:

\[P(M=m \mid C=c) = \frac{P(C=c \mid M=m) \cdot P(M=m)}{P(C=c)}\]

而 $P(C=c) = \sum_{m’} P(C=c \mid M=m’) \cdot P(M=m’) = 2^{-n} \sum_{m’} P(M=m’) = 2^{-n}$。

所以 $P(M=m \mid C=c) = \frac{2^{-n} \cdot P(M=m)}{2^{-n}} = P(M=m)$。

铁律不可违

密钥复用

冷战时期,苏联情报机构使用一次一密和美国的人员通信。(理论上绝对安全,美国肯定不知道怎么办!)但问题是:苏联密码部门因为战时密钥不够用,把部分密钥页重复使用了

美国的 Venona 计划 利用了这个致命错误。如果同一个密钥 $K$ 被用来加密两段不同的明文 $M_1$ 和 $M_2$:

\[C_1 \oplus C_2 = (M_1 \oplus K) \oplus (M_2 \oplus K) = M_1 \oplus M_2\]

密钥被抵消了!

直接得到了两段明文的异或,这下通过语言的统计特性和已知明文攻击,可以还原出两段消息了!

Venona 计划从 1943 年持续到 1980 年,成功破解了大量苏联谍报通信,揭露了多名苏联间谍。

不够随机

二战中德国使用的 Lorenz SZ42 密码机其实就是自动一次一密的尝试,它用一组旋转的轮子来生成伪随机密钥流。问题在于轮子的转动规则是可预测的,所以本质上密钥并不是真随机的。英国密码学家 Bill Tutte 和 Tommy Flowers 正是利用了密钥流中的统计规律,用世界上第一台电子计算机 Colossus 成功破解了它。

这两个历史案例告诉我们:一次一密的三条铁律不能违反,否则安全性就没了。

那为什么大家不全用一次一密?

既然一次一密不可破解,为什么世界上的加密通信没有全部使用它?

它太不实用了!

  1. 密钥分发问题。你想发一条 1 GB 的文件,就得先安全传递 1 GB 的密钥?

  2. 密钥存储问题。你和 100 个人通信,每个人都需要单独的密钥。如果每天通信 1 MB,一年下来光密钥就要存 $100 \times 365 \times 1\text{ MB} \approx 36\text{ GB}$。还要保证这些密钥的物理安全,简直是后勤噩梦。

  3. 不支持重放。密钥用完就得扔,不能重新验证历史消息的真伪。

  4. 没有认证。一次一密只保证保密性,不保证完整性。攻击者虽然不知道明文内容,但可以对密文做手脚。由于 OTP 并不像非对称加密一样可以进行签名验证,所以攻击者可以篡改密文,导致解密后的明文被破坏,而接收方无法检测到。

所以在实际应用中,AES、ChaCha20 这些对称加密算法虽然理论上不具备完美保密性,但它们在目前的计算能力下还是安全的,并且密钥短小、易于管理。

总结

简而言之,一次一密是一种通过使用与消息长度相同、真正随机且只使用一次的密钥,而加密的密码。当满足这三个条件时,它就无法破解。

然而,由于使用极其不便,它并不适合被用于日常加密。通常,需要的时候,OTP 密钥会在线下分发一份密钥清单,清单包含一段时间内的 OTP 密钥。但要确保清单不会被泄露!

一次一密就像理想模型——它不切实际,但它定义了完美。一次一密已经是完美保密的下界了——你不可能用更短的密钥达到同样的安全水平。

所以,下次当你听到某个加密算法号称“没法被破解”时,不妨想想一次一密。在这个世界上,真正无法破解的,只有它。

参考资料:

本文由作者按照 CC BY 4.0 进行授权