【Coel.解题报告】【密码学高手】[CQOI2018]破解D-H协议

题前闲话

最近做了好几道\(BSGS\)题,结果发现思路都差不多,都是推式子-发现是高次同余方程-跑\(BSGS\)的套路……

题目描述

洛谷传送门:[CQOI018]破解D-H协议
Loj 传送门:[CQOI018]破解D-H协议

题目背景

Diffie-Hellman密钥交换协议是一种简单有效的密钥交换方法。它可以让通讯双方在没有事先约定密钥(密码) 的情况下,通过不安全的信道(可能被窃听) 建立一个安全的密钥K,用于加密之后的通讯内容。

题目描述

假定通讯双方名为Alice和Bob,协议的工作过程描述如下(其中 mod 表示取模运算)

  1. 协议规定一个固定的质数P,以及模P 的一个原根g。P 和g 的数值都是公开的,无需保密。

  2. Alice 生成一个随机数a,并计算\(A=g^a\;mod\;P\), 将A 通过不安全信道发送给Bob。

  3. Bob 生成一个随机数b,并计算\(B=g^b\;mod\;P\),将B 通过不安全信道发送给Alice。

  4. Bob 根据收到的A 计算出\(K=A^b\;mod\;P\),而Alice 根据收到的B 计算出\(K=B^a\;mod\;P\)

  5. 双方得到了相同的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\),求解

\[K = A^b \mod P = B^a \mod 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\)好累啊!

posted @ 2022-01-09 13:10  秋泉こあい  阅读(31)  评论(0编辑  收藏  举报