上一篇文章我介绍了流式加密(CK)在游戏发行中的应用,那么流式加密(CK)到底是什么样的加密方式,和现有的加密方法又有什么不同呢?下面根据流式加密(CK)的实际测试对其进行一个介绍,后面会给出加解密的C++实现。
基于发明专利(201810399879.X)的流式加密(CK)是一种基于置换式、多密钥、对称式加密方法,大部分的对称加密算法都是基于DES、AES的变形,DES和AES都是块式加密,也就是说在加密时需要对前面的数据进行对齐,在块中的数据前后依赖,在生成时会将前面生成的密文一起参与生成下一个密文,这样就使解密时必需得到一整块数据才能进行解密,在随机读写时,就必需要进行数据的对齐处理。而流式加密(CK)则只针对字节进行置换式加密,然后通过多密钥的算法,生成伪随机流,伪随机流的大小和明文的大小是一样的,通过算法可以直接求出某一个位置的伪随机流的值,这样可以最有效地支持随机读写,使数据可以直接以密文的形式保存在存储介质上,只在使用前在程序内部进行数据块的解密。
发明专利只讲了加解密方法的实现原理,阅读起来比较费力,以程序化的方式来讲解可能会比较容易理解。
流式加密(CK)由密码表(K)、私钥(PK)、设备密钥(DK)、位置密钥(SK)等多个基本元素(密钥)构成,其中:
- 密码表(K)是一个256字节的数组,初始时数组的值是从[0x00,0xFF]的顺序值,其值的位置被随机打乱后就会形成从[0x00,0xFF]的一个随机密码表,其所有的可能为256!,这是一个天文数字,基本上可以表示地球上的每一个沙尘。
- 私钥(PK)相当于个人的密码,一般由用户输入和保存。
- 设备密钥(DK)可以是硬件设备的唯一标识,或者是用户账号等标识用户身份的唯一标识、或者是待加密文件(或多个文件)的唯一标识,通过设备密钥(DK)来绑定硬件设备,使加密的文件只能在指定的硬件设备上或指定的用户才能解密。DK的引入,使加密的文件即使被拷贝到其它设备上也不能正确解密,确保了文件的不可转移性。
- 位置密钥(SK)根据待加/解密数据的位置通过算法生成伪随机流。
了解了这几个基本元素后,我们看一下它的基本工作原理:因为是字节加密,因此,一个字节所能表示的范围正好也是[0x00,0xFF],我们把待加密的明文用C来表示,也就是说C一定是一个[0x00,0xFF]之间的数字,而把C作为密码表(K)的数组下标,就可以在密码表(K)中得到一个对应的值,这个值就是密文,如果不知道密码表,加密后的数据是不可能还原出明文的。解密过程正好是这个过程的逆过程,密文(P)的取值范围也是[0x00,0xFF],通过密文(P)在密码表中反查P在密码表中的下标位置,这个下标值就是明文C。当然加密和解密的过程反过来也是可以的。
原理实际很简单,你可能会说,这样的加密方法多个相同的明文会变成相同的密文,只要根据上下文中的联系,有足够多的明文和密文做参考,就可以得到密码表。如果流式加密(CK)只实现了这个,那真的不值一提,起不到加密的作用。
要想使相同的明文得到不同的密文,其它的多个密钥就发挥出了巨大的作用。
在对称加密中的算法基本上就是移位或与伪随机流中的某一个数据进行异或,而流式加密中伪随机流的生成就是关键中的关键,如何利用多密钥生成伪随机流实际上也是流式加密(CK)中的关键。
伪随机流由私钥(PK)、设备密钥(DK)、位值密钥(SK)共同构成。设备密钥(DK)、位置密钥(SK)也可以不是单一的密钥,而是多种密钥的组合。例如:私钥(PK)由用户的密码或账号标识的MD5构成,设备密钥(DK)由设备MAC地址的MD5构成,由PK和DK共同组合成一个256字节的基准伪随机密码,而位置密钥(SK)则通过算法实现基准伪随机流的无限扩展的随机性。
下面是用C++的实现,通过代码来进一步了解其实现方法。
首先我通过模板类定义了整个初始化密码表和加解密的过程,然后通过模板参数类来定义了几个密钥和具体算法的实现,这样在修改算法时只需要简单地创建一个参数类就可以快速地定义出新的加解密算法。
//CK_TYPE的可能取值是char、wchar_t
template<typename CK_DATA_TYPE, //CipherKey数据的类型,unsigned char、unsigned short
const int CK_DATA_SIZE, //CipherKey索引表的大小,如果CK_DATA_TYPE类型为unsigned char时为256,CK_DATA_TYPE类型为unsigned short时为65535
typename CREATE_NEW_CK_CLASS, //创建新密码表的模板参数类,定义了创建新的密码表的方法
typename PRIVATE_KEY_CLASS> //私有Key的模板参数类,定义了私有密码的处理方法
class CK_InitImpl
{
private:
CK_DATA_TYPE m_ckBuffer[CK_DATA_SIZE];
public:
CK_InitImpl()
{
};
/**
* 根据传递的PK,生成密码表。
* 密码表的大小由iCipherKeySize来决定
* @param pk 私有密码表的处理类。
* @param iCKDataBufferSize 返回混淆后的密码表的大小。
* @return 返回混淆后的密码表。
*/
CK_DATA_TYPE* makeCipherKey(PRIVATE_KEY_CLASS& pk, int& iCKDataBufferSize)
{
//创建密码表
CREATE_NEW_CK_CLASS::createNewCipherKey(this->m_ckBuffer);
//对密码表中的数据进行加密
CREATE_NEW_CK_CLASS::encryptCipherKeyData(this->m_ckBuffer, pk);
//传递CipherKey在混淆密码表中的偏移位置
iCKDataBufferSize = pk.getCKDataOffset();
//生成混淆后的密码表
CK_DATA_TYPE* pszConfuseCipherKey = CREATE_NEW_CK_CLASS::makeConfuseCipherKey(this->m_ckBuffer, iCKDataBufferSize);
//保存PrivateKey用于快速校验的HASH值
pk.verifyPinByCipherKey(pszConfuseCipherKey, CREATE_NEW_CK_CLASS::CONST_CONFUSE_DATA_SIZE, true);
//也可以对混淆后的密码表,根据PrivateKey,进行再次打乱
return pszConfuseCipherKey;
}
};
上面这个模板类定义了需要创建新的密码表的整个过程。
- 模板参数CK_DATA_TYPE的取值可以是unsigned char或unsigned short,即可以是字节,也可以是按字来实现加解密,或者以块的大小来实现加解密,在以非字节为加解密单元时,需要注意数据的对齐。
- 模板参数CK_DATA_SIZE是一个数字,表示密码表的大小。在以字节为加密单元时,密码表的大小是256,而以字为加密单元时,密码表的大小应该是65536。
- 模板参数类CREATE_NEW_CK_CLASS定义了创建密码表的方法,其混淆和生成随机密码表的方法可以自己来实现。
- 模板参数类PRIVATE_KEY_CLASS定义了私钥(PK)的实现类。
这里要注意的是,引入了一个混淆密码表的概念,即密码表(K)可以被保存在云端或者本地,密码表(K)的256字节的数据被放在了一段乱码中,并且密码表(K)本身的值按照私钥(PK)的值进行了简单的异或操作,做了一个简单的加密处理。这样当混淆后的密码表本身被他人获得时,也密需解密后才能使用。
class CK_CreateNewCK_Byte
{
private:
CK_CreateNewCK_Byte()
{
}
public:
enum
{
CK_DATA_SIZE = 256,
CONST_CONFUSE_DATA_SIZE = CK_DATA_SIZE * 2,
};
/*
生成混淆后的密码表,在密码表的前面和后面填充随机数。
* @param pszCKBuffer 指向原密码表的缓冲区的首地址。
* @param iCKBufferSize 密码表在缓冲区中有效数据的大小。
* @return 返回原密码表的一个副本。
*/
inline static unsigned char* copyCK(const char* pszCKBuffer, int iCKBufferSize)
{
unsigned char* pszCKCopy = new unsigned char[iCKBufferSize];
::memcpy_s((char*)pszCKCopy, iCKBufferSize, pszCKBuffer, iCKBufferSize);
return pszCKCopy;
}
/**
* 对密码表中的数据进行加密
* @param pszCKBuffer 保存密码表的缓冲区的首地址。注意其大小应该是该类的CK_DATA_SIZE规定的大小
* 返回时,该值中的缓冲区内容被填充成了未加密过的随机的密码表
*/
inline static void createNewCipherKey(unsigned char* pszCKBuffer)
{
for (int i = 0; i < CK_CreateNewCK_Byte::CK_DATA_SIZE; ++i)
{
pszCKBuffer[i] = (unsigned char)i;
}
CK_CreateNewCK_Byte::disorganizedIntoPseudoRandomCipherKey(pszCKBuffer);
}
/**
* 对密码表中的数据进行加密
* @param pszCKBuffer 未加密的密码表
* @param pk 私钥
*/
inline static void encryptCipherKeyData(unsigned char* pszCKBuffer, CK_PrivateKey_Byte& pk)
{
for (int i = 0; i < CK_CreateNewCK_Byte::CK_DATA_SIZE; ++i)
{
pszCKBuffer[i] ^= pk.getXorCodeByIndex(i);
}
}
/*
生成混淆后的密码表,在密码表的前面和后面填充随机数。
* @param pszCipherKey 指向密码表的开始。
* @param iCKDataStartOffset 真实密码表在混淆密码表的