背包密码学习笔记
前言
背包密码(Knapsack Cryptosystem)是一类把“背包问题”用于加密的公钥密码。是人类历史上第一个公钥密码体系,但是在现代格密码体系下已经被攻破。本文我们将认识背包密码的同时,了解现代格密码体系是如何攻破它的。
背包问题
想象你有一个背包,以及一堆重量各异的物品。问题是:从中挑选哪些物品,能恰好装满背包? 用数学语言说,就是给定一堆数字,问是否存在一个子集,其和恰好等于目标值。
例如,我们有重量如下的物品:
1 | 2, 3, 7, 14 |
你选择其中一些,要求总重量等于某个目标值。例如选择 2、7、14:
1 | 2 + 7 + 14 = 23 |
这个问题看似很简单,但是它已经被证明是一个 NP完全问题,也就是说不存在多项式级别的算法,这使得当数字很多时,暴力尝试所有组合的计算量会呈指数级爆炸,在经典计算机上几乎无法在合理时间内求解。
加密
将明文转换成二进制:
1 | 1101 |
这对应着我们选择了2、3、14,加密结果就是 $2+3+14=19$。
所以背包密码的加密如下:
- 公钥:一串数字,表示可选的物品。
- 明文:转换为二进制,一串 0/1 字符串。
- 密文:选中的数字总和。
别人拿到密文 19 ,需要进行计算背包问题才能得到明文 1101。
解密
读到这里,聪明的你一定发现背包密码的解密是非常复杂的,如果公钥是普通的数字,那么我们解密它就等价于暴力解决一个NP完全问题,这显然是不行的。
但是,早期的背包密码应用了一个特殊的结构:超递增序列。
超递增序列:一个序列是超递增的,如果每一项都大于它前面所有项的总和。例如
{2, 3, 7, 14}就是一个超递增序列。
我们接下来以 {2, 3, 7, 14} 为例,它满足:
1 | 3 > 2 |
这种序列是很容易解密的。对于总和 23,我们只需要从最大的数开始:
- $23 \ge 14$,选择 $14$ 剩下 $9$
- $9 \ge 7$,选择 $7$ 剩下 $2$
- $2 \lt 3$,不选
- $2 = 2$,选上,结束
得到明文:1011
这个过程是一个贪心算法。它的原理是超递增序列的性质,我们以 $23 \ge 14$ 时为例,如果我们不选择 $14$ ,那么剩下的所有数字加起来都没有 $14$ 大,那显然我们就选不出来了。
伪装
超递增序列解决了背包密码解密困难的问题,但是新的问题也出现了:如果直接把超递增序列公布出去,别人也能轻松解密。
为了解决这个新问题,我们需要对超递增序列进行“伪装”,本文以经典的 Merkle-Hellman 背包密码为例:
私钥中保存如下信息:
1 | 超递增序列 w |
其中满足 $gcd(a,m) = 1$,即 $a$ 与 $m$ 互素。
接下来根据私钥生成公钥,将每个私钥数字转化为:
$$b_i = a \times w_i \bmod m$$
这样公钥就不再是明显的超递增序列了。
举个例子:
1 | w = [2, 3, 7, 14] |
得到公钥:
1 | b = [6, 9, 21, 11] |
假设现在明文是 1101,我们选择公钥的 1 2 4 数字,得到密文 $6+9+11=26$。
这样进行加密后由于攻击者不知道模乘参数,无法精确恢复原始的超递增序列,而我们解密时只需要使用 a 的模逆元,就可以恢复超递增序列来贪心求解。
代码实现
1 | import secrets |
格攻击
约定
设背包公开参数为
$$ a_1,a_2,\ldots,a_n, $$
明文对应的比特为
$$ x_1,x_2,\ldots,x_n,\qquad x_i\in{0,1}, $$
密文为
$$ c=\sum_{i=1}^{n}a_i x_i. $$
攻击者知道 $a_i$ 和 $c$,但不知道 $x_i$。
构造格基
选择一个足够大的缩放参数 $M$,构造格向量:
$$ \begin{aligned} \mathbf v_1&=(2,0,\ldots,0,2Ma_1),\ \mathbf v_2&=(0,2,\ldots,0,2Ma_2),\ &\ \vdots\ \mathbf v_n&=(0,0,\ldots,2,2Ma_n). \end{aligned} $$
这里构造时都乘了 2,目的是避免待会儿我们找到希望距离一样近的点出现 $\frac{1}{2}$。
格中的每个点可以表示为:
$$ \mathbf p = \sum_{i=1}^{n}2z_i\mathbf v_i, \qquad z_i\in\mathbb Z. $$
展开得到:
$$ \mathbf p = (2z_1,2z_2,\ldots,2z_n,;2M\sum_{i=1}^{n}a_i z_i). $$
构造目标点
当所有的 $z_i = x_i$ 的时候,这个向量就表示了背包问题的解。
原因是:
$$\sum_{i=1}^{n}a_i x_i=c. $$
所以正确明文会产生一个最后坐标恰好等于 $2Mc$ 的格点。
我们令目标点为:
$$ \mathbf t = \left( 1,1,\ldots,1,;2Mc \right). $$
这里令前面都是 $1$ 的原因是我们希望目标到 $0$ 和 $2$ 的距离一样近,只有这样才能前面无论是 $0$ 还是 $2$ 误差都一样大,才能不让算法偏向于选择 $0$ 或者 $1$。在后面的构造中会讲到。
正确的格点与目标点之差为:
$$ \mathbf p^\ast-\mathbf t = \left( 2x_1-1, 2x_2-1, \ldots, 2x_n-1, 0 \right). $$
由于 $2x_i \in {0,2}$
所以每个坐标都满足:$2x_i - 1 \in {-1,1}$
因此正确答案对应的向量长度为:
$$ \left|\mathbf p^\ast-\mathbf t\right| = \sqrt{ \sum_{i=1}^{n} \left(2x_i-1\right)^2 } = \sqrt {n}. $$
而错误的格点由于:
$$\sum_{i=1}^{n}a_i x_i \ne c. $$
这会导致最后一维的误差会被 $M$ 放大成一个极大的误差,这就得到了以下的结论:
- 正确答案最后一维的误差是 $0$
- 错误答案最后一维误差极大
这可以推出 $p^\ast - t$ 对应的是一个短向量。
将目标点加入格构造
上面我们已经证明了正确格点与目标点的差很短,为了让这个差本身成为格向量,我们将目标点也加入生成向量:
$$b_0 = t = \left( 1,1,\ldots,1,;2Mc \right)$$
于是,新格中包含了这个整数线性组合:
$$\sum_{i=1}^{n}x_iv_i-b_0 = p^\ast-t = (2x_1-1,\ldots,2x_n-1,;0)$$
注意:这里的 $v_i$ 本身已经给 $x_i$ 提供了 $2$ 的系数!
格基规约
我们已经知道了正确的明文会在这个格中产生一个向量 $p^\ast - t$ ,而它等于 $(2x_1-1,\ldots,2x_n-1,;0)$。
因此我们可以将这个格使用 LLL 算法进行格基规约,得到更短的基向量。
因为这个向量 $p^\ast - t$ 是一个短向量,所以它很有可能在格基规约后的格基之中,因此我们在新的格基中找到:
$$\mathbf w=(w_1,\ldots,w_n,0),\qquad w_i\in{-1,1}.$$
这就是我们要找的向量,根据我们刚刚的构造,$w_i = 2x_i -1$,进行移项:
$$x_i = \frac{w_i +1}{2}$$
这样我们就能恢复 $x_i$ 了,接下来就贪心解密即可!
注意:这里 -1 的贡献是因为我们刚刚对整体进行了一个减去 $b_0$ 的操作的原因。







