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