實驗內容
【時間】5 月 30 號(周六)晚上
【編程語言】Python(推薦)或者 C/C++
【實驗目的】
- 掌握與密碼學相關的基礎數論知識;
- 通過使用 Python(推薦)或者 C,利用中間相遇攻擊來編程實現離散對數的求解。
【實驗內容】
實驗內容和要求請見附件 EXP2-Doc.pdf
實驗准備
本實驗擬采用 C++ 作為編程語言,而實驗需要支持多精度和模運算的環境,因此需要配置 GNU 多重精度運算庫(GNU Multiple Precision Arithmetic Library,簡稱 GMP 或 gmpal)。
Windows 下配置 GMP
在 Windows 上安裝 C++ 編譯器和 GMP 比較方便的方法是通過 MinGW Installer 安裝,MinGW 是 Windows 版本的 GCC 和 GNU Binutils。
MinGW Installer 下載地址:https://osdn.net/projects/mingw/downloads/68260/mingw-get-setup.exe
接着開始安裝。若網絡連接失敗,可以使用學校提供的 VPN 或開啟科學上網后再安裝;若網絡沒有問題仍無法安裝,可能是 MinGW Installer 的版本不是最新。
安裝完成后,可以看到 MinGW Installer Manager 已經打開。
接着,在 Basic Setup 中勾選 mingw32-base-bin 和 mingw32-gcc-g++-bin,在 MinGW Libraries 中勾選 mingw32-gmp-dev、mingw32-gmp-dev-info 和 mingw32-gmp-dev-lic 並 Apply Changes,等待安裝完成。
安裝完成后設置環境變量:
接着測試以下代碼:
#include <gmp.h>
int main()
{
mpz_t a, b;
mpz_init(a);
mpz_init(b);
mpz_set_ui(a, 100);
mpz_set_ui(b, 99);
mpz_t c;
mpz_init(c);
mpz_add(c, a, b);
gmp_printf("%Zd\n", c);
return 0;
}
在命令行中編譯並鏈接,g++ gmpTest.cpp -lgmp -lm -o gmpTest
,再執行 exe 文件,可以看到如下輸出:
頭文件
gmp.h
支持純 C 環境。而對於 C++ 來說,可以調用gmpxx.h
,它封裝了mpz_class
類,並在mpz_class
中重載了一些運算符,使用更加方便,代碼如下:#include <gmpxx.h> #include <iostream> using namespace std; int main() { mpz_class a("100"); mpz_class b("99"); cout << a + b << endl; return 0; }
如果在 IDE 中運行代碼,記得在鏈接器(Linker)中添加 GMP 的庫文件
libgmp.dll.a;libgmpxx.dll.a
,以我所使用的 CodeLite 為例:
添加后,即可運行。
GMP 的安裝參考了 👉 這篇文章
實驗分析
實驗要求
從附件 EXP2-Doc.pdf 中可知。此次實驗需要寫一個程序來計算模素數 \(p\) 的離散對數。
其中 \(p\) 是一個素數,\(g\) 是有限乘法群 \(\mathbb{Z}_{p}^{*}\) 上的一個原根,然后給定一個 \(\mathbb{Z}_{p}^{*}\) 上的 \(h\) 滿足 \(h = g^x\),其中 \(1 \leq x \leq 2^{40}\),目的是找到 \(x\)。也就是說,編寫程序,以 \(p\),\(g\),\(h\) 作為輸入,然后輸出 \(x\)。
test.txt 中提供了三個輸入 \(p\),\(g\),\(h\),該問題最直接的算法就是對 \(x\) 的可能值逐個進行嘗試,由於 \(g\) 是原根,\(x\) 的可能值會有 \(p\) 個,而本實驗規定 \(x\) 的范圍為 \(1 \leq x \leq 2^{40}\),降低了求解難度(\(p\) 的值遠大於 \(2^{40}\))。
我們知道,上述的暴力法需要 \(2^{40}\) 次乘法運算。本實驗要求在此基礎上使用中間相遇攻擊,將時間代價減少到約為 \(\sqrt{2^{40}}=2^{20}\)。
思路
按照附件中提供的思路,我們令 \(B = \lceil\sqrt{x}\rceil = 2^{20}\),\(x = x_0B + x_1\),其中 \(x_0, x_1 \in [0, B)\)。可以得到:
兩邊同時乘以 \(g^{x_1}\) 的逆元,可得到:
通過上式,我們發現可以使用中間相遇攻擊來找到一個解:
- 為等式右邊 \(hg^{-x_1}\) 的所有可能值創建一個哈希表,其中 \(x_1 = 0, 1,..., 2^{20}\)
- 對於每個 \(x_0 = 0, 1,..., 2^{20}\),檢查 \((g^B)^{x_0}\) 是否在哈希表中,如果是,便找到了解 \(x_0\)、\(x_1\),即 \(x = x_0B + x_1\)
為等式右邊 \(hg^{-x_1}\) 的可能值創建哈希表時,涉及到求逆運算。可以使用 gmp 中的
int mpz_invert(mpz_t rop, const mpz_t op1, const mpz_t op2)
,其中rop
為運算結果,op1
為 \(g^{x_1}\),op2
為 \(p\)。代碼如下:unordered_map<string, int> hashMap; for(int i = 0; i < B; i++) { mpz_class x_1 = i; mpz_class powm; mpz_powm(powm.get_mpz_t(), g.get_mpz_t(), x_1.get_mpz_t(), p.get_mpz_t()); mpz_class inv; mpz_invert(inv.get_mpz_t(), powm.get_mpz_t(), p.get_mpz_t()); mpz_class key = h * inv % p; hashMap[key.get_str()] = i; }
以上方法中,創建哈希表的時間代價為 \(\sqrt{x}\),哈希表查找的時間復雜度為 \(O(1)\)。
優化
這篇文章中提到了代碼實現時的一個可優化的點。
我們令 \(x = x_0B - x_1\),其中 \(x_0 \in (0, B]\)、\(x_1 \in [0, B)\),化簡等式 \(h = g^x\) 可得到:
這樣,將等式右邊 \(hg^{x_1}\) 的所有可能值存入哈希表時,就無需在每個循環內都進行冪運算和求逆運算(每個循環內僅有一次乘法運算和模運算)。代碼如下:
unordered_map<string, int> hashMap;
// x1 = 0, 1, 2...。將等式右邊的結果 h * g^x1 % p 存入哈希表,key = h * g^x1 % p,value = x1
mpz_class productRight = h;
for(int i = 0; i < B; i++)
{
hashMap[productRight.get_str()] = i;
productRight = productRight * g % p;
}
實驗思路參考了附件 EXP2-Doc.pdf 以及 👉 這篇文章
代碼
結合上述思路及優化,給出如下代碼:
#include <gmpxx.h>
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
mpz_class p("134078079299425970995740249982058461274793658205923933777235614437217640300735469768018742981669034276"
"90031858186486050853753882811946569946433649006084171");
mpz_class g("117178298803662070095161175963353670885580849999989522055999794590639294997365837466705721764714603129"
"28594829675428279466566527115212748467589894601965568");
mpz_class h("323947510405045044356526437872806578864909752095244952783479245297198197614329255807385693795855318053"
"2878928001494706097394108577585732452307673444020333");
mpz_class B("1048576");
void solve2()
{
mpz_class x0("0"), x1("0"), x("-1");
unordered_map<string, int> hashMap;
// x1 = 0, 1, 2...。將等式右邊的結果 h * g^x1 % p 存入哈希表,key = h * g^x1 % p,value = x1
mpz_class productRight = h;
for(int i = 0; i < B; i++)
{
hashMap[productRight.get_str()] = i;
productRight = productRight * g % p;
}
cout << "----- Without inverse -----" << endl << "Hash map saved!" << endl;
// 等式左邊的底數 baseLeft = g^B % p
mpz_class baseLeft;
// 冪取模運算
mpz_powm(baseLeft.get_mpz_t(), g.get_mpz_t(), B.get_mpz_t(), p.get_mpz_t());
// for(int i = 0; i < B; i++)
// {
// baseLeft = baseLeft * g % p;
// }
// x0 = 0, 1, 2...。判斷等式左邊的結果 (g^B % p)^x0 % p 是否在哈希表中
mpz_class productLeft("1");
for(int i = 1; i <= B; i++)
{
productLeft = productLeft * baseLeft % p;
if(hashMap.find(productLeft.get_str()) != hashMap.end())
{
x0 = i;
x1 = hashMap[productLeft.get_str()];
break;
}
}
x = B * x0 - x1;
if(x == -1)
{
cout << "There is no solution!" << endl;
return;
}
cout << "x0:\t" << x0 << endl;
cout << "x1:\t" << x1 << endl;
cout << "x:\t" << x << endl;
return;
}
int main()
{
solve2();
return 0;
}
👉 兩種方法的完整代碼
實驗結果
運行完整代碼,輸出:
----- With inverse -----
Hash map saved!
x0: 357984
x1: 787046
x: 375374217830
----- Without inverse -----
Hash map saved!
x0: 357985
x1: 261530
x: 375374217830
可得到:\(x = 375374217830\)