OpenSSL 使用AES對檔案加解密

微軟技術分享發表於2023-11-28

AES(Advanced Encryption Standard)是一種對稱加密演算法,它是目前廣泛使用的加密演算法之一。AES演算法是由美國國家標準與技術研究院(NIST)於2001年釋出的,它取代了原先的DES(Data Encryption Standard)演算法,成為新的標準。AES是一種對稱加密演算法,意味著加密和解密使用相同的金鑰。這就要求金鑰的安全性非常重要,因為任何擁有金鑰的人都能進行加密和解密操作。其金鑰長度,包括128位、192位和256位。不同長度的金鑰提供了不同級別的安全性,通常更長的金鑰長度意味著更高的安全性。

該演算法支援多種工作模式,其中兩種常見的模式是CBC(Cipher Block Chaining)和ECB(Electronic Codebook)。

  1. CBC 模式(Cipher Block Chaining):
    • 工作原理:
      • CBC模式對每個明文塊進行加密前,先與前一個密文塊進行異或操作。首個塊使用一個初始化向量(IV)與明文異或。這種鏈式反饋機制使得每個密文塊的加密都依賴於前一個塊的密文,從而增加了安全性。
    • 特點:
      • 帶有初始化向量,對同樣的明文塊加密得到的密文塊會隨著其前面的明文塊的不同而不同。
      • 適用於加密長度超過一個塊的資料。
    • 優點和缺點:
      • 優點:提供更高的安全性,適用於加密大塊的資料。
      • 缺點:由於加密是依賴於前一個塊的密文,所以無法進行並行加密處理。
  2. ECB 模式(Electronic Codebook):
    • 工作原理:
      • ECB模式將明文分割成塊,每個塊獨立加密,然後再組合成密文。相同的明文塊將始終加密為相同的密文塊。
    • 特點:
      • 不需要初始化向量,同樣的明文會得到同樣的密文。
      • 適用於加密獨立的資料塊,但對於相同的塊,ECB模式下的輸出相同。
    • 優點和缺點:
      • 優點:簡單,易於實現。
      • 缺點:相同的明文塊生成相同的密文塊,可能導致安全性問題。不適用於加密大塊的資料。

在選擇模式時,需要根據具體的應用場景和需求權衡安全性和效能。一般來說,CBC模式是更安全的選擇,而ECB模式可能更容易實現和理解。在實際應用中,還可以考慮其他模式,如CTR(Counter)模式和GCM(Galois/Counter Mode)模式等,這些模式結合了安全性和效能的考慮。

本次案例中所需要使用的標頭檔案資訊如下所示;

#define  _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <openssl/err.h>
#include <openssl/aes.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
#include <openssl/pem.h>

extern "C"
{
#include <openssl/applink.c>
}

#pragma comment(lib,"libssl_static.lib")
#pragma comment(lib,"libcrypto.lib")

使用CBC模式加解密

Cipher Block Chaining (CBC) 模式是一種對稱加密的分組密碼工作模式。在 CBC 模式中,明文被分成固定大小的塊,並使用加密演算法逐個處理這些塊。每個塊都與前一個塊的密文進行異或運算,然後再進行加密。這個過程導致了一種“連結”效果,因此得名 Cipher Block Chaining。

以下是 CBC 模式的詳細概述:

初始向量 (Initialization Vector, IV)

  • 在 CBC 模式中,每個訊息的第一個塊使用一個初始向量 (IV)。IV 是一個固定長度的隨機數,它在每次加密不同訊息時都應該是唯一的。IV 的作用是在每個塊的加密中引入隨機性,以防止相同的明文塊生成相同的密文塊。

分組加密

  • 訊息被分成固定大小的塊(通常為 64 位元或 128 位元),然後每個塊都被分組加密。最常用的塊加密演算法是 AES。

異或運算

  • 在每個塊加密之前,明文塊與前一個密文塊進行異或運算。這就是“連結”發生的地方。第一個塊與 IV 異或。

加密

  • 異或運算後的結果被送入塊加密演算法進行加密。得到的密文塊成為下一個塊的 IV。

解密

  • 在解密時,密文塊被送入塊解密演算法進行解密。解密後的結果與前一個密文塊進行異或運算,得到明文塊。

模式序列化

  • CBC 模式是序列的,因為每個塊的加密都依賴於前一個塊的密文。這也意味著無法並行處理整個訊息。

填充

  • 如果明文的長度不是塊大小的整數倍,需要進行填充。常見的填充方案有 PKCS#7 填充。

安全性

  • 當使用 CBC 模式時,密文塊的順序對安全性至關重要。如果訊息的兩個塊對調,解密後會得到不同的明文。因此,必須保證密文塊的順序不被篡改。

使用場景

  • CBC 模式常用於保護傳輸層安全協議(如 TLS)中,以提供加密和資料完整性。

總體而言,CBC 模式提供了一種相對強大的加密方法,但在實現時需要注意使用隨機且不可預測的 IV 以及處理填充的問題。

AES_set_encrypt_key 函式。具體來說,它用於將原始金鑰設定為可以在 AES 加密演算法中使用的格式。以下是該函式的原型:

int AES_set_encrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
  • userKey:指向用於設定金鑰的輸入資料的指標,即原始金鑰。
  • bits:金鑰長度,以位元為單位。在使用 AES 加密演算法時,通常為 128、192 或 256。
  • key:指向 AES_KEY 結構的指標,用於儲存設定後的金鑰資訊。

該函式返回值為零表示成功,非零表示失敗。成功呼叫後,key 引數中儲存了經過格式化的金鑰資訊,可以在後續的 AES 加密操作中使用。

AES_cbc_encrypt 是 OpenSSL 庫中用於執行 AES 演算法中的 Cipher Block Chaining (CBC) 模式的函式。在 CBC 模式中,每個明文塊在加密之前會與前一個密文塊進行異或運算,以增加密碼的隨機性。

以下是 AES_cbc_encrypt 函式的原型:

void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, size_t length, const AES_KEY *key, unsigned char *ivec, const int enc);
  • in:指向輸入資料(明文)的指標。
  • out:指向輸出資料(密文)的指標。
  • length:資料的長度,以位元組為單位。
  • key:指向 AES_KEY 結構的指標,其中包含了加密金鑰。
  • ivec:Initialization Vector(IV),用於增強密碼的隨機性,也是前一個密文塊。在 CBC 模式中,IV 對於第一個資料塊是必需的,之後的 IV 由前一個密文塊決定。
  • enc:指定操作是加密(AES_ENCRYPT)還是解密(AES_DECRYPT)。

AES_set_decrypt_key 函式。該函式用於將加密時使用的金鑰調整為解密時使用的金鑰,以便進行解密操作。

以下是 AES_set_decrypt_key 函式的原型:

int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
  • userKey:指向用於設定解密金鑰的輸入金鑰資料的指標。
  • bits:金鑰長度,以位元為單位。支援的長度包括 128、192 和 256 位元。
  • key:指向 AES_KEY 結構的指標,該結構將儲存設定後的解密金鑰。

實現加解密功能,如下openssl_aes_cbc_encrypt用於使用CBC模式加密資料,openssl_aes_cbc_decrypt則相反用於解密資料。

// 初始化金鑰
const unsigned char key[AES_BLOCK_SIZE] = { 0x12,0x55,0x64,0x69,0xf1 };

// 初始化向量
unsigned char iv[AES_BLOCK_SIZE] = { 0 };

// AES CBC 模式加密
// 引數:
// - in: 待加密的資料
// - len: 待加密資料的長度
// - out: 存放加密結果的緩衝區
// 返回值:
// - 返回填充後加密資料的長度,失敗返回-1
int openssl_aes_cbc_encrypt(char* in, size_t len, char* out)
{
	AES_KEY aes;

	// 填充資料為AES_BLOCK_SIZE的整數倍
	char* aesIn;
	int blockNum, aesInLen;

	// 設定加密金鑰
	if (AES_set_encrypt_key(key, 128, &aes) < 0)
	{
		return -1;
	}

	// 判斷原始資料長度是否AES_BLOCK_SIZE的整數倍
	if ((len % AES_BLOCK_SIZE) != 0)
	{
		// 不是整數倍則用0填充
		blockNum = len / AES_BLOCK_SIZE + 1;
		aesInLen = blockNum * AES_BLOCK_SIZE;
		aesIn = (char*)calloc(aesInLen, 1);
		memcpy(aesIn, in, len);
	}
	else
	{
		aesInLen = len;
		aesIn = (char*)calloc(aesInLen, 1);
		memcpy(aesIn, in, len);
	}

	// AES CBC 模式加密
	AES_cbc_encrypt((unsigned char*)aesIn, (unsigned char*)out, aesInLen, &aes, iv, AES_ENCRYPT);

	// 釋放分配的記憶體
	free(aesIn);

	// 返回填充後加密資料的長度
	return aesInLen;
}

// AES CBC 模式解密
// 引數:
// - in: 待解密的資料
// - len: 待解密資料的長度
// - out: 存放解密結果的緩衝區
// 返回值:
// - 成功返回0,失敗返回-1
int openssl_aes_cbc_decrypt(char* in, size_t len, char* out)
{
	AES_KEY aes;
	
	// 設定解密金鑰
	if (AES_set_decrypt_key(key, 128, &aes) < 0)
	{
		return -1;
	}

	// AES CBC 模式解密
	AES_cbc_encrypt((unsigned char*)in, (unsigned char*)out, len, &aes, iv, AES_DECRYPT);

	// 返回成功
	return 0;
}

當需要對資料加密時,首先開啟被加密檔案這裡我們開啟的時csdn.zip檔案,加密後會寫出為csdn.cbc檔案;

int main(int argc, char* argv[])
{
	// 存放填充位元組數的陣列
	char offset[4] = { '0' };

	char* src = nullptr, *dst = nullptr;
	int inlen, outlen, size;
	FILE* srcFile, *dstFile;

	// 開啟被加密原始檔
	srcFile = fopen("d://comp/csdn.zip", "rb");

	// 加密後寫出檔案
	dstFile = fopen("d://comp/csdn.cbc", "wb+");

	// 獲取檔案大小
	fseek(srcFile, 0, SEEK_END);
	inlen = ftell(srcFile);
	if (inlen < 0)
	{
		return 0;
	}
	fseek(srcFile, 0, SEEK_SET);

	// -------------------------------------------------------
	// 開始加密
	src = (char*)calloc(inlen, 1);
	size = fread(src, 1, inlen, srcFile);
	std::cout << "讀入位元組: " << size << std::endl;

	// 輸出變數申請的空間額外增加16位元組
	outlen = (inlen / 16 + 1) * 16;
	dst = (char*)calloc(outlen, 1);

	// 呼叫加密函式
	size = openssl_aes_cbc_encrypt(src, inlen, dst);

	// 獲取填充的位元組數,記錄到輸出檔案的前4個位元組內
	sprintf(offset, "%d", size - inlen);
	fwrite(offset, sizeof(char), 4, dstFile);

	// -------------------------------------------------------
	// 輸出加密後的檔案或者解密後的檔案,檔案大小應與原始檔案一致
	size = fwrite(dst, 1, size, dstFile);
	std::cout << "輸出檔案大小: " << size << std::endl;

	fcloseall();
	free(src);
	free(dst);
	system("pause");
	return 0;
}

執行後輸出效果圖如下所示;

解密時同樣需要開啟檔案,將加密檔案csdn.cbc開啟,並解密輸出成csdnde.zip檔案;

int main(int argc, char* argv[])
{
	// 存放填充位元組數的陣列
	char offset[4] = { '0' };

	char* src = nullptr, *dst = nullptr;
	int inlen, outlen, size;
	FILE* srcFile, *dstFile;

	// 開啟加密後的檔案
	srcFile = fopen("d://comp/csdn.cbc", "rb");

	// 解密後寫出的檔案
	dstFile = fopen("d://comp/csdnde.zip", "wb+");

	// 獲取檔案大小
	fseek(srcFile, 0, SEEK_END);
	inlen = ftell(srcFile);
	if (inlen < 0)
	{
		return 0;
	}
	fseek(srcFile, 0, SEEK_SET);

	// -------------------------------------------------------
	fread(offset, sizeof(char), 4, srcFile);
	inlen -= 4;
	src = (char*)calloc(inlen, 1);

	// 從加密後的檔案中獲取填充的位元組數
	size = fread(src, 1, inlen, srcFile);
	std::cout << "讀入位元組: " << size << std::endl;

	// 得到原始檔案的大小
	size = size - atoi(offset);

	outlen = (inlen / 16 + 1) * 16;
	dst = (char*)calloc(outlen, 1);

	// 解密
	openssl_aes_cbc_decrypt(src, inlen, dst);

	// -------------------------------------------------------

	// 輸出加密後的檔案或者解密後的檔案,檔案大小應與原始檔案一致
	size = fwrite(dst, 1, size, dstFile);
	std::cout << "輸出檔案大小: " << size << std::endl;

	fcloseall();
	free(src);
	free(dst);
	system("pause");
	return 0;
}

執行後輸出效果圖如下所示;

使用ECB模式加解密

Electronic Codebook (ECB) 模式是一種對稱加密的分組密碼工作模式。在 ECB 模式中,每個明文塊都被獨立加密,不受其他塊的影響。這意味著相同的明文塊將始終生成相同的密文塊,這可能導致一些安全性問題。

以下是 ECB 模式的詳細概述:

分組加密

  • 訊息被分成固定大小的塊(通常為 64 位元或 128 位元),然後每個塊都被獨立加密。最常用的塊加密演算法是 AES。

無連結

  • 在 ECB 模式中,每個塊的加密是獨立的,不會受到前一個或後一個塊的影響。這意味著相同的明文塊將生成相同的密文塊。

模式序列化

  • ECB 模式允許對整個訊息進行並行處理,因為每個塊都是獨立加密的。這是與 CBC 模式相比的一個優勢,因為它允許更高效的實現。

填充

  • 如果明文的長度不是塊大小的整數倍,需要進行填充。常見的填充方案有 PKCS#7 填充。

安全性問題

  • 主要的安全性問題在於相同的明文塊生成相同的密文塊,這可能導致一些攻擊。例如,如果兩個塊的內容相同,那麼它們的密文也將相同。

使用場景

  • 由於安全性問題,ECB 模式並不適合所有場景。一般來說,ECB 模式主要用於對稱加密演算法的基本理解和學術研究,而在實際應用中更常使用其他工作模式,如 CBC 或 GCM。

總體而言,ECB 模式是一種簡單的分組密碼工作模式,但由於安全性問題,實際應用中更常使用其他工作模式。

AES_ecb_encrypt 是 OpenSSL 庫中用於執行 AES 演算法的 ECB 模式加密的函式。下面是對該函式的詳細概述:

int AES_ecb_encrypt(const unsigned char *input, unsigned char *output, const AES_KEY *key, const int enc);

引數說明:

  • input: 要加密的資料的輸入緩衝區的指標。
  • output: 加密後的資料的輸出緩衝區的指標。
  • key: AES 金鑰的結構體指標,其中包含了加密所需的金鑰資訊。
  • enc: 一個整數值,用於指定是執行加密(AES_ENCRYPT)還是解密(AES_DECRYPT)操作。

返回值:

  • 返回 0 表示成功,其他值表示錯誤。

功能說明:

  • AES_ecb_encrypt 函式用於在 ECB 模式下執行 AES 演算法的加密或解密操作,具體取決於 enc 引數。
  • 在 ECB 模式下,該函式將輸入的資料塊獨立地加密(或解密),每個塊的輸出結果不受前後塊的影響。
  • 函式透過 key 引數提供的金鑰資訊執行加密或解密操作。

AES_ecb_encrypt 是 OpenSSL 庫中用於執行 AES 演算法的 ECB 模式加密或解密的函式。下面是對該函式的詳細概述:

int AES_ecb_encrypt(const unsigned char *input, unsigned char *output, const AES_KEY *key, const int enc);

引數說明:

  • input: 要加密或解密的資料塊的輸入緩衝區指標。
  • output: 加密或解密後的資料塊的輸出緩衝區指標。
  • key: AES 金鑰的結構體指標,包含了加密或解密所需的金鑰資訊。
  • enc: 一個整數值,用於指定是執行加密(AES_ENCRYPT)還是解密(AES_DECRYPT)操作。

返回值:

  • 返回 0 表示成功,其他值表示錯誤。

功能說明:

  • AES_ecb_encrypt 函式用於在 ECB 模式下執行 AES 演算法的加密或解密操作,具體取決於 enc 引數。
  • 在 ECB 模式下,該函式將輸入的資料塊獨立地加密(或解密),每個塊的輸出結果不受前後塊的影響。
  • 函式透過 key 引數提供的金鑰資訊執行加密或解密操作。
// AES ECB 模式加密
// 引數:
// - in: 待加密的資料
// - len: 待加密資料的長度
// - out: 存放加密結果的緩衝區
// 返回值:
// - 成功返回填充後加密資料的長度,失敗返回-1
int openssl_aes_ecb_enrypt(char* in, size_t len, char* out)
{
	int i;
	int blockNum;
	int aesInLen;
	char* aesIn;
	AES_KEY aes;

	// 設定加密金鑰
	if (AES_set_encrypt_key(key, 128, &aes) < 0)
		return -1;
	// 判斷原始資料長度是否AES_BLOCK_SIZE的整數倍
	if ((len % AES_BLOCK_SIZE) != 0)
	{
		blockNum = len / AES_BLOCK_SIZE + 1;
		aesInLen = blockNum * AES_BLOCK_SIZE;
		aesIn = (char*)calloc(aesInLen, 1);
		memcpy(aesIn, in, len);
	}
	else
	{
		blockNum = len / AES_BLOCK_SIZE;
		aesInLen = len;
		aesIn = (char*)calloc(aesInLen, 1);
		memcpy(aesIn, in, len);
	}

	// 由於ECB每次只處理AES_BLOCK_SIZE大小的資料,所以透過迴圈完成所有資料的加密
	for (i = 0; i < blockNum; i++)
	{
		AES_ecb_encrypt((unsigned char*)aesIn, (unsigned char*)out, &aes, AES_ENCRYPT);
		aesIn += AES_BLOCK_SIZE;
		out += AES_BLOCK_SIZE;
	}

	// 釋放記憶體
	// free(aesIn);
	// 返回填充後加密資料的長度
	return aesInLen;
}

// AES ECB 模式解密
// 引數:
// - in: 待解密的資料
// - len: 待解密資料的長度
// - out: 存放解密結果的緩衝區
// 返回值:
// - 成功返回0,失敗返回-1
int openssl_aes_ecb_decrypt(char* in, size_t len, char* out)
{
	unsigned int i;
	AES_KEY aes;
	// 設定解密金鑰
	if (AES_set_decrypt_key(key, 128, &aes) < 0)
	{
		return -1;
	}
	// 迴圈解密每個資料塊
	for (i = 0; i < len / AES_BLOCK_SIZE; i++)
	{
		AES_ecb_encrypt((unsigned char*)in, (unsigned char*)out, &aes, AES_DECRYPT);
		in += AES_BLOCK_SIZE;
		out += AES_BLOCK_SIZE;
	}
	// 返回成功
	return 0;
}

當需要對資料加密時,首先開啟被加密檔案這裡我們開啟的時csdn.zip檔案,加密後會寫出為csdn.ecb檔案;

int main(int argc, char* argv[])
{
	// 存放填充位元組數的陣列
	char offset[4] = { '0' };

	char* src = nullptr, *dst = nullptr;
	int inlen, outlen, size;
	FILE* srcFile, *dstFile;

	// 開啟被加密原始檔
	srcFile = fopen("d://comp/csdn.zip", "rb");

	// 加密後寫出檔案
	dstFile = fopen("d://comp/csdn.ecb", "wb+");

	// 獲取檔案大小
	fseek(srcFile, 0, SEEK_END);
	inlen = ftell(srcFile);
	if (inlen < 0)
	{
		return 0;
	}
	fseek(srcFile, 0, SEEK_SET);

	// -------------------------------------------------------
	// 開始加密
	src = (char*)calloc(inlen, 1);
	size = fread(src, 1, inlen, srcFile);
	std::cout << "讀入位元組: " << size << std::endl;

	// 輸出變數申請的空間額外增加16位元組
	outlen = (inlen / 16 + 1) * 16;
	dst = (char*)calloc(outlen, 1);

	// ECB加密
	size = openssl_aes_ecb_enrypt(src, inlen, dst);
	sprintf(offset, "%d", size - inlen);
	fwrite(offset, sizeof(char), 4, dstFile);

	// -------------------------------------------------------
	// 輸出加密後的檔案或者解密後的檔案,檔案大小應與原始檔案一致
	size = fwrite(dst, 1, size, dstFile);
	std::cout << "輸出檔案大小: " << size << std::endl;

	fcloseall();
	free(src);
	free(dst);
	system("pause");
	return 0;
}

執行後輸出效果圖如下所示;

解密時同樣需要開啟檔案,將加密檔案csdn.ecb開啟,並解密輸出成csdnde.zip檔案;

int main(int argc, char* argv[])
{
	// 存放填充位元組數的陣列
	char offset[4] = { '0' };

	char* src = nullptr, *dst = nullptr;
	int inlen, outlen, size;
	FILE* srcFile, *dstFile;

	// 開啟加密後的檔案
	srcFile = fopen("d://comp/csdn.ecb", "rb");

	// 解密後寫出的檔案
	dstFile = fopen("d://comp/csdnde.zip", "wb+");

	// 獲取檔案大小
	fseek(srcFile, 0, SEEK_END);
	inlen = ftell(srcFile);
	if (inlen < 0)
	{
		return 0;
	}
	fseek(srcFile, 0, SEEK_SET);

	// -------------------------------------------------------
	fread(offset, sizeof(char), 4, srcFile);
	inlen -= 4;
	src = (char*)calloc(inlen, 1);

	// 從加密後的檔案中獲取填充的位元組數
	size = fread(src, 1, inlen, srcFile);
	std::cout << "讀入位元組: " << size << std::endl;

	// 得到原始檔案的大小
	size = size - atoi(offset);

	outlen = (inlen / 16 + 1) * 16;
	dst = (char*)calloc(outlen, 1);

	// 解密
	openssl_aes_ecb_decrypt(src, inlen, dst);

	// -------------------------------------------------------

	// 輸出加密後的檔案或者解密後的檔案,檔案大小應與原始檔案一致
	size = fwrite(dst, 1, size, dstFile);
	std::cout << "輸出檔案大小: " << size << std::endl;

	fcloseall();
	free(src);
	free(dst);
	system("pause");
	return 0;
}

執行後輸出效果圖如下所示;

相關文章