实验内容:
在本次实验中,需要实现两个加密/解密系统,一个在密文分组链接模式(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;
- }
- }