實驗內容
【時間】 暫定 5 月 23 號(周六)晚上
【編程語言】 Python(推薦)或者 C/C++
【實驗目的】
- 了解流密碼的結構特點;
- 掌握 One-time Pad 的一般具體實現;
- 通過使用 Python(推薦)或者 C,編程實現一個流密碼加密示例的破解,進一步認識在流密碼加密中多次使用相同密鑰導致的問題。
【實驗內容】
在掌握流密碼結構的基礎上,通過本實驗觀察使用相同流密碼密鑰加密多個明文導致的嚴重后果。
附件 ciphertext.txt 有 11 個十六進制編碼的密文,它們是使用流密碼加密 11 個明文的結果,所有密文都使用相同的流密碼密鑰。
實驗的目標是解密最后一個密文,並提交明文消息。
提示:
- 對密文進行異或,並考慮當空格與 [a ~ z, A ~ Z] 中的字符進行異或時會發生什么。
- 附件 encrypt.py 是用於生成密文的 Python 示例程序(不影響實驗,僅供參考)。
實驗分析
首先從生成密文的示例程序 encrypt.py 入手:
def strxor(a, b):
# xor two strings of different lengths
if len(a) > len(b):
return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a[:len(b)], b)])
else:
return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b[:len(a)])])
其中 strxor(a, b)
為加密函數,參數 a
為密鑰,b
為明文。我們可以看出加密就是對明文和密鑰進行異或。從異或入手,我們可以找出一些線索。
線索 1
從提示中我們可以找到“線索”:字母異或空格,相當於轉換大小寫。
空格 ^ 小寫字母 = 大寫字母
,比如'a' ^ ' ' = 'A'
空格 ^ 大寫字母 = 小寫字母
字母 ^ 字母 = 非字母
因此,對於消息 \(m\),對位置 \(i\),\(j\) 上的字符 \(m_i\), \(m_j\) 進行異或,如果異或結果為英文字母,那么 \(i\),\(j\) 兩個位置上的字符很可能一個是空格,另一個是英文字母(但也有其他可能,比如 '!' ^ 'B' = 'c'
,實驗最后進行了解釋)。
線索 2
對於異或,我們有另外一個性質:\(x = x \oplus y \oplus y\)。
假設密文 \(c\)、消息 \(m\)、密鑰 \(k\),有 \(c_1 = m_1 \oplus k\),\(c_2 = m_2 \oplus k\) ……。於是根據上述異或的性質,我們可以得到 \(c_1 \oplus c_2 = m_1 \oplus k \oplus m_2 \oplus k = m_1 \oplus m_2\),此時,\(k\) 被消去了,得到了等式 \(c_1 \oplus c_2 = m_1 \oplus m_2\)。
思路
根據“線索”,我們可以形成大致的解密思路:
先尋找明文中可能存在的空格,位置記為 \(p\),接着有兩種思路,
- 一種方法是:將 \(p\) 對應的密文(\(c\))和 space 做異或操作,就可得到對應位置的密鑰信息,當獲取足夠多的密鑰信息后,即可對目標密文進行解密
- 解釋:因為此時位置 \(p\) 的明文為空格,即 \(m = space\),所以有 \(c \oplus space = m \oplus k \oplus space = space \oplus k \oplus space = k\)
- 另一種方法是:將 \(p\) 對應的密文(\(c_1\))直接和待解密密文(\(c_2\))進行異或,可得出待解密密文對應位置上的明文(大小寫相反)
- 解釋:\(c_1 \oplus c_2 = m_1 \oplus m_2\),因為 \(m_1\) 為空格,所以 \(c_1 \oplus c_2\) 的結果為 \(m_2\) 轉換大小寫的結果
如何尋找明文中可能存在的空格呢?
在 ciphertext.txt 中,每兩個 16 進制字符代表明文的一個字符(因為 ASCII 的范圍為 0 ~ 255)。我們把某條密文與剩余 9 條密文的相同位置進行異或,如果異或出來結果大部分都是字母,則說明了這條密文的這個位置對應的明文極有可能是個空格。
因此,從上述分析得知,在流密碼加密中多次使用相同密鑰是不安全的。
代碼
以下代碼片段通過計算出密鑰的方法(而非直接對目標密文進行異或)來解密,並對關鍵部分進行注釋。
計算空格的時候,可以把待解密密文也一起放進去異或,增加樣本
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
// 判斷空格的門限值,可根據結果進行修改調整
const int THRESHOLD_VALUE = 5;
const int KEY_LENGTH = 400;
// 存儲各條密文同一個位置的最大 count 值
static vector<int> countArray(KEY_LENGTH);
// 讀取密文
void readText(const string& fileName, vector<string>& cip, string& targetCip)
{
ifstream inFile(fileName);
if(!inFile.is_open())
{
cout << "Can't open the file\n";
exit(EXIT_FAILURE);
}
string temp;
bool flag = false;
while(getline(inFile, temp))
{
string::size_type idx = temp.find("Ciphertext");
if(flag)
{
flag = false;
cip.push_back(temp);
}
if(idx != string::npos)
{
flag = true;
}
}
// 最后一條待解密的密文也放入 cip
cip.push_back(temp);
targetCip = temp;
}
int isAlphabet(int c)
{
if(c >= 65 && c <= 90)
{
return 1;
}
if(c >= 97 && c <= 122)
{
return 1;
}
return 0;
}
int hexToDecimal(const string& hexStr)
{
int decimal;
stringstream ss;
ss.str(hexStr);
ss >> hex >> decimal;
return decimal;
}
string decimalToHex(int decimal)
{
stringstream ss;
ss << hex << decimal;
return ss.str();
}
// 計算可能的空格位置
vector<vector<int>> findSpace(vector<string>& ciphertext)
{
vector<vector<int>> spacePos;
// 對於給出的 10 條密文,計算對應明文中可能為空格的位置
for(vector<int>::size_type i = 0; i != ciphertext.size(); i++)
{
string& cipher = ciphertext[i];
string::size_type cipherLen = cipher.length();
vector<int> space;
// 每兩個 16 進制字符代表明文的一個字符
for(string::size_type j = 0; j < cipherLen; j = j + 2)
{
int tempI = hexToDecimal(cipher.substr(j, 2));
int count = 0;
// 位置 j 和 j + 1 上的字符與其余 10 條密文(包括待解密密文)該位置的字符進行異或
for(vector<string>::size_type k = 0; k != ciphertext.size(); k++)
{
string& residueCipher = ciphertext[k];
if(i == k || j > residueCipher.length())
{
continue;
}
int tempK = hexToDecimal(residueCipher.substr(j, 2));
// 若異或結果為字母,則 count++
count += isAlphabet(tempI ^ tempK);
}
// 若超過 THRESHOLD_VALUE 條的異或結果為字母,那么我們可以認定這個位置對應的明文是空格
if(count > THRESHOLD_VALUE)
{
space.push_back(j);
// count 值越大,空格的可能性越大。存入位置 j 上最大的 count 和對應密文的序號,因為 j + 1
// 位置是空的,剛好可以存密文的序號
if(countArray[j] < count)
{
countArray[j] = count;
countArray[j + 1] = i;
}
}
}
spacePos.push_back(space);
}
return spacePos;
}
// 計算密鑰
vector<string> calculateKey(vector<string>& ciphertext)
{
vector<string> key(KEY_LENGTH);
vector<vector<int>> spacePos = findSpace(ciphertext);
for(vector<string>::size_type i = 0; i != ciphertext.size(); i++)
{
string& cipher = ciphertext[i];
vector<int>& space = spacePos[i];
for(auto pos : space)
{
if(countArray[pos + 1] == (int)i)
{
// 該位置密文與空格進行異或,計算該位置的密鑰
// 32 是空格的 ASCII 碼
int k = 32 ^ hexToDecimal(cipher.substr(pos, 2));
key[pos] = decimalToHex(k);
}
}
}
return key;
}
int main(int argc, char** argv)
{
vector<string> ciphertext;
string targetCiphertext;
readText("./ciphertext.txt", ciphertext, targetCiphertext);
vector<string> key = calculateKey(ciphertext);
string message;
// 解密目標密文
for(string::size_type i = 0; i < targetCiphertext.length(); i = i + 2)
{
// 若對應位置上無密鑰,則該位置統一放置 '0'
if(key[i].empty())
{
message.push_back('0');
}
else
{
char m = hexToDecimal(targetCiphertext.substr(i, 2)) ^ hexToDecimal(key[i]);
message.push_back(m);
}
}
cout << message << endl;
return 0;
}
實驗結果
運行上述代碼,輸出:
The secuet message is: Whtn using aastream cipher, never use the key more than once
可以猜出,原文為:The secret message is: When using a stream cipher, never use the key more than once
我們發現,第 8 個字符
r
(secret
)被解析成了u
。
從上圖可以看出,只有第 5 條密文的第 8 個位置被假定為空格,但是最終第 8 個字符卻解錯了,由此說明第 5 條密文的第 8 個位置實際上並不是空格,它被誤判了。那這個位置的正確明文是什么呢?這篇文章給出了正確率較高的明文,下圖:
可以看到該位置是一個單引號
'
,把單引號和所有字母進行異或:
大多數都是字母,所以單引號被誤判為空格(因為判斷空格的依據是:異或出來的結果是否為字母)。