C/C++

C++ 語言使用 Crypto++ 實作 AES 加密、解密、認證加密教學與範例

介紹如何在 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 替換成 AuthenticatedEncryptionFilterAuthenticatedDecryptionFilter

#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 的順序讀入。

參考資料

Share
Published by
Office Guide

Recent Posts

Python 使用 PyAutoGUI 自動操作滑鼠與鍵盤

本篇介紹如何在 Python ...

1 年 ago

Ubuntu Linux 以 WireGuard 架設 VPN 伺服器教學與範例

本篇介紹如何在 Ubuntu ...

1 年 ago

Linux 網路設定 ip 指令用法教學與範例

本篇介紹如何在 Linux 系...

1 年 ago

Linux 以 Cryptsetup、LUKS 加密 USB 隨身碟教學與範例

介紹如何在 Linux 系統中...

1 年 ago