竟然如此简单!C++实现完全随机加密、解析库,并附上完整代码分析

  最近有位朋友问起我之前编写的随机加密库的实现思路方式,讨论过后,我答应他写一个完全随机加密、解密库。这几天,我利用下班时间在原来的思路上做了更改,写出了一个完全随机的加密头,并通过加密头运算出加密值,然后使用加密值与数据运算得到加密效果。接下来,详细分析每一步编写思路。

首先附上VS2010工程:工程下载!
  工程内主要通过DataOperation_类实现加密、解密运算,它是DataOperation_.h和DataOperation_.cpp文件,也可以把这两个文件拷贝到您的工程进行使用。

章节预览:

1.完全随机加密头实现思路
2.完全随机加密数据
3.解析加密头
4.解析加密数据
5.加密文件
6.解析文件
7.总结

章节内容:

1.完全随机加密头实现思路

  首先说下实现思路:

    1. 加密头的前三个字节“LKY”代表库标识,解密也是通过这个标识识别出属于我们的加密文件。
    2. 生成64-128个随机数,这些数字代表我们之后加密、解密用到的数字值。
    3. 通过随机数的长度加“Y”,运算出校验值。校验值的长度与随机数长度相等,它主要用来解密时,取出随机数值。
    4. 随机生成1-4字节的加密字节段,比如加密字节为1,表示1个字节加密,加密字节为4,表示4个字节一起加密。
    5. 通过加密字节加“K”,运算出校验值。校验值的长度与加密字节长度相等,它主要用来解密时,取出加密字节长度。
    6. 指定一次加密数据长度,它的范围在4-4096之间,一般取整除4即可,比如1024,2048等。
    7. 生成加密数据长度标识“L”,它表示加密头的结束标识。

  接下来参考一下源码,或通过工程中的源码与我一起分析加密库的实现思路:

加密头源码

//生成加密头
std::string DataOperation_::EncryptHead(int EncryLen)
{
	int sranLen = 0;
	std::string sran = EncryptRand(sranLen);
	char sranhead[2048] = {0};

	//填充0到sranLen-1字节的运算数
	memcpy(sranhead, "LKY", strlen("LKY"));
	sranhead[sranLen] = ('Y' + sranLen);
	char srandata = 0;
	for (int i = sranLen - 1;i > 2;i--)
	{
		srandata = ((sranhead[i + 1] ^ 128) + i);
		if (0 == srandata)
			sranhead[i] = i;
		else
			sranhead[i] = srandata;
	}

	//填充随机加密数长度
	sranhead[strlen(sranhead)] = (char)sranLen;

	//填充随机加密数
	memcpy(sranhead + strlen(sranhead), sran.data(), sran.length());

	
	//随机加密字节
	srand((unsigned)time(NULL));
	int encryLen = (1 + (rand() % 4));
	sranhead[strlen(sranhead) + encryLen] = ('K' + encryLen);
	int sranheadLen = strlen(sranhead);
	//填充随机数字节的运算数
	for (int i = (sranheadLen + encryLen - 1);i >= sranheadLen;i--)
	{
		srandata = (sranhead[i + 1] ^ 128) + 5;
		if (0 == srandata)
			sranhead[i] = i;
		else
			sranhead[i] = srandata;
	}

	//保存一次数据长度
	m_EncryptionDataLen = EncryLen;

	//填充加密字节长度
	sranhead[strlen(sranhead)] = (char)encryLen;

	//指定一次加密长度
	char headLen[16] = {0};
	itoa(EncryLen, headLen, 10);
	memcpy(sranhead + strlen(sranhead), headLen, strlen(headLen));
	sranhead[strlen(sranhead)] = 'L';

	return std::string(sranhead);
}

首先,我们从EncryptRand函数分析

EncryptRand函数实现代码

//生成随机数字
std::string DataOperation_::EncryptRand(int & RandLen)
{
	//生成64-128之间的加密字节
	srand((unsigned)time(NULL));
	RandLen = (64 + (rand() % 65));

	//生成随机数
	std::string sran;
	char sdata;
	for (int i = 0;i < RandLen;i++)
	{
		sdata = (128 + (rand() % 128));
		sran.push_back(sdata);
	}

	m_RandData = sran;
	return sran;
}

  这个函数比较容易理解,首先生成64-128之间的一个随机数长度,然后通过随机数长度生成对应的随机数。
随机数是128-255之间的数字,按照“^”运算可以正确推算出原值(后面内容讲述为何这么设计)。

回到EncryptHead函数,继续分析:

  代码49-59行,加密头前三个字节为“LKY”,然后在随机数长度那个字节记下标识,比如随机数长度为65,这里就对于第65个字节。在这个字节中,记录随机数长度加“Y”,并由这个字节值运算出第4个字节至随机数长度之间的运算数,它们通过“((sranhead[i + 1] ^ 128) + i)”,即当前数值由下一个数值“^”128加当前数位置得到,这段代码属于反向推理方式运算。

  代码62行,即随机数长度位置加1,赋值为随机数长度,它用来解密时推算出随机数长度。

  代码65行,填充随机数值,它的长度为随机数长度。

  代码69-71行,生成随机加密字节数,取值范围1-4字节,比如当前值为2,将在之前位置加2的字节位置中,填充字节数加“Y”。

  代码72-81行,又是反向推理方式运算,从填充字节位置向前填充运算字节,比如从字节2填充到字节0,它们通过“(sranhead[i + 1] ^ 128) + 5”,即当前数值由下一个数字“^”125加5得到。

  需要注意的是,为了防止字符串遇0结束,随机头里没有0这个数,为0时,由数字5代替。这种实现方式可以避免误读取,比如我们的字节1-4之间,读取到5时,也不会误判。

  代码84-87行,填充随机加密字节长度。

  代码90-93,填空一次加密长度,比如512,1024或2048,把他们以字符串的形式储存,并在最后加上“L”,表示加密头结束标记。

  这段代码初看也许不理解,配合之后的加密、解密运算会发现,这种设计比较容易解析。

2.完全随机加密数据

  首先说下实现思路:

    1. 读取1-4字节数据,转换到long long类型,也就是8字节。这种设计思路可以避免,因位满得到一个错误数据(数据溢出)而无法反推回原值。
    2. 填充未加密的数据。因为我们采用四字节对齐加密,如果加密字节为3,会有1-2个字节未加密,比如1024字节加密,最后一个字节未加密,但我们也要把它储存到数据中。

加密数据源码

//加密数据计算函数
char *DataOperation_::EncryptionData(char * SrcData,int & nDataLen)
{
	if (0 == SrcData || 0 >= nDataLen)
		return 0;

	char pStrData[4096 + 1] = {0};

	int i = 0;
	for (; i < nDataLen;i += m_ByteLen)
	{
		long long data = LongLongFromData(SrcData + i);
		data ^= m_RandData[m_nCurrent++ % m_RandLen];
		
		memcpy(pStrData + i, (char *)&data, m_ByteLen);
	}
	//把剩余的数写进去
	if (0 != nDataLen - i)
	{
		int res = (m_ByteLen - (i - nDataLen));
		memcpy(pStrData + (nDataLen - res - 1), SrcData + (nDataLen - res - 1), res);
	}

	return pStrData;
}

首先,我们从LongLongFromData函数分析

LongLongFromData函数实现代码

long long DataOperation_::LongLongFromData(char *SrcData)
{
	long long ndata = 0;
	for (int i = 0;i < m_ByteLen;i++)
	{
		ndata |= (long long)(*(SrcData + i) << (i * 8));
		if (i == 0)
			ndata &= 0x00000000000000FF;
		else if (i == 1)
			ndata &= 0x000000000000FFFF;
		else if (i == 2)
			ndata &= 0x0000000000FFFFFF;
		else if (i == 3)
			ndata &= 0x00000000FFFFFFFF;
	}

	return ndata;
}

  这段代码设计思路符合位运算原则,为了保证数据正确存入long long类型中,每一次赋值后都要把它的无效高位清除,这样可以避免因long long类型负数,得到错误数据。

  我们实际只用到32位,但为了避免符号位(数据溢出)造成反推错误,只能用更多位的数据来做这些运算。

回到EncryptionData函数,继续分析:

  代码107-113行,我们通过long long类型数字“^”随机数得到加密后的数字,然后加密字节存储。这种方式只需要保存原有字节长度即可,如果不按这种思路实现,每次加密后都要多保存一个字节用来避免数据溢出造成的影响。编写这个库过程中,我做了大量实验,如果所有位都占用的情况,无法反推到真实数据。其中关键原因就是因数据溢出得到一个错误数据。

  代码115-119行,把未加密的数据储存。

3.解析加密头

  首先说下实现思路:

    1. 首先读取一部分数据,比如512字节。这种随机加密头的长度由随机数限制,最多不会超过随机数的3倍,也就是384字节之内。
    2. 得到随机加密头长度后,把多余的数据返还给原数据中,然后抛弃加密头进行解密运算。

解析加密头源码

//解析加密头
int DataOperation_::DecryptHeadLen(std::string EncryData, int & HeadLen)
{
	if (EncryData.empty())
		return -1;

	m_HeadLen = HeadLen = DecryptHead(EncryData, m_RandData, m_RandLen, m_ByteLen, m_EncryptionDataLen);

	return m_EncryptionDataLen;
}

  通过DecryptHead函数得到加密头保存的有效数据,m_RandData表示随机数,m_RandLen表示随机数长度,m_ByteLen表示随机字节长度,m_EncryptionDataLen表示一次加密长度,m_HeadLen 表示加密头长度。

接下来分析DecryptHead函数

DecryptHead函数实现代码

//解析加密头
int DataOperation_::DecryptHead(std::string HeadData, std::string & EncryRand, int & EncryRandLen, int & ByteLen, int & EncryLen)
{
	if (3 >= HeadData.length() || (HeadData[0] != 'L' || HeadData[1] != 'K' || HeadData[2] != 'Y'))
		return -1;

	int HeadLen = 0;;
	int Num = 0;
	for (int i = 3;i < HeadData.length() - 1;i++)
	{
		if ((char)(HeadData[i + 1] - i) ^ 128 == (char)HeadData[i] || i == (char)HeadData[i])
			++Num;
		else if ((char)HeadData[i + 1] == (char)(HeadData[i] - 'Y'))
		{
			if (3 < Num)
			{
				//获取随机数长度
				EncryRandLen = HeadData[i + 1];
				//记录数据头长度
				HeadLen = (i + 2);
				//截取随机数之前的数据
				HeadData = HeadData.substr(i + 2, HeadData.length() - i - 2);
				break;
			}
			else
				Num = 0;
		}
		else if (i > 128)
			return -1;
	}

	//获取随机数
	EncryRand = HeadData.substr(0, EncryRandLen);

	//截取随机数之后的数据
	HeadData = HeadData.substr(EncryRandLen, HeadData.length() - EncryRandLen);
	HeadLen += EncryRandLen;

	//获取连续加密字节长度
	for (int i = 0;i < HeadData.length() - 1;i++)
	{
		if (((char)HeadData[i + 1] == (char)HeadData[i] - 'K') && (((char)HeadData[i] == (char)((HeadData[i - 1] - 5) ^ 128)) || i == (char)HeadData[i]))
		{
			ByteLen = HeadData[i + 1];
			//记录数据头长度
			HeadLen += (i + 2);
			//截取随机数之前的数据
			HeadData = HeadData.substr(i + 2, HeadData.length() - i - 2);
			break;
		}
	}
	
	//获取一次加密长度
	for (int i = 0;i <  HeadData.length();i++)
	{
		if ('L' == (char)HeadData[i])
		{
			EncryLen = atoi(HeadData.substr(0 , i).data());
			++HeadLen; 
			break;
		}

		++HeadLen;
	}

	return HeadLen;
}

  代码158-159行,首先检测数据前三个字节是否为“LKY”,如果是“LKY”则继续解析。

  代码163-184行,通过第四字节向后解析,通过当前值“^”128或当前值等于当前位置(为了避免加密头中包含0,而赋值为当前位置)。 如果连续4个字节或4个以上字节符合我们的运算要求,并等于随机数长度,则通过当前值减去下一个值加“Y”,取出随机出长度,得到随机数位置。这种设计方式可以避免因偶然数据与我们的运算结果相同导致的“错位”现象。

  代码187行,取出随机数值。

  代码190-191行,截取掉随机数值和之前的数据。

  代码194-205行,通过当前值减去下一个值加“K”取出随机字节长度,实现思路与取随机数相同。

  代码208-218行,通过数据头结束标志“L”,取出一次加密字节数,并确定加密头长度。

4.解析加密数据

  这部分实现思路与加密数据思路相同,因为我们通过long long类型数字 “^” 随机数得到,同样我们也可以按这种方式反推出原数据。

解析加密头源码

//解密数据计算函数
char *DataOperation_::DecryptionData(char * SrcData,int & nDataLen)
{
	if (0 == SrcData || 0 >= nDataLen)
		return 0;

	char pStrData[4096 + 1] = {0};
	int i = 0;
	for (; i < nDataLen;i += m_ByteLen)
	{
		long long data = LongLongFromData(SrcData + i);
		data ^= (char)m_RandData[m_nCurrent++ % m_RandLen];
		memcpy(pStrData + i, (char *)&data, m_ByteLen);
	}
	//把剩余的数写进去
	if (0 != nDataLen - i)
	{
		int res = (m_ByteLen - (i - nDataLen));
		memcpy(pStrData + (nDataLen - res - 1), SrcData + (nDataLen - res - 1), res);
	}

	return pStrData;
}

  这里的代码与加密数据函数相同,这样设计也就是为了方便解密数据,并且可以做到无限叠加形式加密,无限叠加形式解密。比如一个文件加密10次,通过解密10次即可得到原数据。

5.加密文件

  这部分内容就比较简单了,首先做出加密头,然后对原数据进行加密后保存即可。

加密文件源码

//加密文件
bool DataOperation_::EncryptionFile(CString Filename,bool bCover)
{
	if (Filename.IsEmpty())
		return false;
	CFile file1,file2;
	if (!file1.Open(Filename,CFile::modeRead))
		return false;

	ULONG64 FileLen = file1.GetLength();
	if (0 == FileLen)
		return false;

	CString Filename2(Filename.Mid(0,Filename.ReverseFind(_T('.'))));
	Filename2.AppendFormat(_T("_Temp%s"),Filename.Mid(Filename.ReverseFind(_T('.')),
		Filename.GetLength() - Filename.ReverseFind(_T('.'))));

	if (!file2.Open(Filename2,CFile::modeCreate | CFile::modeWrite))
		return false;
	char StrData[4096] = {0};
	int DataLen = 0;
	int Encryt = 0;

	std::string encry = EncryptHead(1024);
	int DecryptionLen = 0;
	if (DecryptionLen = DecryptHeadLen(encry, Encryt))
	{
		file2.Write(encry.data(), encry.length());
	}
	else
	{
		bCover = false; 
		goto Exit;
	}

	while (0 < FileLen)
	{
		if (DecryptionLen <= FileLen)
		{
			file1.Read(StrData,DecryptionLen);
			DataLen = DecryptionLen;
			char *WriteFile = EncryptionData(StrData,DataLen);
			file2.Write(WriteFile,DataLen);
			FileLen -= DecryptionLen;
		}
		else
		{
			file1.Read(StrData,FileLen);
			DataLen = FileLen;
			char *WriteFile = EncryptionData(StrData,DataLen);
			file2.Write(WriteFile,DataLen);
			FileLen -= FileLen;
			break;
		}
	}

Exit:
	file1.Close();
	file2.Close();
	if (bCover)
	{
		USES_CONVERSION;
		//覆盖本地文件
		int nResult = remove(T2A(Filename));
		nResult = rename(T2A(Filename2),T2A(Filename));
	}
	return true;
}

  代码252-258行,检测需要加密的文件不能为空。

  代码260-262行,生成临时文件以文件名加_Temp加后缀命名,比如test123.txt,临时文件名为test123_Temp.txt,文件加密完成后,替换为test123.txt。

  代码270行,生成加密头信息。

  代码272行,解析加密头并在类中保存相关信息。

  代码274行,在临时文件中写入加密头信息。

  后面的内容就是取出数据加密,然后写入临时文件,最后替换原有文件。

6.解析文件

  这部分内容与加密文件相似,首先解析加密头,然后对加密数据进行解析后保存即可。

解析源码

 //解密文件
bool DataOperation_::DecryptionFile(CString Filename,bool bCover)
{
	if (Filename.IsEmpty())
		return false;
	CFile file1,file2;
	if (!file1.Open(Filename,CFile::modeRead))
		return false;
	ULONG64 FileLen = file1.GetLength();
	if (512 >= FileLen)
		return false;

	CString Filename2(Filename.Mid(0,Filename.ReverseFind(_T('.'))));
	Filename2.AppendFormat(_T("_Temp%s"),Filename.Mid(Filename.ReverseFind(_T('.')),
		Filename.GetLength() - Filename.ReverseFind(_T('.'))));

	if (!file2.Open(Filename2,CFile::modeCreate | CFile::modeWrite))
		return false;
	char StrData[2048 + 100] = {0};
	int DataLen = 0;
	file1.Read(StrData,512);
	//解密数据头
	int DecryptionLen = 0;
	int HeadLen = 0;
	if (DecryptionLen = DecryptHeadLen(StrData, HeadLen))
	{
		file1.Seek(HeadLen, CFile::begin);
		FileLen -= HeadLen;
	}
	else
	{
		bCover = false; 
		goto Exit;
	}
	
	while (0 < FileLen)
	{
		if (DecryptionLen <= FileLen)
		{
			DataLen = DecryptionLen;
			file1.Read(StrData,DataLen);
			char *WriteFile = DecryptionData(StrData,DataLen);
			memset(StrData,0,strlen(StrData));
			memcpy(StrData,WriteFile,DataLen);
			file2.Write(StrData,DataLen);
			FileLen -= DecryptionLen;
		}
		else
		{
			DataLen = FileLen;
			file1.Read(StrData,FileLen);
			char *WriteFile = DecryptionData(StrData,DataLen);
			memset(StrData,0,strlen(StrData));
			memcpy(StrData,WriteFile,DataLen);
			file2.Write(StrData,DataLen);
			FileLen -= FileLen;
			break;
		}
	}

Exit:
	file1.Close();
	file2.Close();
	if (bCover)
	{
		USES_CONVERSION;
		//覆盖本地文件
		int nResult = remove(T2A(Filename));
		nResult = rename(T2A(Filename2),T2A(Filename));
	}

	return true;
}


7.总结

  整体来说,这个完全随机加密、解析库还算比较简单,主要运用了位运算相关知识。由于私下时间比较少,我大概就抽出了10个小时,其中5个小时用来调试修改因疏忽造成的数据未对齐问题。

  这个库只能算雏形,以后有时间可能在这个库的基础上增加压缩功能,比如把原数据加密后压缩4倍甚至10倍。当然,如果以后有比较好的思路,也会重新设计一个新形式加密库。

  这个库运用的知识,在本人编写的“一起学习C语言”系列文章中做了详细讲述,有兴趣的朋友可以参考本系列文章!

    原文作者:坤昱
    原文地址: https://blog.csdn.net/a29562268/article/details/104663363
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞