介紹如何在 C++ 語言中使用 Crypto++ 函式庫實作 AES-CBC 加密與解密,以及 AES-GCM 認證加密(AE)與帶有關聯資料的認證加密(AEAD)。
在使用 Crypto++ 函式庫之前,請先確認系統上有安裝好該函式庫,Ubuntu Linux 可以參考 Ubuntu Linux 安裝、使用 Crypto++ 加密函式庫教學。
AES 加密與解密
以下是一個採用 AES 演算法與 CBC 模式進行資料加密與解密的範例:
#include <iostream>
#include <string>
#include "cryptlib.h"
#include "aes.h"
#include "modes.h"
#include "files.h"
#include "osrng.h"
#include "hex.h"
int main(int argc, char* argv[]) {
using namespace CryptoPP;
// 建立隨機亂數產生器
AutoSeededRandomPool prng;
// 輸出十六進位的 Filter
HexEncoder encoder(new FileSink(std::cout));
// 將金鑰與 IV 放置於安全的記憶體空間
SecByteBlock key(AES::DEFAULT_KEYLENGTH); // 採用預設的金鑰長度(128 位元)
// SecByteBlock key(AES::MAX_KEYLENGTH); // 採用最長的金鑰長度(256 位元)
// SecByteBlock key(24); // 自行指定金鑰長度(192 位元)
SecByteBlock iv(AES::BLOCKSIZE);
// 產生隨機金鑰與 IV
prng.GenerateBlock(key, key.size());
prng.GenerateBlock(iv, iv.size());
// 原始資料
std::string plain = "Crypto++ is a free C++ library for cryptography.";
// 用來儲存密文與明文的變數
std::string cipher, recovered;
std::cout << "原始資料:" << plain << std::endl;
try {
// 採用 AES-CBC 加密
CBC_Mode<AES>::Encryption e;
// 設定金鑰與 IV
e.SetKeyWithIV(key, key.size(), iv);
// 進行加密
StringSource s(plain, true,
new StreamTransformationFilter(e,
new StringSink(cipher)
) // StreamTransformationFilter
); // StringSource
} catch(const Exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
std::cout << "金鑰:";
encoder.Put(key, key.size());
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "IV:";
encoder.Put(iv, iv.size());
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "密文:";
encoder.Put((const byte*) &cipher[0], cipher.size());
encoder.MessageEnd();
std::cout << std::endl;
try {
// 採用 AES-CBC 解密
CBC_Mode<AES>::Decryption d;
// 設定金鑰與 IV
d.SetKeyWithIV(key, key.size(), iv);
// 進行解密
StringSource s(cipher, true,
new StreamTransformationFilter(d,
new StringSink(recovered)
) // StreamTransformationFilter
); // StringSource
} catch(const Exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
std::cout << "解開的明文:" << recovered << std::endl;
return EXIT_SUCCESS;
}
這裡我們將金鑰與 IV 存放在 SecByteBlock 所配置的空間中,確保資料在使用完之後會自動抹除,不會保留在記憶體中,而金鑰與 IV 則是透過 AutoSeededRandomPool 以作業系統所提供的隨機亂數產生器來產生。
AES 演算法的金鑰長度有 128 位元、192 位元與 256 位元三種,這裡我們採用預設的 AES::DEFAULT_KEYLENGTH,使用者可以根據自己的需求自由調整金鑰長度。
進行 AES 加密之前,要選擇加密模式(modes),以選定的加密模式搭配 AES 加密演算法,建立一個加密的物件後,藉由 StreamTransformationFilter 放進 Pipelining 中進行加密與解密。
除了 AES 搭配 CBC 之外,我們也可以選用各種加密模式搭配任何 Block Cipher 來進行加密與解密。
將這段程式碼儲存至 aes1.cpp 之後,使用以下指令編譯並執行:
# 編譯 Crypto++ 應用程式
g++ -I/usr/include/crypto++ -o aes1 aes1.cpp -lcryptopp
# 執行應用程式
./aes1
原始資料:Crypto++ is a free C++ library for cryptography. 金鑰:5A0AF5D6BC488E4D900F03F89F29FCDA IV:532A58267F5877A7C101A8E43E142346 密文:DF3F73CF809D6BD6BF9B032D6852ACE7D0186D1ABB4F087FB3762FB285E59C95E427692C010F9EF99CE65E9F8500AF00BD1E9E8D44B8E680D9F8255CEA737251 解開的明文:Crypto++ is a free C++ library for cryptography.
AES 認證加密
AES 可以搭配 CCM、GCM 或 EAX 模式以進行認證加密(authenticated encryption,簡稱 AE),確保資料的真實性,以下是 AES-GCM 的實作認證加密的範例,實作上跟一般的模式差不多,只是選擇 GCM 模式,並將 StreamTransformationFilter 替換成 AuthenticatedEncryptionFilter 與 AuthenticatedDecryptionFilter。
#include <iostream>
#include <string>
#include "cryptlib.h"
#include "aes.h"
#include "gcm.h"
#include "modes.h"
#include "files.h"
#include "osrng.h"
#include "hex.h"
int main(int argc, char* argv[]) {
using namespace CryptoPP;
// 建立隨機亂數產生器
AutoSeededRandomPool prng;
// 輸出十六進位的 Filter
HexEncoder encoder(new FileSink(std::cout));
// 將金鑰與 IV 放置於安全的記憶體空間
SecByteBlock key(AES::DEFAULT_KEYLENGTH); // 採用預設的金鑰長度(128 位元)
// SecByteBlock key(AES::MAX_KEYLENGTH); // 採用最長的金鑰長度(256 位元)
// SecByteBlock key(24); // 自行指定金鑰長度(192 位元)
SecByteBlock iv(AES::BLOCKSIZE);
// 產生隨機金鑰與 IV
prng.GenerateBlock(key, key.size());
prng.GenerateBlock(iv, iv.size());
// 原始資料
std::string plain = "Crypto++ is a free C++ library for cryptography.";
// 用來儲存密文與明文的變數
std::string cipher, recovered;
std::cout << "原始資料:" << plain << std::endl;
try {
// 採用 AES-GCM 加密
GCM<AES>::Encryption e;
// 設定金鑰與 IV
e.SetKeyWithIV(key, key.size(), iv);
// 進行加密
StringSource s(plain, true,
new AuthenticatedEncryptionFilter(e,
new StringSink(cipher)
) // StreamTransformationFilter
); // StringSource
} catch(const Exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
std::cout << "金鑰:";
encoder.Put(key, key.size());
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "IV:";
encoder.Put(iv, iv.size());
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "密文:";
encoder.Put((const byte*) &cipher[0], cipher.size());
encoder.MessageEnd();
std::cout << std::endl;
// 模擬加密資料遭受竄改
cipher[0] |= 0x0F;
try {
// 採用 AES-GCM 解密
GCM<AES>::Decryption d;
// 設定金鑰與 IV
d.SetKeyWithIV(key, key.size(), iv);
// 進行解密
StringSource s(cipher, true,
new AuthenticatedDecryptionFilter(d,
new StringSink(recovered)
) // StreamTransformationFilter
); // StringSource
} catch(const Exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
std::cout << "解開的明文:" << recovered << std::endl;
return EXIT_SUCCESS;
}
這裡我們在解密之前故意將密文修改過,模擬資料遭受竄改的狀況,測試認證加密的效果,編譯與執行方式都跟前例相同,將這段程式碼儲存至 aes2.cpp 之後,使用以下指令編譯並執行:
# 編譯 Crypto++ 應用程式
g++ -I/usr/include/crypto++ -o aes2 aes2.cpp -lcryptopp
# 執行應用程式
./aes2
原始資料:Crypto++ is a free C++ library for cryptography. 金鑰:BBA02ED9BCAC7DE0CEB1E32366ADB89B IV:3BF2E2C35A16885116475896E6D8CAD9 密文:2DE26735F852AC3CF5E890D2D5C35547A922F455C2087B52C5973572BA5F3F1E4E0696101C53EB24A092C810E8D8228910D0AE0F37B9B32A10E1481C36F469B9 HashVerificationFilter: message hash or MAC not valid
我們可以將修改密文的竄改測試用於上面普通的 AES-CBC 加密,就會發現普通沒有認證功能的加密模式,遇到竄改的密文會直接解出錯誤的資料,而具有認證功能的的加密方式,就會直接顯示檢查碼不正確的錯誤訊息,這對於驗證資料的正確性來說非常有幫助。
AES 帶有關聯資料的認證加密
若要採用帶有關聯資料的認證加密(authenticated encryption with associated data,簡稱 AEAD),就要在 AuthenticatedEncryptionFilter 中建立兩條資料管道,DEFAULT_CHANNEL 用於加密與認證,而 AAD_CHANNEL 則單純用於附加認證的認證,以下是一個基本範例。
#include <iostream>
#include <string>
#include "cryptlib.h"
#include "aes.h"
#include "gcm.h"
#include "modes.h"
#include "files.h"
#include "osrng.h"
#include "hex.h"
// P1619 Test Vector 003
// KEY 0000000000000000000000000000000000000000000000000000000000000000
// IV 000000000000000000000000
// AAD 00000000000000000000000000000000
// PTX 00000000000000000000000000000000
// CTX cea7403d4d606b6e074ec5d3baf39d18
// TAG ae9b1771dba9cf62b39be017940330b4
//
// 欄位說明:
// KEY AES 金鑰
// IV 初始向量
// HDR 附加認證資料
// PTX 原始明文
// CTX 加密後的密文
// TAG 認證 tag
int main(int argc, char* argv[]) {
using namespace CryptoPP;
// 配置 AES 金鑰與 IV 儲存空間
byte key[32];
byte iv[12];
// 設定金鑰與 IV(KEY 與 IV)
memset(key, 0, sizeof(key));
memset(iv, 0, sizeof(iv));
// 附加認證資料(AAD)
std::string adata(16, (char) 0x00);
// 原始明文資料(PTX)
std::string pdata(16, (char) 0x00);
// 認證 Tag 長度
const int TAG_SIZE = 16;
// 加密後的密文(含有認證 Tag)與解密後的明文
std::string cipher, recovered;
// 輸出十六進位的 Filter
HexEncoder encoder(new FileSink(std::cout), false);
try {
// 採用 AES-GCM 加密
GCM<AES>::Encryption e;
// 設定金鑰與 IV
e.SetKeyWithIV(key, sizeof(key), iv, sizeof(iv));
// 以 AuthenticatedEncryptionFilter 建立
// DEFAULT_CHANNEL 與 AAD_CHANNEL 兩條資料管道
AuthenticatedEncryptionFilter ef( e,
new StringSink(cipher), false,
TAG_SIZE /* MAC_AT_END */
); // AuthenticatedEncryptionFilter
// 先輸入附加認證資料
ef.ChannelPut(AAD_CHANNEL, (byte*) adata.data(), adata.size());
ef.ChannelMessageEnd(AAD_CHANNEL);
// 再輸入原始明文資料
ef.ChannelPut(DEFAULT_CHANNEL, (byte*) pdata.data(), pdata.size());
ef.ChannelMessageEnd(DEFAULT_CHANNEL);
} catch(CryptoPP::Exception& e) {
std::cerr << "例外:" << e.what() << std::endl;
return EXIT_FAILURE;
}
std::cout << "原始明文:";
encoder.Put((const byte*) &pdata[0], pdata.size());
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "附加認證資料:";
encoder.Put((const byte*) &adata[0], adata.size());
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "AES 金鑰:";
encoder.Put(key, sizeof(key));
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "IV:";
encoder.Put(iv, sizeof(iv));
encoder.MessageEnd();
std::cout << std::endl;
std::cout << "原始密文(含 Tag):";
encoder.Put((const byte*) &cipher[0], cipher.size());
encoder.MessageEnd();
std::cout << std::endl;
// 加密資料遭受竄改
cipher[0] |= 0x0F;
// cipher[31] |= 0x0F;
// 附加認證資料錯誤
// adata[0] |= 0x0F;
std::cout << "竄改密文(含 Tag):";
encoder.Put((const byte*) &cipher[0], cipher.size());
encoder.MessageEnd();
std::cout << std::endl;
try {
// 採用 AES-GCM 解密
GCM<AES>::Decryption d;
// 設定金鑰與 IV
d.SetKeyWithIV(key, sizeof(key), iv, sizeof(iv));
// 以 AuthenticatedDecryptionFilter 建立
// DEFAULT_CHANNEL 與 AAD_CHANNEL 兩條資料管道
AuthenticatedDecryptionFilter df( d, new StringSink(recovered),
AuthenticatedDecryptionFilter::DEFAULT_FLAGS,
TAG_SIZE );
// 先輸入附加認證資料
df.ChannelPut(AAD_CHANNEL, (byte*) adata.data(), adata.size());
df.ChannelMessageEnd(AAD_CHANNEL);
// 再輸入密文資料(含 Tag)
df.ChannelPut(DEFAULT_CHANNEL, (byte*) cipher.data(), cipher.size());
df.ChannelMessageEnd(DEFAULT_CHANNEL);
} catch( CryptoPP::Exception& e ) {
std::cerr << "例外:" << e.what() << std::endl;
return EXIT_FAILURE;
}
std::cout << "解密後的明文:";
encoder.Put((const byte*) &recovered[0], recovered.size());
encoder.MessageEnd();
std::cout << std::endl;
return EXIT_SUCCESS;
}
將這段程式碼儲存至 aes3.cpp 之後,按照同樣的方式編譯並執行:
# 編譯 Crypto++ 應用程式
g++ -I/usr/include/crypto++ -o aes3 aes3.cpp -lcryptopp
# 執行應用程式
./aes3
原始明文:00000000000000000000000000000000 附加認證資料:00000000000000000000000000000000 AES 金鑰:0000000000000000000000000000000000000000000000000000000000000000 IV:000000000000000000000000 原始密文(含 Tag):cea7403d4d606b6e074ec5d3baf39d18ae9b1771dba9cf62b39be017940330b4 竄改密文(含 Tag):cfa7403d4d606b6e074ec5d3baf39d18ae9b1771dba9cf62b39be017940330b4 例外:HashVerificationFilter: message hash or MAC not valid
這裡我們同樣模擬加密資料遭受竄改的情況,以及附加認證資料錯誤的情況,在解密時兩個驗證都必須通過才可以正確解密。
AuthenticatedEncryptionFilter 會將認證 tag 附加在加密過後的密文之後一起輸出,這部分可以仔細對照程式碼中的 P1619 Test Vector 003 進行驗證,而解密時 AuthenticatedDecryptionFilter 預設也是依照密文後附加認證 tag 的順序讀入。
