這篇算是簡單紀錄一下,前陣子透過 Crypto++ 這個 C++ 的函式庫(官網、GitHub)來進行資料加密、解密的紀錄。不過由於 Heresy 和密碼學不熟,所以這邊不會去解釋原理或各種方法,單純是針對怎麼用來做紀錄。
Crypto++ 基本的 header 檔案是 cryptlib.h,主要的 namespace 是 CryptoPP,在使用的時候基本上一定會用到;但是之後根據要使用的演算法,則也還需要另要加上個別的 header 才行。
而在要使用 Crypto++ 加解密的時候,要先決定要用哪種運作模式(mode、官方文件)、以及哪種 block cipher,然後才產生用來加解密的處理器。
在模式的部分,基本上又分成兩種類型的模式:
- Confidentiality-only modes
- 包括:ECB、CBC、CTR、OFB 等等
- Authenticated encryption (authenc) modes
- 包括:CCM、EAX、GCM、OCB
就個人的認知,authenticated encryption(認證加密、維基百科)似乎主要是可以確保解密出來的結果是正確的。相較於 authenc,confidentiality-only modes 有方法在用錯的密碼去解密的時候,也可以解出內容,但是內容會是錯的;而在開發程式的時候,會難以判斷到底有沒有解密成功。
使用時,confidentiality-only modes 的類別都統一定義在 modes.h 這個 header 裡面,而 authenticated encryption modes 的東西,則是個別的 header 檔案,例如要使用 OCB 模式,就要引入 ocb.h。
而 block cipher 的部分,則就包含了 AES、DES、IDEA 這些就算不懂密碼學的人也或多或少聽過的方法了。要使用的時候,要各自引入個別的 header 檔案,例如要用 AES 就是要 include aes.h。
這邊就來稍微紀錄一下 Crypto++ 要怎麼用吧~
基本的加密
決定了要使用的 mode 以及 block cipher 後,接下來就可以開始準備進行加密和解密了!
這邊就先以 confidentiality-only mode 的 CFB + AES 來作例子(官方文件),下面就是一個加密的簡單的範例:
#include <iostream> #include <string> #include <cryptopp/cryptlib.h> #include <cryptopp/filters.h> #include <cryptopp/modes.h> #include <cryptopp/aes.h> int main() { // set IV value CryptoPP::byte aIV[CryptoPP::AES::BLOCKSIZE]; for (int i = 0; i < CryptoPP::AES::BLOCKSIZE; ++i) aIV[i] = 0; // key std::string sKey = "<hello password>"; // initialize encryptor CryptoPP::CFB_Mode<CryptoPP::AES>::Encryption enc; enc.SetKeyWithIV((CryptoPP::byte*)sKey.data(), sKey.size(), aIV); // content std::string sMyData = "Hello Crypto++"; // encrypt std::string sEncData; CryptoPP::StringSource ss(sMyData, true, new CryptoPP::StreamTransformationFilter(enc, new CryptoPP::StringSink(sEncData) ) ); std::cout << sEncData << "\n"; }
首先,要進行加解密會需要「金鑰」(key)和「初始向量」(Initialization Vector、IV、官方文件)作為參數;透過這兩個參數,才能讓別人不知道該怎麼去解密,而如果拿錯誤的 key 或 IV 來解密,就會得到錯誤的結果。(不過像 ECB 模式就不需要 IV)
所以,在這邊就先建立 key 和 IV;理論上他們的型別都要是 CryptoPP::byte,但是由於他實際上是 unsigned char,所以某種意義上可以用 std::string 來強制轉型。
sIV 的長度是 CryptoPP::AES::BLOCKSIZE(定義為 16),這邊是取巧都設定為 0,實際上應該要自己訂出一組數值,或是亂數產生後保存下來。
而 sKey 的長度是 CryptoPP::AES::DEFAULT_KEYLENGTH(也是 16),所以這邊是給他一個長度為 16 的字串。(某些方法的 key 長度似乎可以不需要完全符合)
而要加密的時候,是要產生一個 mode<cipher> 這樣 template 形式、型別為 CryptoPP::CFB_Mode<CryptoPP::AES>::Encryption 的物件(這邊是 enc),然後先透過 SetKeyWithIV() 這個函式來設定 key 和 IV。
再來、sMyData 則就是要加密的資料了。這邊雖然是用文字,但是實際上他也可以針對 binary 資料作加密,不見得要是文字。
而真正要處理的資料的時候,是要先 include filters.h,然後透過 Crypto++ 提供的 StreamTransformationFilter,搭配 StringSource 和 StringSink 來完成加密的流程。
這邊基本上就是先把要存放加密後結果的字串(sEncData)用 CryptoPP::StringSink 包裝起來,作為輸出目的地餵給 StreamTransformationFilter。(官方文件)
而在建立 CryptoPP::StreamTransformationFilter 的時候,第一個參數是要告訴他要用什麼東西做資料的轉換,這邊就是前面建立的 enc;而第二個參數則就是剛剛建立、作為輸出目的地的 StringSink。(官方文件)
最外面的 CryptoPP::StringSource 則是讀取資料來源(sMyData)然後透過 StreamTransformationFilter 做資料的轉換、輸出;中間的 true 是代表是否要立刻把所有資料丟給轉換器(話說,什麼時候會是 false?)。(官方文件)
加密結束後,這邊就是直接把結果作輸出了~不過加密完後的資基本上是 binary 形式,所以這邊輸出的東西應該會變成無法閱讀的就是了。 XD
解密的步驟
Crypto++ 的設計還算是滿簡單好用的,解密的流程和加密算是幾乎一樣,只要把資料的型別換掉就好了。
以這邊來說,其實要修改的只有一開始建立的 enc 的物件,並將型別從 CFB_Mode<AES>::Encryption 改成 CFB_Mode<AES>::Decryption 就可以了。下面就是修改後的結果:
// initialize decryptor CryptoPP::CFB_Mode<CryptoPP::AES>::Decryption dec; dec.SetKeyWithIV((CryptoPP::byte*)sKey.data(), sKey.size(), aIV); // decrypt CryptoPP::StringSource ss(sEncData, true, new CryptoPP::StreamTransformationFilter(dec, new CryptoPP::StringSink(sResult) ) );
理論上,這樣解密的結果是會和本來加密前的結果一樣的。
函式通用化
由於加密和解密的程式差異很小,所以其實真的要寫成函式的時候,是很簡單可以透過 template 來簡化的,下面就是一個寫法:
template<class TProc> std::string process(const std::string& sInput, const std::string& sKey, const CryptoPP::byte* aIV) { std::string sResult; TProc enc; enc.SetKeyWithIV((CryptoPP::byte*)sKey.data(), sKey.size(), aIV); CryptoPP::StringSource ss(sInput, true, new CryptoPP::StreamTransformationFilter(enc, new CryptoPP::StringSink(sResult) ) ); return sResult; } std::string encrypt(const std::string& sInput, const std::string& sKey, const CryptoPP::byte* aIV) { return process<CryptoPP::CFB_Mode<CryptoPP::AES>::Encryption>(sInput, sKey, aIV); } std::string decrypt(const std::string& sInput, const std::string& sKey, const CryptoPP::byte* aIV) { return process<CryptoPP::CFB_Mode<CryptoPP::AES>::Decryption>(sInput, sKey, aIV); }
如此一來,要使用加密或解密,都可以很簡單了。
// content std::string sMyData = "Hello Crypto++"; // encrypt std::string sEncData = encrypt(sMyData, sKey, aIV); std::cout << "Encrypted: " << sEncData << "\n"; // decrypt std::string sDecData = decrypt(sEncData, sKey, aIV); std::cout << "Decrypted: " << sDecData << "\n";
而由於 Crypto++ 大部分 mode 和 cipher 的組合都是相同的操作流程,只要把 CFB_Mode<AES> 改成其它自己需要的組合就好了,所以其實要實作其他加解密的方法,也是很簡單的~甚至也可以透過 template 來寫一個函式套用在大部分的組合上,相當地方便。
例外處理
Crypto++ 的錯誤處理基本上是用 exception 來做的,所以如果要寫錯誤處理的話,就是要用 try – catch
的架構來寫。
而 Crypto++ 丟出來的會是自己定義的例外,主要型別是 CryptoPP::Exception。
所以如果要去接他的例外的話,可以把函式寫成:
template<class TProc> std::string process(const std::string& sInput, const std::string& sKey, const CryptoPP::byte* aIV) { std::string sResult; try { TProc enc; enc.SetKeyWithIV((CryptoPP::byte*)sKey.data(), sKey.size(), aIV); CryptoPP::StringSource ss(sInput, true, new CryptoPP::StreamTransformationFilter(enc, new CryptoPP::StringSink(sResult) ) ); } catch (CryptoPP::Exception e) { std::cerr << e.what() << std::endl; } return sResult; }
什麼時候會出現例外呢?像是如果使用 CBC 模式的時候,在解密的時候使用錯誤的 key 的話,他就會丟出 CryptoPP::InvalidCiphertext 這個例外,訊息是:
StreamTransformationFilter: invalid PKCS #7 block padding found
但是如果是使用 OFB 的話,在使用錯誤的 key 解密的時候,他並不會有任何錯誤訊息,而是會給出一個錯誤的結果,這點是要注意的。
Authenticated encryption Mode
至於 authenticated encryption mode 呢?他的資料處理流程大致上和前面講的一樣,不過他除了要使用 mode 不一樣外,在進行加解密的時候也不是使用 StreamTransformationFilter,而是要使用 AuthenticatedDecryptionFilter 和 AuthenticatedEncryptionFilter。
假設是 GCM + AES 的話,加密程式可以寫成下面的樣子:
// initialize encryptor CryptoPP::GCM<CryptoPP::AES>::Encryption enc; enc.SetKeyWithIV((CryptoPP::byte*)sKey.data(), sKey.size(), aIV); // encrypt std::string sEncData; CryptoPP::StringSource ss(sMyData, true, new CryptoPP::AuthenticatedEncryptionFilter(enc, new CryptoPP::StringSink(sEncData) ) );
基本上,也是大同小異了~
不過,如果在使用錯誤的 key 或 IV 來試著解密的時候,他都會丟出 CryptoPP::HashVerificationFilter::HashVerificationFailed 這個例外,其訊息為:
HashVerificationFilter: message hash or MAC not valid
這樣基本上應該會更容易確認解密到底有沒有成功了。
特殊狀況
上面算是比較一般性的寫法,但是某些模式,則不能這樣寫。
例如 ECB 模式本身並沒有 IV 這個參數,所以不能使用 SetKeyWithIV() 這個函式(雖然他也有這個函式),而是要使用 SetKey() 這個函式。
而像是 CCM 的話,在家密的時候要先透過 SpecifyDataLengths() 這個函式設定資料的大小,所以會多其他的模式一個步驟;解密的時候則更複雜,需要分兩個階段來進行。(官方文件)
如果要使用 AEAD(authenticated encryption with additional authenticated data)的模式來處理的話,則還會有牽涉到 channel 的操作,就又更複雜了。
External Cipher Modes
前面 mode<cipher> 這樣 template 形式在 Crypto++ 裡被稱為是「Internal Cipher Modes」;相對於此,也還有「External Cipher Modes」。
他的寫法就不是用 template 的形式,而是自己產生一個 cipher 的物件,然後把它當參數丟給 mode 來使用了。
如果要這樣用的話,寫法基本上如下:
// Notice no mode for cipher CryptoPP::AES::Encryption cipher; cipher.SetKey(key, key.size()); // Attach a mode of operation CryptoPP::CBC_Mode_ExternalCipher::Encryption encryptor(cipher, iv);
不過 Heresy 自己在用的時候,卻有碰到部分組合在 internal 模式沒問題、但是在 external 模式會錯誤的狀況,所以後來是放棄了… orz
這篇大概就這樣了~其他細節…恩,再說吧。 XD