【Coel.解题报告】【密码学高手】[CQOI2018]破解D-H协议
题前闲话
最近做了好几道\(BSGS\)题,结果发现思路都差不多,都是推式子-发现是高次同余方程-跑\(BSGS\)的套路……
题目描述
洛谷传送门:[CQOI018]破解D-H协议
Loj 传送门:[CQOI018]破解D-H协议
题目背景
Diffie-Hellman密钥交换协议是一种简单有效的密钥交换方法。它可以让通讯双方在没有事先约定密钥(密码) 的情况下,通过不安全的信道(可能被窃听) 建立一个安全的密钥K,用于加密之后的通讯内容。
题目描述
假定通讯双方名为Alice和Bob,协议的工作过程描述如下(其中 mod 表示取模运算)
-
协议规定一个固定的质数P,以及模P 的一个原根g。P 和g 的数值都是公开的,无需保密。
-
Alice 生成一个随机数a,并计算\(A=g^a\;mod\;P\), 将A 通过不安全信道发送给Bob。
-
Bob 生成一个随机数b,并计算\(B=g^b\;mod\;P\),将B 通过不安全信道发送给Alice。
-
Bob 根据收到的A 计算出\(K=A^b\;mod\;P\),而Alice 根据收到的B 计算出\(K=B^a\;mod\;P\)。
-
双方得到了相同的K,即\(g^{ab}\;mod\;P\)。K 可以用于之后通讯的加密密钥。
可见,这个过程中可能被窃听的只有A、B,而a、b、K 是保密的。并且根据A、B、P、g 这4个数,不能轻易计算出K,因此K 可以作为一个安全的密钥。
当然安全是相对的,该协议的安全性取决于数值的大小,通常a、b、P 都选取数百位以上的大整数以避免被破解。然而如果Alice 和Bob 编程时偷懒,为了避免实现大数运算,选择的数值都小于\(2^{31}\),那么破解他们的密钥就比较容易了。
输入格式
输入文件第一行包含两个空格分开的正整数g和P。
第二行为一个正整数n, 表示Alice和Bob共进行了n次连接(即运行了n次协议)。
接下来n 行,每行包含两个空格分开的正整数A 和B,表示某次连接中,被窃听的A、B 数值。
输出格式
输出包含n行,每行1个正整数K,为每次连接你破解得到的密钥。
解题过程
更形象地描述一下题面:
已知\(P,g\)(其中\(P\)为质数,\(g\)为模\(P\)的一个原根),且对于每一个已知的\(A,B\),都有
\(g^a \equiv A \pmod P\)和\(g^b \equiv B \pmod P\),求解
很显然,我们只需要求出\(b\)或者\(a\)的值,并套用快速幂即可求出\(K\)。
考虑到\(g\)为\(P\)的一个原根,所以它们必定互质。
这里解释一下什么是原根(不想看的只要知道\(g\)和\(P\)互质就好了):
首先我们得知道什么是阶:
由欧拉定理可以知道,当\(gcd (a,m) = 1\)的时候,有\(a^{\varphi (m)} \equiv 1 \pmod m\)。
那么,满足\(a^n \equiv 1 \pmod m\)的最小正整数\(n\)就是\(a\)模\(m\)的阶,记作\(\delta_m(a)\)。
当\(\delta_m(a)=\varphi(m)\),也就是说满足\(a^n \equiv 1 \pmod m\)的\(n\)的最小正整数是\(\varphi(m)\)时,\(a\)是模\(m\)的原根。
原根和阶有很多非常复杂的性质,这里就不展开介绍了,有兴趣的可以看看OI-Wiki的介绍。
总之,根据原根的定义可以知道,当\(g\)为\(P\)的一个原根时,必定有\(gcd(g,P)=1\),因此\(g\)和\(P\)必定互质。
(说了这么多其实就是想告诉你不需要用\(ExBSGS\))
用普通的\(BSGS\)求出\(a\)或者\(b\)的值,然后快速幂解决。
代码如下:
#include <cstdio>
#include <cstring>
#include <cctype>
#include <cmath>
#include <unordered_map>
typedef long long ll;
inline ll read() {
ll x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline ll qmul(ll a, ll b, ll p) {//这里用到了一个O(1)的快速乘取模
ll left = a * (b >> 25LL) % p * (1LL << 25) % p;
ll right = a * (b & ((1LL << 25) - 1)) % p;
return (left + right) % p;
}
inline ll qpow(ll a, ll b, ll p) {
ll ans = 1;
while (b) {
if (b % 2 == 1)
ans = qmul(ans, a, p);
a = qmul(a, a, p);
b >>= 1;
}
return ans;
}
inline ll bsgs(ll a, ll b, ll p) {
std::unordered_map<ll, ll> hso;//c++11里hash关键字被占用了,只好换一个
ll m = ceil(sqrt(p)), mul = b % p, tp = 1;
hso[mul] = 1;
for (int i = 1; i <= m; i++) {
mul = qmul(mul, a, p);
hso[mul] = i + 1;
}
mul = qpow(a, m, p);
for (int i = 1; i <= m; i++) {
tp = qmul(tp, mul, p);
if (hso.find(tp) != hso.end())
return ((i * m - hso[tp] + 1) % p + p) % p;
}
return -1;
}
int main() {
ll g = read(), p = read(), n = read();
while (n--) {
ll A = read(), B = read();
ll a = bsgs(g, A, p);//求出a的值
printf("%lld\n", qpow(B, a, p));//快速幂求出K
}
return 0;
}
关于那个\(O(1)\)的快速乘取模,在这个链接里面有详细介绍。
但缺点就是数字不能太大(大概int以内),否则会溢出。
题后碎语
敲\(\LaTeX\)好累啊!