CBC和CTR模式下的AES
实验内容:
在本次实验中,需要实现两个加密/解密系统,一个在密文分组链接模式(CBC)下使用AES,另一个在计数器模式(CTR)中使用AES。
实验环境:
VS2019、C++、 Crypto++
实验过程:
1、安装Crypto++
1.1官网下载Crypto++
官网地址:https://www.cryptopp.com/
1.2解压编译,生成.lib文件
解压后,用vs打开里面的.sln工程文件,会得到四个工程。
将cryptlib项目设为启动项,选中cryptlib,选择Debug x64模式,按下ctrl + B生成cryptlib。
1.3配置工程环境
新建工程,右键工程选择属性,选择VC++目录,设置包含目录和库目录。库目录就是头文件所在目录,库目录就是刚生成的.lib所在目录。
选择链接器->输入->附加依赖项,输入刚才的生成的.lib文件完整名字。
选择c/c++ ->代码生成 -> 运行库,选择多线程调试(/MTd)
2、CBC模式下的AES原理
EBC和CBC模式都是分块加密,经常要对plaintext进行填充,使之满足16字节的整数倍。一般EBC模式下,如果采用相同的内容和相同的秘钥,结果密文是相同的,这样是不安全的。CBC引入向量IV的概念,加密过程除了提供key和plaintext还需要提供IV,这个IV大小为16个字节。密文中前16字节,就是IV。IV会参与第一块的加密,之后就使用上一块加密的结果代替IV。这样做的好处就是使得相同的内容相同的秘钥,加密的结果可能不同。
加密过程:
先将plaintext填充为16字节的整数倍,然后将plaintext等分为n份。第一次加密时,将IV与P1进行异或操作得到结果,然后将这个结果进行AES加密,得到C1。将C1链接到密文中去,并将C1代替IV,参与下一次的异或操作。然后重复上述操作,直到所有的block都进行加密。
解密过程:
选读取密文的前16个字节,这16个字节就是IV。然后将去除IV的plaintext等分为n份。
先取出C1(第一块密文),将C1通过AES解密得到结果T1, 然后将T1与IV异或的得到第一块明文。再用C1替换IV参与下一次解密运算,重复上述操作。
单独处理最后一块,这里最后一块采用的PKCS7的填充方式,这种方式填充最后几个字节表示填充了多少个字节,获取最后一个字节的数字,只需要在明文结果后面删除这个长度的字节就得到真正的明文。
3、CBC模式下AES加密解密实现
3.1 CBC_AES解密代码
void CBCdecrypto(const string& key,const string& ciphertext, string& plaintext)
{
string vi = ciphertext.substr(0, AES::BLOCKSIZE);//AES::BLOCKSIZE =16
string text = ciphertext.substr(AES::BLOCKSIZE, ciphertext.size() - AES::BLOCKSIZE);//前16为vi后16为填充
size_t groupNumber = text.size() / AES::BLOCKSIZE;
AESDecryption cryptor;
cryptor.SetKey((byte*)key.c_str(),key.size());
for (size_t i = 0 ; i<groupNumber;i++)
{
//获取每一次的分组密文
string block = text.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
byte temp[AES::BLOCKSIZE];
memset(temp, 0x30, AES::BLOCKSIZE);
cryptor.ProcessBlock((byte*)block.c_str(), temp);
for (int j = 0; j < AES::BLOCKSIZE; j++) {
plaintext.push_back((byte)vi[j] ^ temp[j]);
}
vi = block;
}
string paddingBlock = plaintext.substr((groupNumber - 1) * AES::BLOCKSIZE, AES::BLOCKSIZE);
int paddingNum = (byte)paddingBlock[AES::BLOCKSIZE - 1];
for (int i = 0; i < paddingNum; i++) {
if (plaintext.back() != paddingNum) {
cout << "密文出错" << endl;
exit(0);
}
plaintext.pop_back();
}
}
3.2 CBC_AES加密代码
string CBCencrypto(string hexKey, string hexVI, string plaintext) {
string key;
hex_to_str(hexKey, key);
string VI;
hex_to_str(hexVI, VI);
string outstr;
outstr.clear();
outstr += VI;
//填充plaintext 使之成为16字节的整数倍
int paddingNum = AES::BLOCKSIZE - (plaintext.size() % AES::BLOCKSIZE);
for (int i = 0; i < paddingNum; i++) {
if (i == paddingNum - 1)
{
plaintext += (char)paddingNum;//最后一个byte表示填充多少个字节
}
else {
plaintext += (char)(15);//其他填充为0x0F
}
}
//获取多少个组
int groupNumber = plaintext.size() / AES::BLOCKSIZE;
AESEncryption encryptor((byte*)key.c_str(), AES::MIN_KEYLENGTH);
for (int i = 0; i < groupNumber;i++)
{
string Pi = plaintext.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
//想让IV 与 Pi异或
byte temp[AES::BLOCKSIZE];
memset(temp, 0x30, AES::BLOCKSIZE);
for (int j = 0; j < AES::BLOCKSIZE; j++)
{
temp[j] = (byte)Pi[j] ^ VI[j];
}
//然后将temp进行aes
byte tempr[AES::BLOCKSIZE];
encryptor.ProcessBlock(temp, tempr);
//将tempr添加到outstr上
string PiResult;
PiResult.clear();
for (int j = 0; j < AES::BLOCKSIZE;j++) {
PiResult += tempr[j];
}
outstr += PiResult;
//更新IV,参与下次运算
VI = PiResult;
}
//还需要将outstr 转成十六进制的情况
string hexOutstr;
hexOutstr.clear();
for (int i = 0; i < outstr.size(); i++)
{
string stemp;
char2hexs(outstr[i], stemp);
hexOutstr += stemp;
}
return hexOutstr;
}
4、CTR模式下的AES原理
CTR有一个计数器counter,一般为16字节,前后两次的加密与加密结果无关。每次加密counter加一,所以加密速度更快,但是安全性比CBC模式稍低点。而且CTR加密不需要填充,类似流模式。密文的前16个字节为counter。
加密过程:
先选取counter,如果没有16字节就填充,现将counter通过AES进行加密,得到结果T1,
然后将T1与明文的第一分组进行异或得到结果C1,将C1链接到密文上,然后将counter+1进行下一轮加密。
对最后一个非整块的明文单独处理,处理方法与上面类似,只是长度按照剩余块的长度处理。
解密过程:
选读取密文中前16个字节作为counter,然后将去除counter的密文按照16个字节等分,最后一个非整16字节的单独处理。
现将counter进行AES解密得到Ti然后,将Ti与密文块Ci进行异或得到明文Pi,将Pi链接到输出明文上,counter+1进行下一轮解密。
对后面非整16的块单独处理,处理方法类似。
5、CTR模式下AES加密解密实现
5.1 CTR_AES 解密代码
void CTRdecrypto(const string& key, const string& ciphertext, string& plaintext)
{
// 密文的前 16 个字节为计数器的初始值
string counter = ciphertext.substr(0, AES::BLOCKSIZE);
string text = ciphertext.substr(AES::BLOCKSIZE, ciphertext.length() - AES::BLOCKSIZE);
int groupNumber = text.length() / AES::BLOCKSIZE;
AESEncryption aesEncryptor;
aesEncryptor.SetKey((byte*)key.c_str(), key.length());
byte aesResult[AES::BLOCKSIZE];
for (int i = 0; i <groupNumber; i++) {
string ciphertextBlock = text.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
memset(aesResult, 0x30, AES::BLOCKSIZE);
aesEncryptor.ProcessBlock((byte*)counter.c_str(), aesResult);
// 密文和 AES 加密结果异或,得到明文
for (int j = 0; j < AES::BLOCKSIZE; j++) {
plaintext.push_back(aesResult[j] ^ (byte)ciphertextBlock[j]);
}
// 计数器自增
counter = counterIncrement(counter, 1);
}
int residueLen = text.length() - groupNumber * AES::BLOCKSIZE;
string residueCiphertext = text.substr(groupNumber * AES::BLOCKSIZE, residueLen);
memset(aesResult, 0, AES::BLOCKSIZE);
aesEncryptor.ProcessBlock((byte*)counter.c_str(), aesResult);
for (int j = 0; j < residueLen; j++) {
plaintext.push_back(aesResult[j] ^ (byte)residueCiphertext[j]);
}
}
5.2 CTR_AES加密代码
string CTRencrypto(string hexKey, string counter, string plaintext)
{
string key;
hex_to_str(hexKey, key);
string outstr;
outstr.clear();
outstr += counter;
//CTR获取多少个整数 的16bytes
int num = plaintext.size() / AES::BLOCKSIZE;
AESEncryption encryptor((byte*)key.c_str(), AES::MIN_KEYLENGTH);
byte temp[AES::BLOCKSIZE];
for (int i = 0; i < num; i++) {
memset(temp, 0x30, AES::BLOCKSIZE);
encryptor.ProcessBlock((byte*)counter.c_str(), temp);
string block = plaintext.substr(i * AES::BLOCKSIZE, AES::BLOCKSIZE);
for (int j = 0; j < AES::BLOCKSIZE; j++) {
outstr.push_back(temp[j]^block[j]);
}
counter = counterIncrement(counter, 1);
}
/*
*处理最后一个非整块的block
*/
int len = plaintext.size() - (num * AES::BLOCKSIZE);
string lastBlock = plaintext.substr(num * AES::BLOCKSIZE - 1, len);
memset(temp, 0x30, AES::BLOCKSIZE);
encryptor.ProcessBlock((byte*)counter.c_str(), temp);
for (int i = 0; i < len; i++) {
outstr.push_back(lastBlock[i] ^ temp[i]);
}
/*
*将输出转换正十六进制
*/
string hexOutstr;
hexOutstr.clear();
for (int i = 0; i < outstr.size(); i++)
{
string stemp;
char2hexs(outstr[i], stemp);
hexOutstr += stemp;
}
return hexOutstr;
}
6、实验结果
对老师给出的test.txt的解密结果如下:
附:
1、其他代码解释
Class Exercise_3
hexKeys 和hexCiphertexts为十六进制的秘钥和密文组。
keys和ciphertexts为转换成byte数组的秘钥和密文组。
plaintexts 是解密后的原文组。
key_path和cipher_path分别为秘钥和密文的存放路径。
modeVec是存放加载的秘钥密文需要解码的方式,这里有定义
方法解释:
bool decrypto() 统一对读取的秘钥密文处理,得到所有的明文。
void printPlaintexts() 打印所有的结果信息。
bool init() 主要实现加载秘钥和密文并转换数据格式。
bool load_keys()加载秘钥。
bool load_ciphers() 加载密文。
void changeDataFormal() 改变秘钥密文格式。
其他顶层函数
void hex_to_str(const string& stringData, string& str)将十六进制字符串转成byte字符串。
void char2hexs(char ch, string& s) 将一个char类型转成字符串类型。
string counterIncrement(string counter, int n) counter的自增操作。
string CBC_AESEncryptStr(string sKey, string sIV, const char* plainText) CBC模式的调库实现。
2、文件目录格式
keys.txt与ciphertexts.txt中数据以空格分隔。
- 上述代码有个错误的地方,单独的 byte表示范围是在-128 到127 ,不能表示我们要的范围0 - 255 ,应该换成 unsigned char
最后一个问题,在由字符串转十六进制的那里有错
对于一个字符转十六进制 直接用 int temp = (int)ch这种方式转会有正有负
这时候可以将它与oxff 相与,就为正了。
还有就是用 stringstream这种方式转,比如0x03,他会转成0x3.所以这个时候可以先设置长度为二 比如 ss<<hex<<setfill('0'); ss<<setw(2),也可以像我这样实现,效果是一样的
void char2hexs(char ch, string& s)
{
s.clear();
stringstream ss;
ss.clear();
int temp = (int)ch;
temp = temp & 0xff;
ss << hex << temp;
s = ss.str();
if (s.size() == 1) {
s = '0' + s;
}
}