C/C++ OpenSSL AES encryption/decryption 加密解密範例

本篇 ShengYu 介紹 C/C++ OpenSSL AES 256 CBC encryption/decryption 加密解密範例,AES 是典型的對稱式加密演算法,對稱式加密演算法是可逆的,也就是用一個金鑰加密後可以再用同一個金鑰解密回來,而 AES 全名是 Advanced Encryption Standard 是用來取代原先的 DES (Data Encryption Standard) 演算法,AES 是目前主流的加密演算法,常見對稱式加密演算法的應用像是將檔案壓成壓縮時 (zip/7-zip) 如果要設定密碼加密就會使用到。

C/C++ OpenSSL AES-256 CBC

AES 提供了幾種模式,分別為 ECB、CBC、CFB、CTR、OFB 五種模式,這邊介紹 C/C++ OpenSSL AES 256 CBC encryption/decryption 加密解密範例,在 openssl 可以常看到 encrypt 與 decrypt 關鍵字,encrypt 表示加密,decrypt 表示解密,在本範例中我們會使用 AES_cbc_encrypt() 做加密,解密的話則是使用 aes_cbc_decrypt()

AES 的區塊長度固定為 128 bits (16 bytes),即多輪且每次對 128 bits 明文區塊作加密,而不是一次對整個明文作加密,明文長度不是 128 bits 的整數倍的話,剩餘不足 128 bits 的區塊會用填充 (Padding) 的方式,填充 (Padding) 的方式有好幾種,最簡單就是用零填充 ZeroBytePadding,常用填充方式為 PKCS5Padding 或 PKCS7Padding,需要注意的是加密用哪一種填充方式,解密時也要同用一種填充方式。

key 就是加密過程中會用到的金鑰,AES 的 key 金鑰長度則可以是 128、192 或 256 bits,也就是平常大家說的 AES-128、AES-192 或 AES-256,以安全性來說 AES-256 安全性最高。

iv 就是初始向量 (Initialization Vector),在加密過程中,原本相同明文區塊使用相同金鑰加密後的密文會相同,加入 iv 可讓每次的相同明文區塊使用相同金鑰加密後的密文不同,
用來防止同樣的內容產生同樣的加密資料,解密時用的 iv 必須跟加密的 iv 內容一樣,長度必須為 16 bytes (128 bits),在使用 AES_cbc_encrypt() 加密時會修改 iv 的數值,所以在 aes_cbc_decrypt() 解密時務必確認是用相同的 iv。

另外 openssl command 還提供了 salt 的選項,salt 就是加鹽的意思,是個隨機產生的資料,在密碼 password 推導成金鑰 key 時可以使用,使用 salt 的話相同的密碼 password 就不會每次都推導成相同的金鑰 key 了。

以下的範例是直接設定金鑰 key 與 iv,沒有使用 salt,

cpp-aes-cbc.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <openssl/aes.h>
#include <iostream>
#include <vector>
#include <string>

int main() {
vector<unsigned char> key = from_hex_string("2B7E151628AED2A6ABF7158809CF4F3C");
vector<unsigned char> iv = from_hex_string("000102030405060708090A0B0C0D0E0F");
vector<unsigned char> plain = from_hex_string("6BC1BEE22E409F96E93D7E117393172A");

vector<unsigned char> cipher = aes_128_cbc_encrypt(plain, key, iv);

cout << "plain : " << to_hex_string(plain) << endl;
cout << "cipher : " << to_hex_string(cipher) << endl;
cout << "iv : " << to_hex_string(iv) << endl;

vector<unsigned char> decrypt_text = aes_128_cbc_decrypt(cipher, key, iv);

cout << "decrypt: " << to_hex_string(decrypt_text) << endl;
return 0;
}

加密的函式,

cpp-aes-cbc.cpp
1
2
3
4
5
6
7
8
9
10
11
vector<unsigned char> aes_128_cbc_encrypt(vector<unsigned char> &plain,
vector<unsigned char> &key,
vector<unsigned char> iv) {

AES_KEY ctx;
AES_set_encrypt_key(key.data(), 128, &ctx);
vector<unsigned char> cipher(16);
AES_cbc_encrypt(plain.data(), cipher.data(), 16, &ctx, iv.data(), AES_ENCRYPT);

return cipher;
}

解密的函式,

cpp-aes-cbc.cpp
1
2
3
4
5
6
7
8
9
10
11
vector<unsigned char> aes_128_cbc_decrypt(vector<unsigned char> &cipher,
vector<unsigned char> &key,
vector<unsigned char> iv) {

AES_KEY ctx;
AES_set_decrypt_key(key.data(), 128, &ctx);
vector<unsigned char> plain(16);
AES_cbc_encrypt(cipher.data(), plain.data(), 16, &ctx, iv.data(), AES_DECRYPT);

return plain;
}

結果輸出如下,

1
2
3
4
plain  : 6BC1BEE22E409F96E93D7E117393172A
cipher : 7649ABAC8119B246CEE98E9B12E9197D
iv : 7649ABAC8119B246CEE98E9B12E9197D
decrypt: 6BC1BEE22E409F96E93D7E117393172A

這邊列出一些網路上一些 AES 的 Test Vectors 測試向量可以測試看看加密後資料是否一致,這邊使用 Apple 提供的 4 組 AES-128-CBC Test Vectors,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Cipher = AES-128-CBC
Key = 2B7E151628AED2A6ABF7158809CF4F3C
IV = 000102030405060708090A0B0C0D0E0F
Plaintext = 6BC1BEE22E409F96E93D7E117393172A
Ciphertext = 7649ABAC8119B246CEE98E9B12E9197D

Cipher = AES-128-CBC
Key = 2B7E151628AED2A6ABF7158809CF4F3C
IV = 7649ABAC8119B246CEE98E9B12E9197D
Plaintext = AE2D8A571E03AC9C9EB76FAC45AF8E51
Ciphertext = 5086CB9B507219EE95DB113A917678B2

Cipher = AES-128-CBC
Key = 2B7E151628AED2A6ABF7158809CF4F3C
IV = 5086CB9B507219EE95DB113A917678B2
Plaintext = 30C81C46A35CE411E5FBC1191A0A52EF
Ciphertext = 73BED6B8E3C1743B7116E69E22229516

Cipher = AES-128-CBC
Key = 2B7E151628AED2A6ABF7158809CF4F3C
IV = 73BED6B8E3C1743B7116E69E22229516
Plaintext = F69F2445DF4F9B17AD2B417BE66C3710
Ciphertext = 3FF1CAA1681FAC09120ECA307586E1A7

將這些 Key、IV、Plaintext 替換程式中的值,再看看程式執行完的 Ciphertext 是否一致。

加密前,計算原始明文長度如果使用 strlen 的話會有個缺點,strlen 是計算到 '\0' 結束字元為止,如果原始明文資料中間就有包含 '\0' 的話就會被 strlen 給截斷,那麼結果就是只會加密 '\0' 前面這一段,所以通常還是要傳入真正的原始明文資料長度,而不是使用 strlen 去計算明文長度。

Padding 填充

以 PKCS5Padding 或 PKCS7Padding 為例,假設 block size 為 16 byte,那麼明文資料長度不足 16 的倍數就需要填充,如果明文資料長度是 15/31/47 byte 的話就填充 1 byte,填充的數值為 1,如果明文資料長度是 14/30/46 byte 的話就填充 2 byte,填充的數值為 2,如果明文資料長度是 13/29/45 byte 的話就填充 3 byte,填充的數值為 3,等等依此類推。

如果明文資料長度剛好是 16 的整數倍就額外填充 16 byte,填充的數值為 16,這一步很重要而且是必須的!為什麼明文資料長度為 16 整數倍時還要多填充 16 byte 呢?因為這樣解密出來明文才能區分最後的 byte 資料是填充的資料而不是原始明文資料的一部分。舉個例子,假設我原始明文資料長度為 16 且最後 1 個 byte 為 1,如果不填充的話,經過加密再解密回來時,那要怎麼知道資料的最後 1 個 byte 的 1 是填充還是原始明文資料呢?解決方法就是明文長度是整數倍時還是進行填充。

以下以 block size 為 4 byte 為例,那麼填充的各種結果會是這樣,

1
2
3
4
DD DD DD DD | DD DD DD 01
DD DD DD DD | DD DD 02 02
DD DD DD DD | DD 03 03 03
DD DD DD DD | 04 04 04 04

PKCS5Padding 與 PKCS7Padding 在實作中基本上是完全相同的,只是 PKCS5Padding 是用來處理 block size 長度為 8 byte (64 bit),所以 AES block size 為 16 的話是要使用 PKCS7Padding 才比較正確。

C/C++ OpenSSL AES-256 CBC 使用 EVP API

後來 openssl 還有推出了 EVP 的 API,EVP 的 API 提供了所有對稱式加密演算法的統一介面,對開發者來說就可以很輕易的就換成其他演算法,另外 EVP API 還有硬體加速的部分,關於 EVP API 的討論可以看看這篇這篇,下一篇就介紹怎麼用 EVP API 來寫 AES-256 CBC。

C++ class that interfaces to OpenSSL ciphers – Joe’s Blog
https://joelinoff.com/blog/?p=664
https://github.com/jlinoff/openssl-aes-cipher
這個人寫了一個 C++ Cipher class for OpenSSL AES-256-CBC,


其他參考
進階加密標準 - 維基百科,自由的百科全書
https://zh.wikipedia.org/wiki/%E9%AB%98%E7%BA%A7%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86
高級加密標准AES的工作模式(ECB、CBC、CFB、OFB)_天天向上99的博客-CSDN博客_aes cfb
https://blog.csdn.net/charleslei/article/details/48710293
漫画:什么是 AES 算法?
漫画:AES 算法的底层原理

填充模式
Padding (cryptography) - Wikipedia
https://en.wikipedia.org/wiki/Padding_(cryptography)
Day 22. 加密演算法要注意的那些毛 (二) - 填充模式 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
https://ithelp.ithome.com.tw/articles/10250386
(瞭解 PKCS#5 的 PKCS#7 的差別)
cryptography - How does PKCS7 not lose data? - Stack Overflow
https://stackoverflow.com/questions/7447242/how-does-pkcs7-not-lose-data

Salt and IV 的差異
encryption - Passphrase, Salt and IV, do I need all of these? - Stack Overflow
https://stackoverflow.com/questions/1905112/passphrase-salt-and-iv-do-i-need-all-of-these
encryption - Why would you need a salt for AES-CBS when IV is already randomly generated and stored with the encrypted data? - Information Security Stack Exchange
https://security.stackexchange.com/questions/48000/why-would-you-need-a-salt-for-aes-cbs-when-iv-is-already-randomly-generated-and

AES Test Vectors 測試向量
aes 加密算法 测试向量(Test Vectors)示例 - 天行常
https://github.com/Anexsoft/Bolt-CMS/blob/master/vendor/passwordlib/passwordlib/test/Data/Vectors/aes-ofb.test-vectors
https://boringssl.googlesource.com/boringssl/+/2490/crypto/cipher/test/cipher_test.txt
https://opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/test/evptests.txt.auto.html

其它相關文章推薦
OpenSSL AES encryption 對稱式加密指令用法與範例
Ubuntu 2 種安裝 OpenSSL 的方法
macOS 2 種安裝 OpenSSL 的方法