最近有位朋友问起我之前编写的随机加密库的实现思路方式,讨论过后,我答应他写一个完全随机加密、解密库。这几天,我利用下班时间在原来的思路上做了更改,写出了一个完全随机的加密头,并通过加密头运算出加密值,然后使用加密值与数据运算得到加密效果。接下来,详细分析每一步编写思路。
首先附上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语言”系列文章中做了详细讲述,有兴趣的朋友可以参考本系列文章!