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;
		}
}
posted @ 2020-06-04 11:45  cyssmile  阅读(2864)  评论(0编辑  收藏  举报