介紹如何在 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 中進行加密與解密。
將這段程式碼儲存至 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 的順序讀入。