實驗一 Many Time Pad


實驗內容

【時間】 暫定 5 月 23 號(周六)晚上

【編程語言】 Python(推薦)或者 C/C++

【實驗目的】

  1. 了解流密碼的結構特點;
  2. 掌握 One-time Pad 的一般具體實現;
  3. 通過使用 Python(推薦)或者 C,編程實現一個流密碼加密示例的破解,進一步認識在流密碼加密中多次使用相同密鑰導致的問題。

【實驗內容】

在掌握流密碼結構的基礎上,通過本實驗觀察使用相同流密碼密鑰加密多個明文導致的嚴重后果。

附件 ciphertext.txt 有 11 個十六進制編碼的密文,它們是使用流密碼加密 11 個明文的結果,所有密文都使用相同的流密碼密鑰。

實驗的目標是解密最后一個密文,並提交明文消息。

提示:

  1. 對密文進行異或,並考慮當空格與 [a ~ z, A ~ Z] 中的字符進行異或時會發生什么。
  2. 附件 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 個字符 rsecret)被解析成了 u

從上圖可以看出,只有第 5 條密文的第 8 個位置被假定為空格,但是最終第 8 個字符卻解錯了,由此說明第 5 條密文的第 8 個位置實際上並不是空格,它被誤判了。那這個位置的正確明文是什么呢?這篇文章給出了正確率較高的明文,下圖:

可以看到該位置是一個單引號 ',把單引號和所有字母進行異或:

大多數都是字母,所以單引號被誤判為空格(因為判斷空格的依據是:異或出來的結果是否為字母)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM