1. 程式人生 > >高階加密標準AES的工作模式(ECB、CBC、CFB、OFB)

高階加密標準AES的工作模式(ECB、CBC、CFB、OFB)

最近在重構之前寫的HTTP代理,這個代理是由代理客戶端和代理服務端組成的,二者之前使用SSL保證通訊內容不會受到中間人(MITM)攻擊。而新的實現打算移除SSL,因為SSL握手的開銷過大,尤其是客戶端與服務端之間隔了個太平洋,另一方面本月中旬的時候Google安全團隊證明了SSLv3已經是不安全的了,需要升級到TLS,但TLS同樣有握手的開銷。在新的實現中客戶端和服務端之間的通訊將使用AES加密,每個連線使用獨立的隨機生成的金鑰和初始化向量。客戶端在向服務端發起連線後使用非對稱加密演算法RSA將金鑰和初始化向量加密後傳送給服務端,服務端在收到金鑰和初始化向量後就全部使用AES加密通訊,這保證了通訊內容不會被竊聽(但可能被篡改)。
本文作為備忘記錄了學習分組加密模式的一些體會和理解。AES作為一種分組加密演算法為了適應不同的安全性要求和傳輸需求允許在多種不同的加密模式下工作,本文只涉及到ECB、CBC、CFB和OFB四種加密模式,以OpenSSL開源庫和C++語言作為描述。

高階加密標準AES

高階加密標準(Advanced Encryption Standard: AES)是美國國家標準與技術研究院(NIST)在2001年建立了電子資料的加密規範。它是一種分組加密標準,每個加密塊大小為128位,允許的金鑰長度為128、192和256位。
下面列出了AES在OpenSSL中最主要的函式

1
2
3
4
int AES_set_encrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
int AES_set_decrypt_key(const unsigned char *userKey, const
int bits, AES_KEY *key)
;

void AES_encrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key);
void AES_decrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key);

這些函式宣告在<openssl/aes.h>檔案中,其中AES_set_encryp_keyAES_set_decrypt_key用來為加密器和解密器設定金鑰,AES_encrypt用來加密單塊資料(128位),AES_decrypt

用來解密單塊資料。OpenSSL中對ECB、CBC、CFB和OFB等加密模式都是對這兩個函式的封裝。
為了描述方便引入兩個C++函式from_hex_stringto_hex_string前者用來將16進位制字串轉為對應的二進位制向量,後者用來將二進位制向量序列化成16進位制的字串。

1
2
std::vector<unsigned char> from_hex_string(const std::string& hex);
std::string to_hex_string(const std::vector<unsigned char>& vec);

一個簡單的使用AES加密一個數據塊並解密的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto key = from_hex_string("2B7E151628AED2A6ABF7158809CF4F3C");
auto plan_vec = from_hex_string("6BC1BEE22E409F96E93D7E117393172A");
std::vector<unsigned char> cipher_vec(16);
std::vector<unsigned char> decrypt_vec(16);

// aes encrypt
AES_KEY aes_enc_ctx;
AES_set_encrypt_key(key.data(), 128, &aes_enc_ctx);
AES_encrypt(plan_vec.data(), cipher_vec.data(), &aes_enc_ctx);

// aes decrypt
AES_KEY aes_dec_ctx;
AES_set_decrypt_key(key.data(), 128, &aes_dec_ctx);
AES_decrypt(cipher_vec.data(), decrypt_vec.data(), &aes_dec_ctx);

std::cout << "plan : " << to_hex_string(plan_vec) << std::endl;
std::cout << "cipher : " << to_hex_string(cipher_vec) << std::endl;
std::cout << "decrypt: " << to_hex_string(decrypt_vec) << std::endl;

第8行設定128位金鑰2B7E151628AED2A6ABF7158809CF4F3C,第9行使用AES_encrypt對128位的資料塊6BC1BEE22E409F96E93D7E117393172A進行加密,第13、14行使用與加密時相同的金鑰解密。
輸出

1
plan   : 6BC1BEE22E409F96E93D7E117393172A
cipher : 3AD77BB40D7A3660A89ECAF32466EF97
decrypt: 6BC1BEE22E409F96E93D7E117393172A

ECB模式(電子密碼本模式:Electronic codebook)

ECB是最簡單的塊密碼加密模式,加密前根據加密塊大小(如AES為128位)分成若干塊,之後將每塊使用相同的金鑰單獨加密,解密同理。

ECB加密流程(圖片來自維基百科)ECB加密流程(圖片來自維基百科)

ECB解密流程(圖片來自維基百科)ECB解密流程(圖片來自維基百科)

OpenSSL中針對ECB模式,有個名為AES_ecb_encrypt的函式,它的實現是這樣的(其實就是AES_encrypt和AES_decrypt套了個馬甲)。

1
2
3
4
5
6
7
8
void AES_ecb_encrypt(const unsigned char *in, unsigned char *out,const AES_KEY *key, const int enc) {
assert(in && out && key);
assert((AES_ENCRYPT == enc)||(AES_DECRYPT == enc));
if (AES_ENCRYPT == enc)
AES_encrypt(in, out, key);
else
AES_decrypt(in, out, key);
}

ECB模式由於每塊資料的加密是獨立的因此加密和解密都可以平行計算,ECB模式最大的缺點是相同的明文塊會被加密成相同的密文塊,這種方法在某些環境下不能提供嚴格的資料保密性。

CBC模式(密碼分組連結:Cipher-block chaining)

CBC模式對於每個待加密的密碼塊在加密前會先與前一個密碼塊的密文異或然後再用加密器加密。第一個明文塊與一個叫初始化向量的資料塊異或。

CBC加密流程(圖片來自維基百科)CBC加密流程(圖片來自維基百科)

CBC解密流程(圖片來自維基百科)CBC解密流程(圖片來自維基百科)

OpenSSL中用於AES-CBC加密的函式是AES_cbc_encrypt

1
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:當enc為AES_ENCRYPT時此引數要用AES_set_encrypt_key初始化,當enc為AES_DECRYPT時要用AES_set_decrypt_key初始化
  • enc:加密AES_ENCRYPT 解密AES_DECRYPT

AES_cbc_encrypt允許length不是16(128位)的整數倍,不足的部分會用0填充,輸出總是16的整數倍。完成加密或解密後會更新初始化向量IV。舉例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
auto key = from_hex_string("2B7E151628AED2A6ABF7158809CF4F3C");
auto i_vec = from_hex_string("000102030405060708090A0B0C0D0E0F");
auto plan_vec = from_hex_string("6B");

AES_KEY aes_enc_ctx;
AES_set_encrypt_key(key.data(), 128, &aes_enc_ctx);
std::vector<unsigned char> cipher_vec(16);
AES_cbc_encrypt(plan_vec.data(), cipher_vec.data(), 1, &aes_enc_ctx, i_vec.data(), AES_ENCRYPT);

std::cout << "plan : " << to_hex_string(plan_vec) << std::endl;
std::cout << "cipher : " << to_hex_string(cipher_vec) << std::endl;
std::cout << "ivec : " << to_hex_string(i_vec) << std::endl;

AES_KEY aes_dec_ctx;
AES_set_decrypt_key(key.data(), 128, &aes_dec_ctx);
std::vector<unsigned char> decrypt_vec(16);
i_vec = from_hex_string("000102030405060708090A0B0C0D0E0F");
AES_cbc_encrypt(cipher_vec.data(), decrypt_vec.data(), 16, &aes_dec_ctx, i_vec.data(), AES_DECRYPT);

std::cout << "decrypt: " << to_hex_string(decrypt_vec) << std::endl;

輸出

1
plan   : 6B
cipher : F05F94CA1B1459C236C2C35A4BCA72ED
ivec   : F05F94CA1B1459C236C2C35A4BCA72ED
decrypt: 6B000000000000000000000000000000

CBC模式相比ECB有更高的保密性,但由於對每個資料塊的加密依賴與前一個數據塊的加密所以加密無法並行。與ECB一樣在加密前需要對資料進行填充,不是很適合對流資料進行加密。

CFB模式(密文反饋:Cipher feedback)

與ECB和CBC模式只能夠加密塊資料不同,CFB能夠將塊密文(Block Cipher)轉換為流密文(Stream Cipher)。

CFB加密流程(圖片來自維基百科)CFB加密流程(圖片來自維基百科)

CFB解密流程(圖片來自維基百科)CFB解密流程(圖片來自維基百科)

注意:CFB、OFB和CTR模式中解密也都是用的加密器而非解密器。
CFB的加密工作分為兩部分:

  1. 將一前段加密得到的密文再加密;
  2. 將第1步加密得到的資料與當前段的明文異或。

由於加密流程和解密流程中被塊加密器加密的資料是前一段密文,因此即使明文資料的長度不是加密塊大小的整數倍也是不需要填充的,這保證了資料長度在加密前後是相同的。
這種模式稱為128位的CFB模式(又稱CFB128)在OpenSSL中用來進行這種加解密的函式為AES_cfb128_encrypt

1
void AES_cfb128_encrypt(const unsigned char *in, unsigned char *out, size_t length, const AES_KEY *key, unsigned char *ivec, int *num, const int enc)

這個函式中大部分的引數都和AES_cbc_encrypt相同,除了

  • key: 無論是加密還是解密這個key都要用AES_set_encrypt_key初始化
  • num: 用於返回自上次對前一個密文塊加密後到函式返回為止已處理的明文資料長度(位元組)見下文分析
  • enc: 加密AES_ENCRYPT 解密AES_DECRYPT

剛開始用這個函式的時候一直疑惑於num這個引數,並且Google了下也沒找到答案,就直接去原始碼中看實現了。

1
2
3
4
5
6
7
8
9
10
11
/* The input and output encrypted as though 128bit cfb mode is being
* used. The extra state information to record how much of the
* 128bit block we have used is contained in *num;
*/


void AES_cfb128_encrypt(const unsigned char *in, unsigned char *out,
size_t length, const AES_KEY *key,
unsigned char *ivec, int *num, const int enc)
{


CRYPTO_cfb128_encrypt(in,out,length,key,ivec,num,enc,(block128_f)AES_encrypt);
}

上面這段程式碼註釋中就是在講這個引數的用途,但看了還是不明覺厲。下面是CRYPTO_cfb128_encrypt的實現(去掉了無關緊要的程式碼)。

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
32
33
void CRYPTO_cfb128_encrypt(const unsigned char *in, unsigned char *out,
size_t len, const void *key,
unsigned char ivec[16], int *num,
int enc, block128_f block)

{

unsigned int n;
size_t l = 0;
assert(in && out && key && ivec && num);
n = *num;
if (enc) {
while (l<len) {
if (n == 0) {
(*block)(ivec, ivec, key);
}
out[l] = ivec[n] ^= in[l];
++l;
n = (n+1) % 16;
}
*num = n;
}
else {
while (l<len) {
unsigned char c;
if (n == 0) {
(*block)(ivec, ivec, key);
}
out[l] = ivec[n] ^ (c = in[l]); ivec[n] = c;
++l;
n = (n+1) % 16;
}
*num=n;
}
}

看到這段程式碼就很清楚了,128位的CFB是對前一段資料的密文用塊加密器加密後儲存在IV中,之後用這128位資料與後面到來的128位資料異或,num用來記錄自上次呼叫加密器後已經處理的資料長度(位元組),當num重新變為0的時候就會再呼叫加密器,即每處理128位呼叫一次加密器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto key = from_hex_string("2B7E151628AED2A6ABF7158809CF4F3C");
auto i_vec = from_hex_string("000102030405060708090A0B0C0D0E0F");
auto plan_vec = from_hex_string("6BC1BEE22E409F96E93D7E117393172A52"); // 136 bits 17 bytes

AES_KEY aes_enc_ctx;
AES_set_encrypt_key(key.data(), 128, &aes_enc_ctx);
std::vector<unsigned char> cipher_vec(plan_vec.size());
int num = 0;
for(std::size_t index = 0; index < plan_vec.size(); ++index) {
AES_cfb128_encrypt(&plan_vec[index], &cipher_vec[index], 1, &aes_enc_ctx, i_vec.data(), &num, AES_ENCRYPT);
std::cout << "i_vec: " << to_hex_string(i_vec) << " num: " << num << std::endl;
}
std::cout << "cipher : " << to_hex_string(cipher_vec) << std::endl;

AES_KEY aes_dec_ctx;
AES_set_encrypt_key(key.data(), 128, &aes_dec_ctx);
std::vector<unsigned char> decrypt_vec(cipher_vec.size());
i_vec = from_hex_string("000102030405060708090A0B0C0D0E0F"); // reset i_vec
num = 0;
AES_cfb128_encrypt(cipher_vec.data(), decrypt_vec.data(), cipher_vec.size(), &aes_dec_ctx, i_vec.data(), &num, AES_DECRYPT);

std::cout << "decrypt: " << to_hex_string(decrypt_vec) << std::endl;

輸出

1
i_vec: 3BFE67CC996D32B6DA0937E99BAFEC60 num: 1
i_vec: 3B3F67CC996D32B6DA0937E99BAFEC60 num: 2
i_vec: 3B3FD9CC996D32B6DA0937E99BAFEC60 num: 3
i_vec: 3B3FD92E996D32B6DA0937E99BAFEC60 num: 4
i_vec: 3B3FD92EB76D32B6DA0937E99BAFEC60 num: 5
i_vec: 3B3FD92EB72D32B6DA0937E99BAFEC60 num: 6
i_vec: 3B3FD92EB72DADB6DA0937E99BAFEC60 num: 7
i_vec: 3B3FD92EB72DAD20DA0937E99BAFEC60 num: 8
i_vec: 3B3FD92EB72DAD20330937E99BAFEC60 num: 9
i_vec: 3B3FD92EB72DAD20333437E99BAFEC60 num: 10
i_vec: 3B3FD92EB72DAD20333449E99BAFEC60 num: 11
i_vec: 3B3FD92EB72DAD20333449F89BAFEC60 num: 12
i_vec: 3B3FD92EB72DAD20333449F8E8AFEC60 num: 13
i_vec: 3B3FD92EB72DAD20333449F8E83CEC60 num: 14
i_vec: 3B3FD92EB72DAD20333449F8E83CFB60 num: 15
i_vec: 3B3FD92EB72DAD20333449F8E83CFB4A num: 0
i_vec: 348BCF60BEB005A35354A201DAB36BDA num: 1
cipher : 3B3FD92EB72DAD20333449F8E83CFB4A34
decrypt: 6BC1BEE22E409F96E93D7E117393172A52

CFB128是每處理128位資料呼叫一次加密器,此外還有兩種常用的CFB是CFB8和CFB1,前者每處理8位呼叫一次加密器,後者每處理1位呼叫1次加密器,就運算量來講CFB1是CFB8的8倍,是CFB128的128倍。對於CFB8和CFB1需要將IV作為移位暫存器。

CFB8的加密流程

  1. 使用加密器加密IV的資料;
  2. 將明文的最高8位與IV的最高8位異或得到8位密文;
  3. 將IV資料左移8位,最低8位用剛剛計算得到的8位密文補上。

重複1到3。

CFB1的加密流程

  1. 使用加密器加密IV的資料;
  2. 將明文的最高1位與IV的最高1位異或得到1位密文;
  3. 將IV資料左移1位,最低1位用剛剛計算得到的1位密文補上。

重複1到3。

OpenSSL中AES_cfb8_encryptAES_cfb1_encrypt分別用來加解密CFB8和CFB1。這兩個函式的引數和AES_cfb128_encrypt完全一樣,但num和length含義略有不同。

  • num: 應總是為0 否則會觸發斷言
  • length: CFB8單位為byte CFB1單位為bit
    CFB模式非常適合對流資料進行加密,解密可以平行計算。

OFB模式(輸出反饋:Output feedback)

OFB是先用塊加密器生成金鑰流(Keystream),然後再將金鑰流與明文流異或得到密文流,解密是先用塊加密器生成金鑰流,再將金鑰流與密文流異或得到明文,由於異或操作的對稱性所以加密和解密的流程是完全一樣的。

OFB加密流程(圖片來自維基百科)OFB加密流程(圖片來自維基百科)
OFB解密流程(圖片來自維基百科)OFB解密流程(圖片來自維基百科)

OpenSSL中用來加解密AES-OFB的函式是AES_ofb128_encrypt其引數和各引數的含義與AES_cfb128_encrypt完全一樣,但由於OFB加密和解密是對稱的所以沒有引數enc。
OFB與CFB一樣都非常適合對流資料的加密,OFB由於加密和解密都依賴與前一段資料,所以加密和解密都不能並行。