使用 Crypto++ 進行資料的加解密

這篇算是簡單紀錄一下,前陣子透過 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,搭配 StringSourceStringSink 來完成加密的流程。

這邊基本上就是先把要存放加密後結果的字串(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,而是要使用 AuthenticatedDecryptionFilterAuthenticatedEncryptionFilter

假設是 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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。