C/C++

C 語言使用 OpenSSL 實作 AES 加密、解密教學與範例

介紹如何在 C 語言程式中使用 OpenSSL 函式庫,以 AES 對稱式加密演算法實作資料的加密與解密。

安裝 OpenSSL 函式庫

若在 Ubuntu Linux 中,可以使用 apt 安裝 OpenSSL 函式庫與編譯相關套件:

# 安裝 OpenSSL 函式庫與編譯相關套件
sudo apt install build-essential libssl-dev

AES-256 加密

以下是採用 OpenSSL 加密函式庫,實作 AES-256 搭配 CBC 模式加密的 C 程式碼:

#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/aes.h>
#include <openssl/rand.h>

#define AES_256_KEY_LENGTH 32
#define AES_256_IV_LENGTH 16
#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    // 256 位元密鑰
    unsigned char key[AES_256_KEY_LENGTH] = {
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,
        16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
    };

    // 128 位元 IV
    unsigned char iv[AES_256_IV_LENGTH];

    // 以亂數產生 IV
    RAND_bytes(iv, sizeof(iv));

    // 檢視亂數產生的 IV
    printf("IV:\n");
    BIO_dump_fp(stdout, iv, sizeof(iv));

    // 採用 AES-256 演算法,配置 Cipher 空間
    EVP_CIPHER *cipher = EVP_CIPHER_fetch(NULL, "AES-256-CBC", NULL);

    // Cipher 的 Block Size 值
    int cipherBlockSize = EVP_CIPHER_block_size(cipher);

    // 配置 Cipher Context 空間
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();

    // 初始化加密用的 Cipher Context
    EVP_EncryptInit_ex2(ctx, cipher, key, iv, NULL);

    // 確認密鑰與 IV 長度正確性
    OPENSSL_assert(EVP_CIPHER_CTX_key_length(ctx) == AES_256_KEY_LENGTH);
    OPENSSL_assert(EVP_CIPHER_CTX_iv_length(ctx) == AES_256_IV_LENGTH);

    // 資料輸入與輸出緩衝空間
    unsigned char inBuffer[BUFFER_SIZE], outBuffer[BUFFER_SIZE + cipherBlockSize];
    int inCount, outCount;

    // 開啟輸入與輸出檔案
    FILE *inFile = fopen(argv[1], "rb");
    FILE *outFile = fopen(argv[2], "wb");

    // 將 IV 寫入輸出檔案
    fwrite(iv, sizeof(unsigned char), AES_256_IV_LENGTH, outFile);

    while(1) {
        // 讀取輸入檔案內容
        inCount = fread(inBuffer, sizeof(unsigned char),
                BUFFER_SIZE, inFile);

        // 更新
        EVP_EncryptUpdate(ctx, outBuffer, &outCount, inBuffer, inCount);

        // 寫入輸出檔案
        fwrite(outBuffer, sizeof(unsigned char), outCount, outFile);

        // 若讀取至檔案結尾則跳出
        if (inCount < BUFFER_SIZE) break;
    }

    // 處理結尾 block
    EVP_EncryptFinal_ex(ctx, outBuffer, &outCount);

    // 寫入輸出檔案
    fwrite(outBuffer, sizeof(unsigned char), outCount, outFile);

    // 關閉輸入與輸出檔案
    fclose(inFile);
    fclose(outFile);

    /* 釋放 Cipher 空間 */
    EVP_CIPHER_free(cipher);

    return EXIT_SUCCESS;
}

這裡為了方便初學者閱讀,省略了大部分的錯誤處理程序,標準的做法必須在每條函數呼叫之後,檢查函數傳回值是否正常,建議可參考 OpenSSL 官方的範例寫法

AES 是對稱式演算法,所以在加密時要指定一組密鑰,這裡我們為了方便示範,所以將密鑰寫在程式中,實務上不可以將密鑰寫在程式中,可能的作法很多,例如從其他檔案輸入、隨機產生後儲存於檔案等,或是由使用者從鍵盤輸入密碼,再搭配 PBKDF2scrypt 這類的演算法,產生高強度的密鑰。

由於在加密的時候會使用一組隨機產生的 IV,在解密時也會用到這一組 IV,所以我們直接將其寫在加密檔的開頭,所以後續在解密時也會需要根據同樣的格式將 IV 讀取出來。

EVP_CIPHER_fetch() 可以根據名稱來建立 cipher,而可用的 cipher 名稱可以使用以下指令查詢:

# 查詢可用的 Cipher
openssl list -cipher-algorithms

將這份加密程式碼儲存為 encrypt.c 之後,可以使用以下指令編譯:

# 編譯 AES-256 加密程式
gcc encrypt.c -lcrypto -o encrypt

編譯完成後,會產生 encrypt 這個執行檔,執行時要指定輸入與輸出檔案:

# 將 message.txt 加密後儲存至 message.txt.enc
./encrypt message.txt message.txt.enc

執行後所產生的 message.txt.enc 就是經過 AES-256 加密的檔案了。

AES-256 解密

以下是採用 OpenSSL 加密函式庫,實作 AES-256 搭配 CBC 模式解密的 C 程式碼,加密檔案格式對應上面的加密方式,檔案開頭是 IV,後面接著密文:

#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/aes.h>
#include <openssl/rand.h>

#define AES_256_KEY_LENGTH 32
#define AES_256_IV_LENGTH 16
#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    // 256 位元密鑰
    unsigned char key[AES_256_KEY_LENGTH] = {
         0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15,
        16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31
    };

    // 128 位元 IV
    unsigned char iv[AES_256_IV_LENGTH];

    // 開啟輸入與輸出檔案
    FILE *inFile = fopen(argv[1], "rb");
    FILE *outFile = fopen(argv[2], "wb");

    // 讀取輸入檔案中的 IV
    fread(iv, sizeof(unsigned char), AES_256_IV_LENGTH, inFile);

    // 檢視讀入的 IV
    printf("IV:\n");
    BIO_dump_fp(stdout, iv, sizeof(iv));

    // 採用 AES-256 演算法,配置 Cipher 空間
    EVP_CIPHER *cipher = EVP_CIPHER_fetch(NULL, "AES-256-CBC", NULL);

    // Cipher 的 Block Size 值
    int cipherBlockSize = EVP_CIPHER_block_size(cipher);

    // 配置 Cipher Context 空間
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();

    // 初始化加密用的 Cipher Context
    EVP_DecryptInit_ex2(ctx, cipher, key, iv, NULL);

    // 確認密鑰與 IV 長度正確性
    OPENSSL_assert(EVP_CIPHER_CTX_key_length(ctx) == AES_256_KEY_LENGTH);
    OPENSSL_assert(EVP_CIPHER_CTX_iv_length(ctx) == AES_256_IV_LENGTH);

    // 資料輸入與輸出緩衝空間
    unsigned char inBuffer[BUFFER_SIZE], outBuffer[BUFFER_SIZE + cipherBlockSize];

    int inCount, outCount;
    while(1) {
        // 讀取輸入檔案內容
        inCount = fread(inBuffer, sizeof(unsigned char),
                BUFFER_SIZE, inFile);

        // 更新
        EVP_DecryptUpdate(ctx, outBuffer, &outCount, inBuffer, inCount);

        // 寫入輸出檔案
        fwrite(outBuffer, sizeof(unsigned char), outCount, outFile);

        // 若讀取至檔案結尾則跳出
        if (inCount < BUFFER_SIZE) break;
    }

    // 處理結尾 block
    EVP_DecryptFinal_ex(ctx, outBuffer, &outCount);

    // 寫入輸出檔案
    fwrite(outBuffer, sizeof(unsigned char), outCount, outFile);

    // 關閉輸入與輸出檔案
    fclose(inFile);
    fclose(outFile);

    /* 釋放 Cipher 空間 */
    EVP_CIPHER_free(cipher);

    return EXIT_SUCCESS;
}

將這份解密程式碼儲存為 decrypt.c 之後,可以使用以下指令編譯:

# 編譯 AES-256 解密程式
gcc decrypt.c -lcrypto -o decrypt

編譯完成後,會產生 decrypt 這個執行檔,可用來解密上面 AES-256 加密程式產生的加密檔案:

# 將 message.txt.enc 解密後儲存至 message.txt.out
./decrypt message.txt.enc message.txt.out

測試加密與解密

我們可以利用以下的方式,測試加密與解密程式的效能與正確性:

# 產生 1GB 的測試檔案
head -c 1G /dev/urandom > message.dat

測量加密時間:

# 測量加密時間
time ./encrypt message.dat message.dat.enc
IV:
0000 - d6 c8 13 79 ce 66 0f 51-75 e9 84 5a f2 aa af c2   ...y.f.Qu..Z....

real    0m3.636s
user    0m1.364s
sys     0m2.268s

測量解密時間

# 測量解密時間
time ./decrypt message.dat.enc message.dat.out
IV:
0000 - d6 c8 13 79 ce 66 0f 51-75 e9 84 5a f2 aa af c2   ...y.f.Qu..Z....

real    0m2.775s
user    0m0.580s
sys     0m2.193s

最後檢查加密前與解密後的檔案是否一致:

# 檢查檔案
md5sum message.dat message.dat.out
f390b7d96adfd2ac1c973b296798e993  message.dat
f390b7d96adfd2ac1c973b296798e993  message.dat.out

參考資料

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