这个系列是Gatech OMSCS 高级算法课程(CS 6515: Intro to Grad Algorithms)的同步课程笔记。课程内容涉及动态规划、随机算法、分治、图算法、最大流、线性规划以及NP完备等高级算法的设计思想和相关应用,本节主要介绍随机算法。
随机算法在密码学中有着非常重要的应用。
Modular Arithmetic
在介绍随机算法与密码学的关系前我们需要先来引入模算数(modular arithmetic)的相关概念。对于整数$x$我们定义取模运算mod来获得它关于$N$的余数,如果两个整数$x$和$y$具有相同的余数则称它们关于$N$同余(congruence):
以除数3为例,通过取模运算可以定义3个同余等价类。
Basic Fact
同余的一个基本性质是如果$x$和$y$以及$a$和$b$分别关于$N$同余等价,则$x+a$和$y+b$同余等价且$xa$和$yb$同余等价。
Modular Exp
模算数的一个重要应用是计算指数的余数。对于n-bit整数$x$和$y$,我们可以通过递推的方法来计算$x^y$的余数:
上述算法的复杂度依赖于$y$的大小。更高效的算法是利用平方运算来减少迭代步骤:
Multiplicative Inverse
我们定义mod运算的逆为满足$x z \equiv 1 \ \text{mod} \ N$的整数,记为$x \equiv z^{-1} \ \text{mod} \ N$。
需要注意的是并不是所有的整数都存在逆。
实际上当且仅当$x$和$N$的最大公约数(greatest common divisor, GCD)为1时才存在逆。
GCD
因此计算mod的逆就归结于计算$x$和$N$的最大公约数。关于最大公约数的研究可以追溯到古希腊的欧几里得,他指出$x$和$y$的最大公约数等于$(x \ \text{mod} \ y)$和$y$的最大公约数:
基于欧几里得定理可以得到计算最大公约数的算法,其复杂度为$O(n^3)$:
Euclid(x, y):
if y == 0:
return x
else:
return Euclid(y, x mod y)
这样就可以计算mod的逆,其复杂度为$O(n^3)$:
Ext-Euclid(x, y):
if y == 0:
return (x, 1, 0)
else:
d, alpha', beta' = Ext-Euclid(y, x mod y)
return (d, beta', alpha' - x//y * beta')
RSA
Fermat’s Little Theorem
在模算数的基础上就可以介绍密码学中重要的RSA算法了,不过在此之前我们还需要引入费马小定理(Fermat’s little theorem)。它指出对于任意质数$p$和小于$p$的整数$z$,$z^{p-1}$与1关于$p$同余:
Euler’s Theorem
欧拉定理(Euler’s theorem)是对费马小定理的推广,它指出互质的整数$N$和$z$,$z^{\phi(N)}$与1关于$N$同余,其中$\phi(N)$为小于$N$且与$N$互质的整数数量。
如果我们进一步令$N$为两个质数$p$和$q$的积$N=pq$,可以证明$\phi(N)=(p-1)(q-1)$。这样有$z^{(p-1)(q-1)}$与1关于$N$同余。
RSA Idea
RSA算法的基本思想是利用欧拉定理来对$z$进行加密。我们取两个质数$p$和$q$并计算它们的积$N=pq$,接下来取关于$(p-1)(q-1)$互逆的一对整数$d$和$e$。进行加密时加密方已知$e$和$N$,这样可以计算$z^e$;而在解密时解密方知道$e$,利用$z^{de}$就可以恢复$z$。
Cryptography Setting
在密码学中最常见的一类场景是公开密钥加密(public-key cryptography)。此时数据发送方通过加密函数e()
对信息进行加密,而接收方通过解密函数d()
进行解密,它们之间的直接通信都是加密后的消息e(m)
。同时接收方会公开公钥(public key),这样任何拥有公钥的人都可以加密,但只有数据接收方能够利用私钥(private key)进行解密。
RSA Protocols
因此数据接收方的任务在于计算公钥和私钥,然后公开公钥并保留私钥用来解密。为此它需要首先选择两个质数$p$和$q$,然后计算与$(p-1)(q-1)$互质的$e$用来加密,这样就得到了公钥$N=pq$和$e$,最后计算并保存私钥$d$即可。
对于数据发送方来说,它只需要先获取公钥$N$和$e$,然后对消息$m$进行加密$y \equiv m^e \ \text{mod} \ N$,最后发送加密后的消息$y$即可。
而当接收方需要进行解密时只需要利用私钥计算$y^d \ \text{mod} \ N = m$。
RSA Pitfalls
使用RSA进行加密时需要注意的是保证原始数据$m$和$N=pq$是互质的,而如果它们存在公约数则容易导致一些问题。假设$m$和$N$存在公约数$p$,对于数据接收方仍然可以正确地进行解密,但是任何截获加密数据$m^e$的人都可以利用gcd(y, N)
来破解$p$进而获得私钥。因此在进行加密时需要注意验证$m$和$N$是否是互质。
另一方面我们还希望原始数据$m$既不是特别大也不是特别小的数字,当$m$特别大或者特别小时都容易导致加密失效。
RSA Recap
总结一下目前为止的内容,整个RSA算法的流程如下:
Random Prime Numbers
整个RSA算法依赖于选取质数$p$和$q$,因此我们需要设计随机质数的生成算法。要生成随机质数我们首先需要生成随机n-bit整数,这个过程比较简单只需要对n-bit随机赋予0或者1即可。然后我们验证这个随机整数是否是一个质数,如果它是质数就得到了一个随机质数,否则重新生成一个新的随机整数再进行判断即可。可以证明n-bit随机整数恰为质数的概率约为$\frac{1}{n}$,因此理论上我们只需要重复生成n次随机整数就可以得到一个随机质数。
要验证一个整数是否是质数则需要费马素性检验(Fermat primality test)。根据费马小定理,任意小于质数$r$的整数$z$一定有$z^{r-1} \equiv 1 \ \text{mod} \ r$。因此如果我们能够找到一个小于$r$的整数$z$使得$z^{r-1} \not\equiv 1 \ \text{mod} \ r$,那么$r$一定是一个合数,这样的$z$称为Fermat witness。
可以证明任意合数都至少存在一个Fermat witness。
因此合数的因数一定是Fermat witness,称为trivial Fermat witness。而实际上Fermat witness不一定都是因数,此时则称其为non-trivial Fermat witness。只有trivial Fermat witness的合数称为Carmichael数(Carmichael number),在使用费马素性检验时它们的性质非常接近于质数,因此也被称为伪质数(pseudo primes)。不过Carmichael数非常少见,在简单情况下我们可以忽略它们的存在。
如果我们忽略Carmichael数的存在可以得到质数的检验算法:
Bloom Filters
Ball into Bins
假设我们有$n$个完全相同的小球与$n$个盒子,每次随机将一个小球置入其中一个盒子中,此时第$i$个盒子中的小球数量是一个随机变量$\text{load}(i)$。记盒子中数目最多的小球数为$\max \ \text{load}$,它同样是一个随机变量且最大值为$n$,即所有小球同时落入一个盒子中。不过这种情况的概率很低,为$(\frac{1}{n})^n$。
要分析$\max \ \text{load}$的分布并不容易。首先,前$\log (n)$个小球都落在第$i$个盒子的概率为$(\frac{1}{n})^{\log n}$。
在此基础上可以证明第$i$个盒子中小球数量至少为$\log (n)$的概率小于等于${n \choose \log n} (\frac{1}{n})^{\log n}$。
当$n$比较大时组合数具有上界:
\[{n \choose k} \approx \bigg( \frac{n}{k} \bigg)^k \leq \bigg( \frac{n \ e}{k} \bigg)^k\]带入概率上界可以得到:
\[P \leq \bigg( \frac{n \ e}{\log n} \bigg)^{\log n} \bigg( \frac{1}{n} \bigg)^{\log n} = \bigg( \frac{e}{\log n} \bigg)^{\log n}\]由于$\frac{e}{\log n}$随$n$的增加而减小,我们可以进一步使用常数$\frac{1}{4}$进行放缩。进一步假设$n > 2^{11}$可以得到最终的概率上界$\frac{1}{n^2}$。
利用上面的结论可以推导出$\max \ \text{load}$至少为$\log n$的概率上界为$\frac{1}{n}$,而它的下界有大概率为$\frac{\log n}{\log \log n}$。
除了随机放置小球外,另一种放置小球的方式是每次随机选择两个盒子然后把小球放到$\text{load}$较小的那个盒子中。
可以证明按照这种方式进行放置时$\max \ \text{load}$的上界有大概率为$\log \log n$。
Chain Hashing
Hashing在现实生活中有着非常多的应用,比如说可以使用hashing作为容器来保存数据,此时我们需要回答的问题是容器中是否存在某个给定的元素$x$。最常见的实现方法是构造一个数组,其中每个位置对应一个链表。然后设计一个函数$h(x)$将可能的元素$x$映射到链表的编号。这样的实现方式称为chain hashing。假设$h(x)$是一个随机函数,我们可以把链表想象成小球问题中的盒子,而元素$x$则为小球,这样就可以使用上一节的结论来进行分析。
对于chain hashing这种数据结构,检测$x$是否在容器中需要两步:
- 使用$h(x)$计算链表的编号$h(x) = i$。
- 检测$x$是否在链表$H[i]$中。
记$x$的所有可能取值数量为$m$,链表的数目为$n$。利用上一小节的结论可以得到每个链表的大小有大概率为$\log n$,即查询的复杂度为$O(\log n)$。
要提升查询的效率可以使用两个$h(x)$函数,每次添加元素时选择$\text{load}$较少的那个链表来加入。需要注意的是在进行查询时需要同时检查两个链表中的元素。可以证明此时的查询复杂度为$O(\log \log n)$。
Bloom Filter
Bloom滤波器(Bloom filter)是对chain hashing的一种改进,它可以实现$O(1)$的查询效率而且实现起来非常简单。不过Bloom滤波器仅是概率正确的,存在假阳性的可能性。对于精度没有特别严格要求的场景,Bloom滤波器是一个非常强大的工具。
Bloom滤波器包括两个基本操作,插入insert(x)
以及查询query(x)
。
在实现时Bloom滤波器使用了一个大小为n的二进制数组。首先把数组的每一位都初始化为0,当我们需要插入$x$时将数组的第$h(x)$位设置为1,而在查询时只需要检测$H[h(x)]$是否为1即可。显然此时两种操作都只需要$O(1)$的复杂度,但查询操作可能会出现假阳性的情况。
要提升系统的鲁棒性可以同时使用k个hashing函数。此时要进行插入和查询则需要同时检查数组的k个可能位置,当数组的k个位置上都为1时说明$x$在容器中。
显然k值对于假阳性的概率有着重要的影响,可以证明出现假阳性的概率约为$(1-e^{-\frac{k}{c}})^k$,这样最优的k值为$c \ln 2$而对应的假阳性概率约为$0.6185^c$。