前言
AES算法是當前最流行的對稱加密算法,也是一種分組加密算法,分組密碼就是把明文分爲固定長度的一組一組,每次加密一組數據,直到加密完整個明文數據。AES算法根據分組長度可以分爲AES128, AES192,AES256,其所要求的祕鑰長度和加密輪數也各不相同。鑑於這三種模式的算法在本質上沒有區別,所以本文主要介紹AES-128(數據分組爲16字節,祕鑰長度爲16字節,加密輪數爲10輪),並給出C語言實現。
確切的說分組密碼只是規定了怎麼加密一組明文,如果明文數據比較長,其他的組需要怎麼進行加密取決於使用何種分組密碼工作模式。對於AES-128而言,每次只加密16字節長度的數據,如果明文長度爲32字節話,我們很容易想到第2組16字節可以仿照第1組16字節數據進行處理,這就是最簡單的分組密碼工作模式ECB(電子密碼本)模式,本文主要講述AES算法實現,對於長數據也是使用這種最簡單的ECB分組處理方式,更多其他分組密碼工作模式,請參考另一篇文章圖解分組密碼五大工作模式。
前面討論的數據長度都是16字節,或者其整倍數長度的加密算法實現,對於數據長度不是分組長度整倍數的情形,通常需要對數據進行填充,使其長度達到分組長度的整倍數再來進行加密。對於數據長度不足分組長度整倍數使用何種格式進行數據填充有多種不同的填充標準,比如在數據後面填充二進制的0x0,直到達到要求的長度,這就是ZeroPadding方式;比如數據缺少幾位就填充二進制的幾,例如缺少4位填充0x04 0x04 0x04 0x04,這就是PKCS7/PKCS5填充方式。本文提供的實現不涉及數據填充,假定明文數據都是16字節的整倍數長度。
AES算法流程
AES算法主要可以分爲祕鑰擴展、字節替換、行移位、列混合和輪祕鑰加這5個步驟。
- 祕鑰擴展(KeyExpansions:給定的初始祕鑰一般比較短,比如16字節,而算法如果進行10輪運算的話就需要16x(10+1)字節長度的祕鑰,需要對原始祕鑰進行祕鑰擴展。
- 字節替換(SubBytes):一個非線性的替換步驟,根據查表把一個字節替換爲另一個字節。
- 行移位(ShiftRows):將數據矩陣的每一行循環移位一定長度。
- 列混合(MixColumns):將數據矩陣乘以一個固定的矩陣,增加混淆程度。
- 輪祕鑰加(AddRoundKey):將數據矩陣與祕鑰矩陣進行異或操作。
AES加密
AES-128加密流程可以使用如下僞代碼表示:
AES-128加密(uint8 in[16], uint8 out[16], uint8 key[16]){
uint8 state[4,4] = in;
uint32 w[44] = KeyExpansions(key[16]);
addRoundKey(state, w[0-3]);
for (int j = 1; j < 10; ++j) {
subBytes(state);
shiftRows(state);
mixColumns(state);
addRoundKey(state, w); //w[4-7],w[8-11]...w[37-40]
}
subBytes(state);
shiftRows(state);
addRoundKey(state, w[41-44]);
out = state;
}
AES解密
AES-128解密流程可以使用如下僞代碼表示:
AES-128解密(uint8 in[16], uint8 out[16], uint8 key[16]){
uint8 state[4,4] = in;
uint32 w[44] = KeyExpansions(key[16]);
//此時使用的祕鑰是加密時使用的祕鑰的倒序
addRoundKey(state, w[41-44]);
for (int j = 1; j < 10; ++j) {
inverse-subBytes(state);
inverse-shiftRows(state);
inverse-mixColumns(state);
addRoundKey(state, w); //w[37-40], ... w[8-11],w[4-7],...
}
inverse-subBytes(state);
inverse-shiftRows(state);
addRoundKey(state, w[0-3]);
out = state;
}
AES算法步驟
前提
AES運算都是以下4x4字節表示的二維數組矩陣uint8_t state[4][4]
爲一個單位,把一個連續的序列”1234567890abcdef”放在矩陣中,對應的順序爲下圖所示。一定要注意 排列的順序是豎排的,不是橫排的!
祕鑰擴展 KeyExpansion
示例祕鑰key = “abcdefghijklmnop”={0x61, 0x62,…,0x6F,0x70}。
AES128中原始祕鑰key爲16字節,運算中需要11個矩陣大小的祕鑰,每一列所包含的32位記爲一個uint32_t W
,所以祕鑰擴展一共需要生產44個列W
,即uint32_t W[44]
。
W[0-3]爲直接複製的原始祕鑰。
- W[0] = 0x61626364.
- W[1] = 0x65666768.
- W[2] = 0x696A6B6C.
- W[3] = 0x6D6E6F70.
W[4-43]爲擴展的祕鑰。
RotWord()爲循環左移一位,如輸入0x12345678,輸出0x34567812。
SubWord()爲字節替換,可以參考字節替換。
rcon爲輪常量異或,常量數組爲:
static const uint32_t rcon[10] = {
0x01000000UL, 0x02000000UL, 0x04000000UL, 0x08000000UL, 0x10000000UL,
0x20000000UL, 0x40000000UL, 0x80000000UL, 0x1B000000UL, 0x36000000UL
};
祕鑰key = “abcdefghijklmnop”,祕鑰擴展後生成的擴展祕鑰uint32_t W[44]爲:
W[ 0-3 ] 61626364 65666768 696A6B6C 6D6E6F70
W[ 4-7 ] FFCA3258 9AAC5530 F3C63E5C 9EA8512C
W[ 8-11] 3F1B4353 A5B71663 5671283F C8D97913
W[12-15] 0EAD3EBB AB1A28D8 FD6B00E7 35B279F4
W[16-19] 311B812D 9A01A9F5 676AA912 52D8D0E6
W[20-23] 406B0F2D DA6AA6D8 BD000FCA EFD8DF2C
W[24-27] 01F57EF2 DB9FD82A 669FD7E0 894708CC
W[28-31] E1C53555 3A5AED7F 5CC53A9F D5823253
W[32-35] 72E6D856 48BC3529 14790FB6 C1FB3DE5
W[36-39] 66C1012E 2E7D3407 3A043BB1 FBFF0654
W[40-43] 46AE2121 68D31526 52D72E97 A92828C3
字節替換 SubBytes
字節替換就是簡單的查表操作,AES定義了加密用的S盒和解密用的逆S盒來進行字節替換。
S盒爲256個元素的數組,即1個字節(0x00~0xff)可以表示的數量,所以進行字節替換時可以直接把該字節的值作爲S盒數組的下標來進行替換。比如0x03字節替換結果爲S[0x03]=0x7B,逆S盒同理。圖示中 。
看到有的地方把S盒定義爲16x16二維數組S[16][16],字節替換時取該字節的高4位作爲行下標,低4位作爲列下標。這種方式因爲還得對需要替換字節分別取高低位,得到結果再合併高低位,無疑把字節替換操作複雜化了。採用S[256]一維數組完全可以省去這些不必要的操作。
S盒爲
unsigned char S[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
};
解密時逆字節替換就是使用逆S盒進行字節替換,逆S盒爲:
unsigned char inv_S[256] = {
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
};
行移位 ShiftRows
前面已經說過,AES運算都是基於4x4二維數組進行的。行移位操作爲:第0行不移動,第1行循環左移1字節,第2行循環左移2字節,第3行循環左移3字節。
解密時逆行移位操作爲:第0行不移動,第1行循環右移1字節,第2行循環右移2字節,第3行循環右移3字節。
列混合 MixColumns
列混合通過矩陣相乘來實現,經過移位後的矩陣左乘一個固定的矩陣,得到混淆後的矩陣,如下公式所示
上述矩陣相乘可以化簡爲如下表達式:
其中矩陣的乘法和加法並不是通常意義上的乘法和加法,而是定義在伽羅華域上的二元運算,且使用的不可約多項式爲 。關於伽羅華域運算我在另一篇文章中有詳細介紹《伽羅華域運算及C語言實現》。其加法爲模二加法,相當於異或運算,其乘法可以使用GMul表示,則上式運算可以表示爲:
解密時逆列混合操作和列混合一樣,只是左乘的矩陣使用如下矩陣。可以驗證此矩陣B是列混合使用矩陣A的逆矩陣,兩者乘積爲單位矩陣,即AB=BA=E。
輪祕鑰加 AddRoundKey
輪祕鑰加是將數據與相應的祕鑰逐位進行異或操作。上面已經講到擴展後的祕鑰結構爲uint32_t W[44],輪祕鑰加操作取16字節長度的祕鑰,即取uint32_t W[4]作爲一個單元,把這個總長度爲16字節的祕鑰按照加密數據同樣的格式寫成uint8_t b[4][4]二維數組矩陣,然後將此4x4矩陣與4x4的數據矩陣逐位進行異或操作。
如下圖所示, 表示數據矩陣, 表示祕鑰,比如W[0]= ,W[1]= ,W[0]= ,W[0]= 。
AES-128 C語言實現
#include <stdint.h>
#include <stdio.h>
#include <string.h>
typedef struct{
uint32_t eK[44], dK[44]; // encKey, decKey
int Nr; // 10 rounds
}AesKey;
#define BLOCKSIZE 16 //AES-128分組長度爲16字節
// uint8_t y[4] -> uint32_t x
#define LOAD32H(x, y) \
do { (x) = ((uint32_t)((y)[0] & 0xff)<<24) | ((uint32_t)((y)[1] & 0xff)<<16) | \
((uint32_t)((y)[2] & 0xff)<<8) | ((uint32_t)((y)[3] & 0xff));} while(0)
// uint32_t x -> uint8_t y[4]
#define STORE32H(x, y) \
do { (y)[0] = (uint8_t)(((x)>>24) & 0xff); (y)[1] = (uint8_t)(((x)>>16) & 0xff); \
(y)[2] = (uint8_t)(((x)>>8) & 0xff); (y)[3] = (uint8_t)((x) & 0xff); } while(0)
// 從uint32_t x中提取從低位開始的第n個字節
#define BYTE(x, n) (((x) >> (8 * (n))) & 0xff)
/* used for keyExpansion */
// 字節替換然後循環左移1位
#define MIX(x) (((S[BYTE(x, 2)] << 24) & 0xff000000) ^ ((S[BYTE(x, 1)] << 16) & 0xff0000) ^ \
((S[BYTE(x, 0)] << 8) & 0xff00) ^ (S[BYTE(x, 3)] & 0xff))
// uint32_t x循環左移n位
#define ROF32(x, n) (((x) << (n)) | ((x) >> (32-(n))))
// uint32_t x循環右移n位
#define ROR32(x, n) (((x) >> (n)) | ((x) << (32-(n))))
/* for 128-bit blocks, Rijndael never uses more than 10 rcon values */
// AES-128輪常量
static const uint32_t rcon[10] = {
0x01000000UL, 0x02000000UL, 0x04000000UL, 0x08000000UL, 0x10000000UL,
0x20000000UL, 0x40000000UL, 0x80000000UL, 0x1B000000UL, 0x36000000UL
};
// S盒
unsigned char S[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
};
//逆S盒
unsigned char inv_S[256] = {
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
};
/* copy in[16] to state[4][4] */
int loadStateArray(uint8_t (*state)[4], const uint8_t *in) {
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
state[j][i] = *in++;
}
}
return 0;
}
/* copy state[4][4] to out[16] */
int storeStateArray(uint8_t (*state)[4], uint8_t *out) {
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
*out++ = state[j][i];
}
}
return 0;
}
//祕鑰擴展
int keyExpansion(const uint8_t *key, uint32_t keyLen, AesKey *aesKey) {
if (NULL == key || NULL == aesKey){
printf("keyExpansion param is NULL\n");
return -1;
}
if (keyLen != 16){
printf("keyExpansion keyLen = %d, Not support.\n", keyLen);
return -1;
}
uint32_t *w = aesKey->eK; //加密祕鑰
uint32_t *v = aesKey->dK; //解密祕鑰
/* keyLen is 16 Bytes, generate uint32_t W[44]. */
/* W[0-3] */
for (int i = 0; i < 4; ++i) {
LOAD32H(w[i], key + 4*i);
}
/* W[4-43] */
for (int i = 0; i < 10; ++i) {
w[4] = w[0] ^ MIX(w[3]) ^ rcon[i];
w[5] = w[1] ^ w[4];
w[6] = w[2] ^ w[5];
w[7] = w[3] ^ w[6];
w += 4;
}
w = aesKey->eK+44 - 4;
//解密祕鑰矩陣爲加密祕鑰矩陣的倒序,方便使用,把ek的11個矩陣倒序排列分配給dk作爲解密祕鑰
//即dk[0-3]=ek[41-44], dk[4-7]=ek[37-40]... dk[41-44]=ek[0-3]
for (int j = 0; j < 11; ++j) {
for (int i = 0; i < 4; ++i) {
v[i] = w[i];
}
w -= 4;
v += 4;
}
return 0;
}
// 輪祕鑰加
int addRoundKey(uint8_t (*state)[4], const uint32_t *key) {
uint8_t k[4][4];
/* i: row, j: col */
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
k[i][j] = (uint8_t) BYTE(key[j], 3 - i); /* 把 uint32 key[4] 先轉換爲矩陣 uint8 k[4][4] */
state[i][j] ^= k[i][j];
}
}
return 0;
}
//字節替換
int subBytes(uint8_t (*state)[4]) {
/* i: row, j: col */
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
state[i][j] = S[state[i][j]]; //直接使用原始字節作爲S盒數據下標
}
}
return 0;
}
//逆字節替換
int invSubBytes(uint8_t (*state)[4]) {
/* i: row, j: col */
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
state[i][j] = inv_S[state[i][j]];
}
}
return 0;
}
//行移位
int shiftRows(uint8_t (*state)[4]) {
uint32_t block[4] = {0};
/* i: row */
for (int i = 0; i < 4; ++i) {
//便於行循環移位,先把一行4字節拼成uint_32結構,移位後再轉成獨立的4個字節uint8_t
LOAD32H(block[i], state[i]);
block[i] = ROF32(block[i], 8*i);
STORE32H(block[i], state[i]);
}
return 0;
}
//逆行移位
int invShiftRows(uint8_t (*state)[4]) {
uint32_t block[4] = {0};
/* i: row */
for (int i = 0; i < 4; ++i) {
LOAD32H(block[i], state[i]);
block[i] = ROR32(block[i], 8*i);
STORE32H(block[i], state[i]);
}
return 0;
}
/* Galois Field (256) Multiplication of two Bytes */
// 兩字節的伽羅華域乘法運算
uint8_t GMul(uint8_t u, uint8_t v) {
uint8_t p = 0;
for (int i = 0; i < 8; ++i) {
if (u & 0x01) { //
p ^= v;
}
int flag = (v & 0x80);
v <<= 1;
if (flag) {
v ^= 0x1B; /* x^8 + x^4 + x^3 + x + 1 */
}
u >>= 1;
}
return p;
}
// 列混合
int mixColumns(uint8_t (*state)[4]) {
uint8_t tmp[4][4];
uint8_t M[4][4] = {{0x02, 0x03, 0x01, 0x01},
{0x01, 0x02, 0x03, 0x01},
{0x01, 0x01, 0x02, 0x03},
{0x03, 0x01, 0x01, 0x02}};
/* copy state[4][4] to tmp[4][4] */
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j){
tmp[i][j] = state[i][j];
}
}
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) { //伽羅華域加法和乘法
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j])
^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
}
}
return 0;
}
// 逆列混合
int invMixColumns(uint8_t (*state)[4]) {
uint8_t tmp[4][4];
uint8_t M[4][4] = {{0x0E, 0x0B, 0x0D, 0x09},
{0x09, 0x0E, 0x0B, 0x0D},
{0x0D, 0x09, 0x0E, 0x0B},
{0x0B, 0x0D, 0x09, 0x0E}}; //使用列混合矩陣的逆矩陣
/* copy state[4][4] to tmp[4][4] */
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j){
tmp[i][j] = state[i][j];
}
}
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j])
^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
}
}
return 0;
}
// AES-128加密接口,輸入key應爲16字節長度,輸入長度應該是16字節整倍數,
// 這樣輸出長度與輸入長度相同,函數調用外部爲輸出數據分配內存
int aesEncrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *pt, uint8_t *ct, uint32_t len) {
AesKey aesKey;
uint8_t *pos = ct;
const uint32_t *rk = aesKey.eK; //解密祕鑰指針
uint8_t out[BLOCKSIZE] = {0};
uint8_t actualKey[16] = {0};
uint8_t state[4][4] = {0};
if (NULL == key || NULL == pt || NULL == ct){
printf("param err.\n");
return -1;
}
if (keyLen > 16){
printf("keyLen must be 16.\n");
return -1;
}
if (len % BLOCKSIZE){
printf("inLen is invalid.\n");
return -1;
}
memcpy(actualKey, key, keyLen);
keyExpansion(actualKey, 16, &aesKey); // 祕鑰擴展
// 使用ECB模式循環加密多個分組長度的數據
for (int i = 0; i < len; i += BLOCKSIZE) {
// 把16字節的明文轉換爲4x4狀態矩陣來進行處理
loadStateArray(state, pt);
// 輪祕鑰加
addRoundKey(state, rk);
for (int j = 1; j < 10; ++j) {
rk += 4;
subBytes(state); // 字節替換
shiftRows(state); // 行移位
mixColumns(state); // 列混合
addRoundKey(state, rk); // 輪祕鑰加
}
subBytes(state); // 字節替換
shiftRows(state); // 行移位
// 此處不進行列混合
addRoundKey(state, rk+4); // 輪祕鑰加
// 把4x4狀態矩陣轉換爲uint8_t一維數組輸出保存
storeStateArray(state, pos);
pos += BLOCKSIZE; // 加密數據內存指針移動到下一個分組
pt += BLOCKSIZE; // 明文數據指針移動到下一個分組
rk = aesKey.eK; // 恢復rk指針到祕鑰初始位置
}
return 0;
}
// AES128解密, 參數要求同加密
int aesDecrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *ct, uint8_t *pt, uint32_t len) {
AesKey aesKey;
uint8_t *pos = pt;
const uint32_t *rk = aesKey.dK; //解密祕鑰指針
uint8_t out[BLOCKSIZE] = {0};
uint8_t actualKey[16] = {0};
uint8_t state[4][4] = {0};
if (NULL == key || NULL == ct || NULL == pt){
printf("param err.\n");
return -1;
}
if (keyLen > 16){
printf("keyLen must be 16.\n");
return -1;
}
if (len % BLOCKSIZE){
printf("inLen is invalid.\n");
return -1;
}
memcpy(actualKey, key, keyLen);
keyExpansion(actualKey, 16, &aesKey); //祕鑰擴展,同加密
for (int i = 0; i < len; i += BLOCKSIZE) {
// 把16字節的密文轉換爲4x4狀態矩陣來進行處理
loadStateArray(state, ct);
// 輪祕鑰加,同加密
addRoundKey(state, rk);
for (int j = 1; j < 10; ++j) {
rk += 4;
invShiftRows(state); // 逆行移位
invSubBytes(state); // 逆字節替換,這兩步順序可以顛倒
addRoundKey(state, rk); // 輪祕鑰加,同加密
invMixColumns(state); // 逆列混合
}
invSubBytes(state); // 逆字節替換
invShiftRows(state); // 逆行移位
// 此處沒有逆列混合
addRoundKey(state, rk+4); // 輪祕鑰加,同加密
storeStateArray(state, pos); // 保存明文數據
pos += BLOCKSIZE; // 輸出數據內存指針移位分組長度
ct += BLOCKSIZE; // 輸入數據內存指針移位分組長度
rk = aesKey.dK; // 恢復rk指針到祕鑰初始位置
}
return 0;
}
上面是AES-128加解密算法的實現,下面給出使用方法和測試用例:
#include <stdio.h>
// 方便輸出16進制數據
void printHex(uint8_t *ptr, int len, char *tag) {
printf("%s\ndata[%d]: ", tag, len);
for (int i = 0; i < len; ++i) {
printf("%.2X ", *ptr++);
}
printf("\n");
}
int main() {
// case 1
const uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};
const uint8_t pt[16]={0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34};
uint8_t ct[16] = {0}; // 外部申請輸出數據內存,用於加密後的數據
uint8_t plain[16] = {0}; // 外部申請輸出數據內存,用於解密後的數據
aesEncrypt(key, 16, pt, ct, 16); // 加密
printHex(pt, 16, "plain data:"); // 打印初始明文數據
printf("expect cipher:\n39 25 84 1D 02 DC 09 FB DC 11 85 97 19 6A 0B 32\n"); // 正常解密後的數據內容
printHex(ct, 16, "after encryption:"); // 打印加密後的密文
aesDecrypt(key, 16, ct, plain, 16); // 解密
printHex(plain, 16, "after decryption:"); // 打印解密後的明文數據
// case 2
// 16字節字符串形式祕鑰
const uint8_t key2[]="1234567890123456";
// 32字節長度字符串明文
const uint8_t *data = (uint8_t*)"abcdefghijklmnopqrstuvwxyz123456";
uint8_t ct2[32] = {0}; //外部申請輸出數據內存,用於存放加密後數據
uint8_t plain2[32] = {0}; //外部申請輸出數據內存,用於存放解密後數據
//加密32字節明文
aesEncrypt(key2, 16, data, ct2, 32);
printf("\nplain text:\n%s\n", data);
printf("expect ciphertext:\nfcad715bd73b5cb0488f840f3bad7889\n");
printHex(ct2, 32, "after encryption:");
// 解密32字節密文
aesDecrypt(key2, 16, ct2, plain2, 32);
// 打印16進制形式的解密後的明文
printHex(plain2, 32, "after decryption:");
// 因爲加密前的數據爲可見字符的字符串,打印解密後的明文字符,與加密前明文進行對比
printf("output plain text\n");
for (int i = 0; i < 32; ++i) {
printf("%c ", plain2[i]);
}
return 0;
}
下面爲程序運行後的測試結果,可以看到明文加密再解密後的數據和加密前的數據是一樣的。
plain data:
data[16]: 32 43 F6 A8 88 5A 30 8D 31 31 98 A2 E0 37 07 34
expect cipher:
39 25 84 1D 02 DC 09 FB DC 11 85 97 19 6A 0B 32
after encryption:
data[16]: 39 25 84 1D 02 DC 09 FB DC 11 85 97 19 6A 0B 32
after decryption:
data[16]: 32 43 F6 A8 88 5A 30 8D 31 31 98 A2 E0 37 07 34
plain text:
abcdefghijklmnopqrstuvwxyz123456
expect ciphertext:
fcad715bd73b5cb0488f840f3bad7889
after encryption:
data[32]: FC AD 71 5B D7 3B 5C B0 48 8F 84 0F 3B AD 78 89 D0 E7 09 D0 FF D3 8C 6D FE C5 5C CB 9F 47 5B 01
after decryption:
data[32]: 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 31 32 33 34 35 36
output plain text
a b c d e f g h i j k l m n o p q r s t u v w x y z 1 2 3 4 5 6
Process finished with exit code 0
代碼完整的工程已上傳到Github倉庫:https://github.com/lmshao/AES。
後記
好久之前就看過AES算法的介紹,總是感覺特別複雜特別煩瑣,後來想想自己要是能用代碼實現一遍印象肯定特別深刻,經過了多天的研究,查看了很多資料和別人家的代碼終於把用代碼實現了AES-128算法。感覺只是寫出了代碼可能時間長就忘了,要是能用文字做個筆記總結一下肯定印象更深刻,於是乎就有了這篇文章。
由於之前有很多人總結的太好了,所以就借鑑了很多其他人的東西。包括看了多遍這篇博客《AES加密算法的詳細介紹與實現》,寫的實在太好了!非常詳細!祕鑰擴展那一節的圖片就是借用他的,我沒事又興致勃勃地用Visio重新畫了一遍。還反覆查看了多遍AES標準文檔和維基百科的AES介紹,其他的圖片都直接鏈接的維基百科的圖片。
看別人的文章,然後動手實現一遍是記住這個算法比較好的一個方法,因爲總會遇到不曾考慮過的細節。比如AES算法中用的4x4狀態矩陣與一維數組的映射關係,狀態矩陣是按列的順序排列的。多次計算出來的結果都不正確,好在AES標準文檔給出了測試用例每一個步驟之後的狀態矩陣,我就一步一步進行對照查找問題所在。
代碼完整的工程已上傳到Github倉庫:https://github.com/lmshao/AES。有問題歡迎留言交流。