實驗內容
【時間】6 月 6 號(周六)晚上
【地點】待定
【編程語言】Python(推薦)或者 C/C++
【實驗目的】
- 了解分組密碼的結構特點;
- 掌握傳統分組密碼結構 AES,以及 AES 在兩種工作模式 CBC 和 CTR 下的實現;
- 通過使用 Python(推薦)或者 C,編程分別實現 CBC 和 CTR 模式下的 AES 加密解密。
【實驗內容】
在本次實驗中,需要實現兩個加密/解密系統,一個在密文分組鏈接模式(CBC)下使用 AES,另一個在計數器模式(CTR)中使用 AES。
完成程序后,使用附件的 test.txt 中給出的四組密鑰和密文(十六進制形式)來驗證你的代碼。
【要求】:
- 在兩種模式下,16 字節的加密 IV 都要求是隨機生成的,並被添加到密文前面;
- 對於 CBC 加密,要求使用 PKCS5 填充方案;
- 對於 AES 的基本實現,你可以使用現有的加密庫,如 PyCrypto(Python),Crypto++(C++)或任何其他語言和庫;
- 要求自己實現 CBC 和 CTR 模式,而不是直接調用 AES 庫的內置功能;
實驗准備
本實驗擬采用 C++ 作為編程語言,並調用 Crypto++ 對 AES 進行基本實現。
Crypto++ 下載地址:https://www.cryptopp.com/#download
由於 Crypto++ 的編譯版本是使用 MSVC 構建的,我們用 Visual Studio 打開解決方案 cryptest.sln,並將 cryptlib 設為啟動項目,然后按下 Ctrl + B 生成 cryptlib。此時路徑 ./Win32/Output/Debug
下會輸出 cryptlib.lib 文件。
接着,新建一個項目測試可用性。
首先需要引入頭文件和庫文件:打開 項目 - *** 屬性 - VC++ 目錄
,把 Crypto++ 頭文件所在路徑和先前輸出的庫文件路徑分別添加到包含目錄和庫目錄。
然后在屬性頁的 鏈接器 - 輸入 - 附加依賴項
中添加 cryptlib.lib。
最后,在屬性頁的 C/C++ - 代碼生成 - 運行庫
中選擇 多線程調試(/MTD)
。
運行測試代碼:
#include <iostream>
#include <aes.h>
using namespace std;
using namespace CryptoPP;
int main()
{
cout << "Hello Crypto++" << endl;
cout << "Aes block size is " << AES::BLOCKSIZE << endl;
return 0;
}
並有如下輸出:
Hello Crypto++
Aes block size is 16
Crypto++ 的安裝和使用參考了這篇文章
實驗分析
實驗要求實現在 CBC 和 CTR 下的 AES,並有如下附加說明:
- 在兩種模式下,16 字節的加密 IV 都要求是隨機生成的,並被添加到密文前面;
- 對於 CBC 加密,要求使用 PKCS5 填充方案;
- 對於 AES 的基本實現,你可以使用現有的加密庫,如 PyCrypto(Python),Crypto++(C++)或任何其他語言和庫;
- 要求自己實現 CBC 和 CTR 模式,而不是直接調用 AES 庫的內置功能;
最后,用 test.txt 給出的密鑰和密文驗證代碼。
密文分組鏈接模式(CBC)
CBC 的加解密過程如下:
填充
CBC 是分組密碼的一種工作模式,在加密前要對最后一塊明文進行填充,實驗要求使用 PKCS5 填充方案。
PKCS#5 是按 8 字節分組對數據進行填充的(不足 8 字節時,補全到 8 字節):如果要填充 1 個字節,那填入的值就是 0x01
;如果要填充 2 個字節,那么填入的值就是 0x02
,以此類推。但若待加密數據長度正好為 8 的整數倍時,則需要填入 8 個 0x08
。
填充示例,分組大小為 8 字節,摘自這里:
h<0x07><0x07><0x07><0x07><0x07><0x07><0x07> 7
he<0x06><0x06><0x06><0x06><0x06><0x06> 6
hel<0x05><0x05><0x05><0x05><0x05> 5
hell<0x04><0x04><0x04><0x04> 4
hello<0x03><0x03><0x03> 3
hello <0x02><0x02> 2
hello w<0x01> 1
hello wo<0x08><0x08><0x08><0x08><0x08><0x08><0x08><0x08> 8
實際上,PKCS5 為 PKCS7 的一個子集(PKCS7 並不限於 8 字節的分組)。由於 AES 按 16 字節大小分組,如果采用 PKCS5,實質上就是采用 PKCS7。
填充發生在明文加密之前,而解密之后的明文需要去掉填充,這個過程可以看作是填充的逆過程。
去填充過程:首先我們獲取字符串的最后一個字符 paddingNum
,這個字符一定是填充的值。同時,說明有 paddingNum
個字符被填充進去了。我們只需循環地去掉末尾的 paddingNum
個字符即可。
加密前的填充代碼
string padding(const string& plaintext)
{
string lastBlock;
int len = plaintext.length();
int paddingNum = AES::BLOCKSIZE - len % AES::BLOCKSIZE;
int quotient = len / AES::BLOCKSIZE;
lastBlock = plaintext.substr(AES::BLOCKSIZE * quotient, len % AES::BLOCKSIZE);
for(int i = 0; i < AES::BLOCKSIZE - len % AES::BLOCKSIZE; i++)
{
lastBlock.push_back((unsigned char)paddingNum);
}
return plaintext.substr(0, AES::BLOCKSIZE * quotient) + lastBlock;
}
解密后的去填充代碼
// 密文/明文被分為 multiple 組
// 獲取解密后的最后一組明文
string lastBlock = plaintext.substr((multiple - 1) * AES::BLOCKSIZE, AES::BLOCKSIZE);
// 從字符串最后一個字符獲取填充字符
int paddingNum = (unsigned char)lastBlock[AES::BLOCKSIZE - 1];
// 把填充字符從明文中去掉
for(int i = 0; i < paddingNum; i++)
{
// 若填充字符出現不同,則說明給定密文有誤
if(plaintext.back() != paddingNum)
{
return "Ciphertext is invalid!";
}
plaintext.pop_back();
}
代碼
最后,根據以上 CBC 的解密過程圖和去填充思路可以寫出解密代碼:
string decrypt(const string& strCiphertext, const string& strKey)
{
string plaintext;
// 原始 key 為 16 進制形式,需按字節轉換為 char
string key = hexToStr(strKey);
string ciphertext = hexToStr(strCiphertext);
// 密文的前 16 個字節為 vi
string vi = ciphertext.substr(0, AES::BLOCKSIZE);
ciphertext = ciphertext.substr(AES::BLOCKSIZE, ciphertext.length() - AES::BLOCKSIZE);
int multiple = ciphertext.length() / AES::BLOCKSIZE;
AESDecryption aesDecryptor;
aesDecryptor.SetKey((byte*)key.c_str(), key.length());
for(int i = 0; i < multiple; i++)
{
// 分組密文
string ciphertextBlock = ciphertext.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
unsigned char outBlock[AES::BLOCKSIZE];
memset(outBlock, 0, AES::BLOCKSIZE);
aesDecryptor.ProcessBlock((byte*)ciphertextBlock.c_str(), outBlock);
// AES 輸出結果與上組密文或 vi 異或,得到明文
for(int j = 0; j < AES::BLOCKSIZE; j++)
{
plaintext.push_back(outBlock[j] ^ (unsigned char)vi[j]);
}
vi = ciphertextBlock;
}
// 解密后,最后一組明文單獨處理
string lastBlock = plaintext.substr((multiple - 1) * AES::BLOCKSIZE, AES::BLOCKSIZE);
// 從字符串最后一個字符獲取填充字符
int paddingNum = (unsigned char)lastBlock[AES::BLOCKSIZE - 1];
// 把填充字符從明文中去掉
for(int i = 0; i < paddingNum; i++)
{
// 若填充字符出現不同,則說明給定密文有誤
if(plaintext.back() != paddingNum)
{
return "Ciphertext is invalid!";
}
plaintext.pop_back();
}
return plaintext;
}
加解密完整代碼 👉 傳送門
計數器模式(CTR)
CTR 的加解密過程如下:
CTR 相較於 CBC 少了填充的過程。另外,CTR 需要維護一個自增的計數器。
計數器的自增代碼
string counterIncrement(const string& counter, int n)
{
string res = counter;
int addend = n;
for(int i = counter.length() - 1; i >= 0; i--)
{
unsigned char tempChar = counter[i];
if((int)tempChar + addend > 255)
{
tempChar = tempChar + addend;
addend = 1;
}
else
{
tempChar = tempChar + addend;
addend = 0;
}
res[i] = tempChar;
}
return res;
}
代碼
根據以上 CTR 的解密過程圖可以寫出解密代碼(可根據上方 CBC 修改):
string decrypt(const string& strCiphertext, const string& strKey)
{
string plaintext = "";
string key = hexToStr(strKey);
string ciphertext = hexToStr(strCiphertext);
// 密文的前 16 個字節為計數器的初始值
string counter = ciphertext.substr(0, AES::BLOCKSIZE);
ciphertext = ciphertext.substr(AES::BLOCKSIZE, ciphertext.length() - AES::BLOCKSIZE);
int multiple = ciphertext.length() / AES::BLOCKSIZE;
AESEncryption aesEncryptor;
aesEncryptor.SetKey((byte*)key.c_str(), key.length());
for(int i = 0; i < multiple; i++)
{
string ciphertextBlock = ciphertext.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
string xorBlock;
unsigned char outBlock[AES::BLOCKSIZE];
memset(outBlock, 0, AES::BLOCKSIZE);
aesEncryptor.ProcessBlock((byte*)counter.c_str(), outBlock);
// 密文和 AES 加密結果異或,得到明文
for(int j = 0; j < AES::BLOCKSIZE; j++)
{
xorBlock.push_back(outBlock[j] ^ (unsigned char)ciphertextBlock[j]);
}
plaintext += xorBlock;
// 計數器自增
counter = counterIncrement(counter, 1);
}
// 最后的分組可能不完整,單獨輸出
int residueLen = ciphertext.length() - multiple * AES::BLOCKSIZE;
string residueCiphertext = ciphertext.substr(multiple * AES::BLOCKSIZE, residueLen);
string xorBlock;
unsigned char outBlock[AES::BLOCKSIZE];
memset(outBlock, 0, AES::BLOCKSIZE);
aesEncryptor.ProcessBlock((byte*)counter.c_str(), outBlock);
for(int j = 0; j < residueLen; j++)
{
xorBlock.push_back(outBlock[j] ^ (unsigned char)residueCiphertext[j]);
}
plaintext += xorBlock;
return plaintext;
}
加解密完整代碼 👉 傳送門
實驗結果
對於 txt 中的測試 1 和測試 2,分別輸出:Basic CBC mode encryption needs padding.
和 Our implementation uses rand. IV
。
測試 3 和測試 4,分別輸出:CTR mode lets you build a stream cipher from a block cipher.
和 Always avoid the two time pad!
。
Crypto++ 中的 AES 庫也內置了包括 CBC 和 CTR 在內的各種模式。此處為直接調用庫函數的實現代碼。
參考:
https://www.cnblogs.com/lit10050528/p/4081658.html
https://www.cnblogs.com/YZFHKMS-X/p/11829021.html
https://www.cryptopp.com/wiki/Advanced_Encryption_Standard
https://segmentfault.com/a/1190000019793040