實驗內容:
在本次實驗中,需要實現兩個加密/解密系統,一個在密文分組鏈接模式(CBC)下使用AES,另一個在計數器模式(CTR)中使用AES。
實驗環境:
VS2019、C++、 Crypto++
實驗過程:
1、安裝Crypto++
1.1官網下載Crypto++
官網地址:https://www.cryptopp.com/
1.2解壓編譯,生成.lib文件
解壓后,用vs打開里面的.sln工程文件,會得到四個工程。
將cryptlib項目設為啟動項,選中cryptlib,選擇Debug x64模式,按下ctrl + B生成cryptlib。
1.3配置工程環境
新建工程,右鍵工程選擇屬性,選擇VC++目錄,設置包含目錄和庫目錄。庫目錄就是頭文件所在目錄,庫目錄就是剛生成的.lib所在目錄。
選擇鏈接器->輸入->附加依賴項,輸入剛才的生成的.lib文件完整名字。
選擇c/c++ ->代碼生成 -> 運行庫,選擇多線程調試(/MTd)
2、CBC模式下的AES原理
EBC和CBC模式都是分塊加密,經常要對plaintext進行填充,使之滿足16字節的整數倍。一般EBC模式下,如果采用相同的內容和相同的秘鑰,結果密文是相同的,這樣是不安全的。CBC引入向量IV的概念,加密過程除了提供key和plaintext還需要提供IV,這個IV大小為16個字節。密文中前16字節,就是IV。IV會參與第一塊的加密,之后就使用上一塊加密的結果代替IV。這樣做的好處就是使得相同的內容相同的秘鑰,加密的結果可能不同。
加密過程:
先將plaintext填充為16字節的整數倍,然后將plaintext等分為n份。第一次加密時,將IV與P1進行異或操作得到結果,然后將這個結果進行AES加密,得到C1。將C1鏈接到密文中去,並將C1代替IV,參與下一次的異或操作。然后重復上述操作,直到所有的block都進行加密。
解密過程:
選讀取密文的前16個字節,這16個字節就是IV。然后將去除IV的plaintext等分為n份。
先取出C1(第一塊密文),將C1通過AES解密得到結果T1, 然后將T1與IV異或的得到第一塊明文。再用C1替換IV參與下一次解密運算,重復上述操作。
單獨處理最后一塊,這里最后一塊采用的PKCS7的填充方式,這種方式填充最后幾個字節表示填充了多少個字節,獲取最后一個字節的數字,只需要在明文結果后面刪除這個長度的字節就得到真正的明文。
3、CBC模式下AES加密解密實現
3.1 CBC_AES解密代碼
void CBCdecrypto(const string& key,const string& ciphertext, string& plaintext)
{
string vi = ciphertext.substr(0, AES::BLOCKSIZE);//AES::BLOCKSIZE =16
string text = ciphertext.substr(AES::BLOCKSIZE, ciphertext.size() - AES::BLOCKSIZE);//前16為vi后16為填充
size_t groupNumber = text.size() / AES::BLOCKSIZE;
AESDecryption cryptor;
cryptor.SetKey((byte*)key.c_str(),key.size());
for (size_t i = 0 ; i<groupNumber;i++)
{
//獲取每一次的分組密文
string block = text.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
byte temp[AES::BLOCKSIZE];
memset(temp, 0x30, AES::BLOCKSIZE);
cryptor.ProcessBlock((byte*)block.c_str(), temp);
for (int j = 0; j < AES::BLOCKSIZE; j++) {
plaintext.push_back((byte)vi[j] ^ temp[j]);
}
vi = block;
}
string paddingBlock = plaintext.substr((groupNumber - 1) * AES::BLOCKSIZE, AES::BLOCKSIZE);
int paddingNum = (byte)paddingBlock[AES::BLOCKSIZE - 1];
for (int i = 0; i < paddingNum; i++) {
if (plaintext.back() != paddingNum) {
cout << "密文出錯" << endl;
exit(0);
}
plaintext.pop_back();
}
}
3.2 CBC_AES加密代碼
string CBCencrypto(string hexKey, string hexVI, string plaintext) {
string key;
hex_to_str(hexKey, key);
string VI;
hex_to_str(hexVI, VI);
string outstr;
outstr.clear();
outstr += VI;
//填充plaintext 使之成為16字節的整數倍
int paddingNum = AES::BLOCKSIZE - (plaintext.size() % AES::BLOCKSIZE);
for (int i = 0; i < paddingNum; i++) {
if (i == paddingNum - 1)
{
plaintext += (char)paddingNum;//最后一個byte表示填充多少個字節
}
else {
plaintext += (char)(15);//其他填充為0x0F
}
}
//獲取多少個組
int groupNumber = plaintext.size() / AES::BLOCKSIZE;
AESEncryption encryptor((byte*)key.c_str(), AES::MIN_KEYLENGTH);
for (int i = 0; i < groupNumber;i++)
{
string Pi = plaintext.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
//想讓IV 與 Pi異或
byte temp[AES::BLOCKSIZE];
memset(temp, 0x30, AES::BLOCKSIZE);
for (int j = 0; j < AES::BLOCKSIZE; j++)
{
temp[j] = (byte)Pi[j] ^ VI[j];
}
//然后將temp進行aes
byte tempr[AES::BLOCKSIZE];
encryptor.ProcessBlock(temp, tempr);
//將tempr添加到outstr上
string PiResult;
PiResult.clear();
for (int j = 0; j < AES::BLOCKSIZE;j++) {
PiResult += tempr[j];
}
outstr += PiResult;
//更新IV,參與下次運算
VI = PiResult;
}
//還需要將outstr 轉成十六進制的情況
string hexOutstr;
hexOutstr.clear();
for (int i = 0; i < outstr.size(); i++)
{
string stemp;
char2hexs(outstr[i], stemp);
hexOutstr += stemp;
}
return hexOutstr;
}
4、CTR模式下的AES原理
CTR有一個計數器counter,一般為16字節,前后兩次的加密與加密結果無關。每次加密counter加一,所以加密速度更快,但是安全性比CBC模式稍低點。而且CTR加密不需要填充,類似流模式。密文的前16個字節為counter。
加密過程:
先選取counter,如果沒有16字節就填充,現將counter通過AES進行加密,得到結果T1,
然后將T1與明文的第一分組進行異或得到結果C1,將C1鏈接到密文上,然后將counter+1進行下一輪加密。
對最后一個非整塊的明文單獨處理,處理方法與上面類似,只是長度按照剩余塊的長度處理。
解密過程:
選讀取密文中前16個字節作為counter,然后將去除counter的密文按照16個字節等分,最后一個非整16字節的單獨處理。
現將counter進行AES解密得到Ti然后,將Ti與密文塊Ci進行異或得到明文Pi,將Pi鏈接到輸出明文上,counter+1進行下一輪解密。
對后面非整16的塊單獨處理,處理方法類似。
5、CTR模式下AES加密解密實現
5.1 CTR_AES 解密代碼
void CTRdecrypto(const string& key, const string& ciphertext, string& plaintext)
{
// 密文的前 16 個字節為計數器的初始值
string counter = ciphertext.substr(0, AES::BLOCKSIZE);
string text = ciphertext.substr(AES::BLOCKSIZE, ciphertext.length() - AES::BLOCKSIZE);
int groupNumber = text.length() / AES::BLOCKSIZE;
AESEncryption aesEncryptor;
aesEncryptor.SetKey((byte*)key.c_str(), key.length());
byte aesResult[AES::BLOCKSIZE];
for (int i = 0; i <groupNumber; i++) {
string ciphertextBlock = text.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
memset(aesResult, 0x30, AES::BLOCKSIZE);
aesEncryptor.ProcessBlock((byte*)counter.c_str(), aesResult);
// 密文和 AES 加密結果異或,得到明文
for (int j = 0; j < AES::BLOCKSIZE; j++) {
plaintext.push_back(aesResult[j] ^ (byte)ciphertextBlock[j]);
}
// 計數器自增
counter = counterIncrement(counter, 1);
}
int residueLen = text.length() - groupNumber * AES::BLOCKSIZE;
string residueCiphertext = text.substr(groupNumber * AES::BLOCKSIZE, residueLen);
memset(aesResult, 0, AES::BLOCKSIZE);
aesEncryptor.ProcessBlock((byte*)counter.c_str(), aesResult);
for (int j = 0; j < residueLen; j++) {
plaintext.push_back(aesResult[j] ^ (byte)residueCiphertext[j]);
}
}
5.2 CTR_AES加密代碼
string CTRencrypto(string hexKey, string counter, string plaintext)
{
string key;
hex_to_str(hexKey, key);
string outstr;
outstr.clear();
outstr += counter;
//CTR獲取多少個整數 的16bytes
int num = plaintext.size() / AES::BLOCKSIZE;
AESEncryption encryptor((byte*)key.c_str(), AES::MIN_KEYLENGTH);
byte temp[AES::BLOCKSIZE];
for (int i = 0; i < num; i++) {
memset(temp, 0x30, AES::BLOCKSIZE);
encryptor.ProcessBlock((byte*)counter.c_str(), temp);
string block = plaintext.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
for (int j = 0; j < AES::BLOCKSIZE; j++) {
outstr.push_back(temp[j]^block[j]);
}
counter = counterIncrement(counter, 1);
}
/*
*處理最后一個非整塊的block
*/
int len = plaintext.size() - (num * AES::BLOCKSIZE);
string lastBlock = plaintext.substr(num * AES::BLOCKSIZE - 1, len);
memset(temp, 0x30, AES::BLOCKSIZE);
encryptor.ProcessBlock((byte*)counter.c_str(), temp);
for (int i = 0; i < len; i++) {
outstr.push_back(lastBlock[i] ^ temp[i]);
}
/*
*將輸出轉換正十六進制
*/
string hexOutstr;
hexOutstr.clear();
for (int i = 0; i < outstr.size(); i++)
{
string stemp;
char2hexs(outstr[i], stemp);
hexOutstr += stemp;
}
return hexOutstr;
}
6、實驗結果
對老師給出的test.txt的解密結果如下:
附:
1、其他代碼解釋
Class Exercise_3
hexKeys 和hexCiphertexts為十六進制的秘鑰和密文組。
keys和ciphertexts為轉換成byte數組的秘鑰和密文組。
plaintexts 是解密后的原文組。
key_path和cipher_path分別為秘鑰和密文的存放路徑。
modeVec是存放加載的秘鑰密文需要解碼的方式,這里有定義
方法解釋:
bool decrypto() 統一對讀取的秘鑰密文處理,得到所有的明文。
void printPlaintexts() 打印所有的結果信息。
bool init() 主要實現加載秘鑰和密文並轉換數據格式。
bool load_keys()加載秘鑰。
bool load_ciphers() 加載密文。
void changeDataFormal() 改變秘鑰密文格式。
其他頂層函數
void hex_to_str(const string& stringData, string& str)將十六進制字符串轉成byte字符串。
void char2hexs(char ch, string& s) 將一個char類型轉成字符串類型。
string counterIncrement(string counter, int n) counter的自增操作。
string CBC_AESEncryptStr(string sKey, string sIV, const char* plainText) CBC模式的調庫實現。
2、文件目錄格式
keys.txt與ciphertexts.txt中數據以空格分隔。
- 上述代碼有個錯誤的地方,單獨的 byte表示范圍是在-128 到127 ,不能表示我們要的范圍0 - 255 ,應該換成 unsigned char
最后一個問題,在由字符串轉十六進制的那里有錯
對於一個字符轉十六進制 直接用 int temp = (int)ch這種方式轉會有正有負
這時候可以將它與oxff 相與,就為正了。
還有就是用 stringstream這種方式轉,比如0x03,他會轉成0x3.所以這個時候可以先設置長度為二 比如 ss<<hex<<setfill('0'); ss<<setw(2),也可以像我這樣實現,效果是一樣的
void char2hexs(char ch, string& s)
{
s.clear();
stringstream ss;
ss.clear();
int temp = (int)ch;
temp = temp & 0xff;
ss << hex << temp;
s = ss.str();
if (s.size() == 1) {
s = '0' + s;
}
}