AES-128
加密模式处理¶
ECB¶
The Electronic Codebook Mode
CBC¶
The Cipher Block Chaining Mode
CFB¶
The Cipher Feedback Mode
这里只需要考虑 CFB-128bit,也就是 \(s=b\),所以上述过程可以简化为如下:
OFB¶
The Output Feedback Mode
最后一个分组 block 长度可以小于 128 bit,这里假定其长度为 \(u\)。
我消去 \(I\),仅用 \(O\) 表示密钥流的变化,更加直观:
\(b\) 是一个 block 分组的大小,这里就是 128 bit。
CTR¶
The Counter Mode
AES Cipher & Inverse¶
下图展现了实现加密和解密 AES 的过程,将每一轮每一个变换函数建立了一一对应关系。
其中除了 Add 操作之外(直接异或即可),包含主要 3 对变换和反变换。另外还需要进行 Expand Key,在 AES-128 中就是将 128 bits 输入 key 计算得到规模为 128x11 bits 的 11 个 key。
SubBytes & Inverse¶
这一变换数学上的功能是对每一个字节实现如下变换:
-
将八位字节视为有限域 \(GF(2^8)\) 上的元素,进行取逆
-
然后进行如下仿射变换:(\(b_i\) 表示一个字节从低位开始数的第 \(i+1\) 位)
从结果上来说,上述变换过程及其反变换完全可以通过查表来完成。所以对于 SubBytes
和 InvSubBytes
变换,就是分别将每一个字节经过预先计算好的 AES S-Box 和 AES Inv-S-Box 进行查表置换。
对于一个字节,有 \(2^8=256\) 种取值,所以两个 Box 的大小都是 256,内容如下:
const uint8_t Sbox[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,
};
const uint8_t InvSbox[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 & Inverse¶
这一变换比较简单,就是对表示为 4x4 矩阵的 16 字节数据,对第 \(i\) 行进行 \(i\) 字节轮转。(\(0\leq i\leq 3\))
所以显然第一行,也就是第 \(0\) 行进行 \(0\) 字节轮转是没有实际作用可以省略的。其过程如下图所示:
逆变换则如下所示:
cpp 使用冒泡的方式处理如下:
// uint8_t c[16]
inline void ShiftRows(uint8_t * c){
swap(c[1], c[5]);
swap(c[5], c[9]);
swap(c[9], c[13]);
swap(c[2], c[10]);
swap(c[6], c[14]);
swap(c[15], c[11]);
swap(c[11], c[7]);
swap(c[7], c[3]);
}
inline void InvShiftRows(uint8_t * c){
swap(c[9], c[13]);
swap(c[5], c[9]);
swap(c[1], c[5]);
swap(c[2], c[10]);
swap(c[6], c[14]);
swap(c[7], c[3]);
swap(c[11], c[7]);
swap(c[15], c[11]);
}
MixColumns & Inverse¶
对表示为 4x4 矩阵的 16 字节数据,每一列 4 字节向量,视为一个 \(x=2^8\) 并且系数来自 \(GF(2^8)\) 的多项式
AES 使用了一对可逆的多项式,通过对这两个多项式进行模乘实现变换和逆变换:
于是 \(a(x)\otimes b(x)\) 就可以表示为有限域 \(GF(2^8)\) 上的向量与可逆矩阵乘法:
同理,\(a(x)\otimes b^{-1}(x)\) 也表示为有限域 \(GF(2^8)\) 上的向量与可逆矩阵乘法:
这里有限域 \(GF(2^8)\) 上的模乘运算如果再通过查表算,就需要 \(7\times256\) 大小的表,因为两个矩阵总共出现了 7 种不同的系数。在 AES 标准中没有使用查表,而是使用有限域 \(GF(2^8)\) 上位运算实现的乘 2 模运算 xtime
实现。这个函数运算非常简单,cpp 实现如下:
对于一般的有限域 \(GF(2^8)\) 上模乘运算 \(a\otimes b\),就能用 xtime
表示如下:
然后矩阵运算还可以技巧性的使用中间变量,避免进行重复相同的运算,cpp 实现单独对一个列变换和反变换如下:
inline void MixColumn(uint8_t * a){
uint8_t t, u;
t = a[0] ^ a[1] ^ a[2] ^ a[3];
u = a[0];
a[0] ^= t ^ xtime(a[i] ^ a[1]);
a[1] ^= t ^ xtime(a[1] ^ a[2]);
a[2] ^= t ^ xtime(a[2] ^ a[3]);
a[3] ^= t ^ xtime(a[3] ^ u);
}
inline void InvMixColumn(uint8_t * a){
uint8_t u, v;
u = xtime(xtime(c[i] ^ c[i + 2]));
v = xtime(xtime(c[i + 1] ^ c[i + 3]));
c[i] ^= u;
c[i + 1] ^= v;
c[i + 2] ^= u;
c[i + 3] ^= v;
MixColumn(c);
}
Key Expansion¶
过程大致如图所示:
NIST 的 AES 标准文档 FIPS 197 给出的伪代码如下:
这里关键的几个步骤在于如下 2 个函数:
-
SubWord
,对一个 word 的 4 个字节分别进行和 SubBytes 变换一样的操作 -
RotWord
,对一个 word 的 4 个字节进行和 ShiftRows 变换第二行一样的操作
还有一个数组 Rcon
,定义为:
uint8_t Rcon[32] = {
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
}
在 AES-128 中只需要用到前 11 个。
思考题¶
问题 1¶
除了课堂上介绍的五种模式以外,分组加密还有一些其他的工作模式。考虑下面的两个场景,使用什么工作模式可以满足它们的需求?这个工作模式的原理是什么?
- 你需要检查密文是否被篡改(不需要单独使用 Hash 函数)。
- 你需要加密一块磁盘。磁盘上的数据是以扇区(通常为 512 Bytes)为单位读写的,操作系统可以通过扇区号随机访问扇区。磁盘加密的密文长度必须要和明文相同,不能使用额外的空间。(有兴趣的话,可以尝试使用一下磁盘加密软件,例如开源的 VeraCrypt)
回答¶
检查密文是否被篡改,就需要认证功能,分组密码中支持认证的模式被称为 AE 模式,例如:
有的模式也允许为未加密的关联数据进行认证,因此被称为 AEAD(Authenticated-Encryption with Associated-Data。
对于磁盘加密,要求密文长度与明文相同且不能使用额外的空间,可以使用 ECB、CBC、CTR 、XTS(XEX Tweakable Block Cipher with ciphertext stealing)模式。根据我查阅到资料, XTS 模式是专门设计用于磁盘加密的工作模式,例如 Kingston 等厂商就会在其产品中使用。
问题 2¶
尝试在实验 3-2 中实现全部五种工作模式:ECB、CBC、CFB、OFB、CTR。
- 只要实现了对单个分组的加密和解密,实现其他的工作模式也是很简单的!
- 选做,不会对分数产生影响。
在前面有各个模式分析,当时我没时间搞这个
问题 3¶
尝试使用 x64 CPU 的 AES 指令集(AES-NI)实现 AES-256。
- 参考资料:x86/x64 SIMD 命令による AES 暗号処理
- 理解 AES-NI 的各个指令的用途。
- 上面的链接中有 C++ 编写的示例代码,仅供参考,不要直接提交或使用 GPT 格式化重命名后提交。
- 请先用自己实现的版本 AC 后再编写使用 AES-NI 的版本,并在实验报告中附上两个版本的提交 ID。
- 不计入思考题分数,但是可以影响性能分。
见附录
Reference¶
- NIST 的 AES 标准文档 FIPS 197
- 一个叫 crypto_commons 的 python 库中 AES 的实现
- Youtube 上的一个 AES 图解系列视频
- NIST 的文档 Recommendation for Block Cipher Modes of Operation
附录¶
当时忙着一个写数据库的比赛(Mini OceanBase )没来得及弄密码学了
没来得及完全完成 x64 CPU 的 AES 指令集(AES-NI)实现 AES-256,实际上我在自己实现的时候就已开始想使用 __m128i
一步到位,然后发现这是思考题附加内容,并且使用 AES 指令是真的“一步到位”,非常简单。所以我自己的实现只好退而求其次使用 uint32_t
和 uint8_t
来实现。
已经初步写好,但好像还是错的,没来得及 debug 了,如下:
#include <cstdint>
#include <iostream>
#include <vector>
#include <algorithm>
#include <stdint.h>
#include <cstring>
#include <emmintrin.h>
#include <immintrin.h>
// Number of columns (32-bit words) comprising the State. For this standard, Nb = 4
#define Nb 4
// Number of 32-bit words comprising the Cipher Key. For this standard, Nk = 4, 6, or 8
#define Nk 4
// Number of rounds, which is a function of Nk and Nb (which is fixed). For this standard, Nr = 10, 12, or 14
#define Nr 10
using namespace std;
void ecb_en(__m128i & KEY, uint32_t & len);
void ecb_de(__m128i & KEY, uint32_t & len);
void cbc_en(__m128i & KEY, __m128i & IV, uint32_t & len);
void cbc_de(__m128i & KEY, __m128i & IV, uint32_t & len);
void cfb_en(__m128i & KEY, __m128i & IV, uint32_t & len);
void cfb_de(__m128i & KEY, __m128i & IV, uint32_t & len);
void ofb_en(__m128i & KEY, __m128i & IV, uint32_t & len);
void ofb_de(__m128i & KEY, __m128i & IV, uint32_t & len);
void ctr_en(__m128i & KEY, __m128i & IV, uint32_t & len);
void ctr_de(__m128i & KEY, __m128i & IV, uint32_t & len);
enum MODE : uint8_t {
ECB_EN = 0x00,
ECB_DE = 0x80,
CBC_EN = 0x01,
CBC_DE = 0x81,
CFB_EN = 0x02,
CFB_DE = 0x82,
OFB_EN = 0x03,
OFB_DE = 0x83,
CTR_EN = 0x04,
CTR_DE = 0x84,
};
#ifndef ONLINE_JUDGE
#include <chrono>
#include <iomanip>
FILE *output, *input;
chrono::time_point<chrono::high_resolution_clock> start_time, end_time;
#endif
int main(){
#ifndef ONLINE_JUDGE
output = freopen("output.bin", "wb", stdout);
input = freopen("input.bin", "rb", stdin);
start_time = chrono::high_resolution_clock::now();
#endif
uint8_t mode;
__m128i KEY;
__m128i IV;
uint32_t len;
fread(&mode, sizeof(mode), 1, stdin);
fread(&KEY, sizeof(KEY), 1, stdin);
fread(&IV, sizeof(IV), 1, stdin);
fread(&len, sizeof(len), 1, stdin);
switch(mode){
// case ECB_EN:{ecb_en(KEY, len);}break;
// case ECB_DE:{ecb_de(KEY, len);}break;
case CBC_EN:{cbc_en(KEY, IV, len);} break;
case CBC_DE:{cbc_de(KEY, IV, len);} break;
// case CFB_EN:{cfb_en(KEY, IV, len);}break;
// case CFB_DE:{cfb_de(KEY, IV, len);}break;
// case OFB_EN:{ofb_en(KEY, IV, len);}break;
// case OFB_DE:{ofb_de(KEY, IV, len);}break;
// case CTR_EN:{ctr_en(KEY, IV, len);}break;
// case CTR_DE:{ctr_de(KEY, IV, len);}break;
}
#ifndef ONLINE_JUDGE
fclose(input);
fclose(output);
output = freopen("time", "w", stdout);
end_time = chrono::high_resolution_clock::now();
auto ns = chrono::duration_cast<chrono::nanoseconds>(end_time - start_time);
auto us = chrono::duration_cast<chrono::microseconds>(end_time - start_time);
auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time);
cout << ns.count() << " ns\n"
<< us.count() << " us\n"
<< ms.count() << " ms";
fclose(output);
#endif
return 0;
}
// uint32_t key[Nk] ---expand---> __m128i w128[Nr+1]
void KeyExpansion(__m128i & key, __m128i * w128, bool decrypting){
w128[0] = key;
__m128i t, t2;
#define EXPAND(n, RCON) \
t = _mm_slli_si128(key, 4);\
t = _mm_xor_si128(key, t);\
t2 = _mm_slli_si128(t, 8);\
t = _mm_xor_si128(t, t2);\
key = _mm_aeskeygenassist_si128(key, RCON);\
key = _mm_shuffle_epi32(key, 0xFF);\
key = _mm_xor_si128(t, key);\
w128[n] = key;
EXPAND(1, 0x01);
// Go on...
EXPAND(2, 0x02);
EXPAND(3, 0x04);
EXPAND(4, 0x08);
EXPAND(5, 0x10);
EXPAND(6, 0x20);
EXPAND(7, 0x40);
EXPAND(8, 0x80);
EXPAND(9, 0x1b);
EXPAND(10, 0x36);
if (decrypting) {
w128[0] = w128[0];
w128[1] = _mm_aesimc_si128(w128[1]);
w128[2] = _mm_aesimc_si128(w128[2]);
w128[3] = _mm_aesimc_si128(w128[3]);
w128[4] = _mm_aesimc_si128(w128[4]);
w128[5] = _mm_aesimc_si128(w128[5]);
w128[6] = _mm_aesimc_si128(w128[6]);
w128[7] = _mm_aesimc_si128(w128[7]);
w128[8] = _mm_aesimc_si128(w128[8]);
w128[9] = _mm_aesimc_si128(w128[9]);
w128[Nr] = w128[Nr];
}
}
// Add 128-bit k to c
inline void AddRoundKey(__m128i & c, __m128i & k){
c = _mm_xor_si128(c, k);
}
// AES Cipher
inline void Ciph(__m128i c, __m128i * w128){
__m128i state = _mm_loadu_si128(&c);
state = _mm_xor_si128(state, w128[0]);
state = _mm_aesenc_si128(state, w128[1]);
state = _mm_aesenc_si128(state, w128[2]);
state = _mm_aesenc_si128(state, w128[3]);
state = _mm_aesenc_si128(state, w128[4]);
state = _mm_aesenc_si128(state, w128[5]);
state = _mm_aesenc_si128(state, w128[6]);
state = _mm_aesenc_si128(state, w128[7]);
state = _mm_aesenc_si128(state, w128[8]);
state = _mm_aesenc_si128(state, w128[9]);
state = _mm_aesenclast_si128(state, w128[Nr]);
_mm_storeu_si128(reinterpret_cast<__m128i *>(&c), state);
}
// AES Inverse Cipher
inline void EqInvCiph(__m128i & c, __m128i * w128){
__m128i state = _mm_loadu_si128(&c);
state = _mm_xor_si128(state, w128[Nr]);
state = _mm_aesdec_si128(state, w128[9]);
state = _mm_aesdec_si128(state, w128[8]);
state = _mm_aesdec_si128(state, w128[7]);
state = _mm_aesdec_si128(state, w128[6]);
state = _mm_aesdec_si128(state, w128[5]);
state = _mm_aesdec_si128(state, w128[4]);
state = _mm_aesdec_si128(state, w128[3]);
state = _mm_aesdec_si128(state, w128[2]);
state = _mm_aesdec_si128(state, w128[1]);
state = _mm_aesdeclast_si128(state, w128[0]);
_mm_storeu_si128(reinterpret_cast<__m128i *>(&c), state);
}
// CBC mode encryption
void cbc_en(__m128i & KEY, __m128i & IV, uint32_t & len){
__m128i p, c, w[Nr+1];
uint8_t p_bytes[16];
uint8_t c_bytes[16];
c = IV;
KeyExpansion(KEY, w, 0);
while(len >= Nb * 4){
len -= Nb * 4;
fread(&p, sizeof(p), 1, stdin);
AddRoundKey(c, p);
Ciph(c, w);
fwrite(&c, sizeof(c), 1, stdout);
}
memset(p_bytes + len, Nb * 4 - len, Nb * 4 - len);
fread(&p, sizeof(uint8_t), len, stdin);
AddRoundKey(c, p);
Ciph(c, w);
fwrite(&c, sizeof(c), 1, stdout);
}
// CBC mode decryption, input must be multiple of 128 bits
void cbc_de(__m128i & KEY, __m128i & IV, uint32_t & len){
__m128i p, c, last_c, w[Nr+1];
uint8_t p_bytes[16];
memcpy(&last_c, &IV, Nb * 4);
KeyExpansion(KEY, w, 1);
while(len > 0){
fread(&c, sizeof(c), 1, stdin);
p = c;
EqInvCiph(p, w);
AddRoundKey(p, last_c);
last_c = c;
if(len ** Nb * 4){
_mm_storeu_si128((__m128i*)p_bytes, p);
int l = p_bytes[Nb * 4 - 1];
for(int i = 1; i < l; ++i){
if(p_bytes[Nb * 4 - 1 - i] != l) {
l = 0;
break;
}
}
fwrite(&p, sizeof(uint8_t), Nb * 4 - l, stdout);
} else {
fwrite(&p, sizeof(p), 1, stdout);
}
len -= Nb * 4;
}
}