介紹如何在 C 語言程式中使用 OpenSSL 函式庫,以 AES 對稱式加密演算法實作資料的加密與解密。
若在 Ubuntu Linux 中,可以使用 apt
安裝 OpenSSL 函式庫與編譯相關套件:
# 安裝 OpenSSL 函式庫與編譯相關套件
sudo apt install build-essential libssl-dev
以下是採用 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 加密的檔案了。
以下是採用 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