2.3 对资源的加密解密
对资源进行加密可以很好地防止资源被盗用,一般需要对游戏的图片、模型、配置、脚本等资源进行加密,对于图片和脚本的加密,Cocos2d-x提供了比较便捷的加密解密方法,当然也可以使用DES、3DES、AES等常用的加密算法,甚至自己设计的加密算法来对资源进行加密。
2.3.1 使用TexturePacker加密纹理
TexturePacker是非常强大的图片打包工具,提供了强大的加密功能,在Cocos2d-x中可以通过一行简单的代码设置密钥,在加载TexturePacker加密过的图片时会自动解密,TexturePacker使用的是安全高效的xxtea算法,但美中不足的是目前只支持.pvr.ccz格式,这个格式并不建议在iOS之外的平台使用。首先来了解一下如何加密,可以通过TexturePacker的界面工具和命令行工具进行加密,需要设置一个32位十六进制值的密钥。在TexturePacker左侧的输出设置面板中设置纹理格式为.pvr.ccz,然后单击Content protection旁边的小锁按钮,就会弹出密钥设置窗口(如图2-4所示),可以在编辑框中输入密钥,或者单击Create new key按钮自动生成一个新的密钥,Clear/Disable按钮可以清除密码。
图2-4 TexturePacker加密
通过TexturePacker的命令行工具,在命令行中添加一个选项-content-protection <key>即可,使用命令行工具可以很方便地在脚本中对图片进行批量处理。在TexturePacker的官网https://www.codeandweb.com/texturepacker/documentation有命令行工具使用的详细介绍。
在代码中只需要添加一行代码,把密钥设置进去即可。
ZipUtils::ccSetPvrEncryptionKey(0xd8479b9f, 0xd8961025, 0x419da14a, 0x81e5d801);
2.3.2 对Lua脚本进行加密
Quick提供了一个简单的脚本加密工具,可以在Windows和Mac系统下使用,它可以将Lua脚本编译、加密并压缩成一个zip包,在Cocos2d-x中也可以很方便地使用加密后的脚本,可以在github上面获取Quick的源码https://github.com/chukong/quick-cocos2d-x。
在Quick的bin目录下可以找到compile_scripts脚本,在Windows下是compile_scripts.bat,在Mac系统下则是compile_scripts.sh,在控制台中运行该脚本,传入对应的参数即可。例如,执行compile_scripts -i ..\welcome\src -o welcome.zip -e xxtea_zip -ek mykey,即可将指定目录下的所有脚本编译打包为zip文档,并进行加密,如图2-5所示。
图2-5 加密Lua脚本
compile_scripts的选项有很多,直接输入compile_scripts或compile_scripts -h命令即可显示帮助说明,如图2-6所示。常用选项的含义如下。
图2-6 编译脚本帮助说明
❑ i:指定源文件路径。
❑ -o:指定输出文件路径。
❑ -p:包前缀。
❑ -x:指定要排除的目录(不打包)。
❑ -m:编译模式。
❑ -e:加密模式。
❑ -ek:加密密钥,设置了加密模式之后必须设置密钥。
❑ -es:加密签名,默认值为XXTEA,意义不大。
❑ -ex:加密文件的扩展名(默认是.lua)。
❑ -c:使用指定的配置来编译。
❑ -q:静默编译,不输出任何信息。
编译有以下3种模式:
❑ zip模式为默认模式,即将所有源码编译后打包成一个zip压缩包。
❑ c模式会将所有源码编译后生成一对C的源文件和头文件,文件中定义了存储字节码的数组以及相关的接口,使用生成的接口可以加载这些Lua脚本。
❑ files模式会将所有源码编译之后不进行打包,编译后的文件会被输出到-o选项所指定的路径下。
加密有以下两种模式:
❑ xxtea_zip模式会使用XXTEA算法加密整个zip包,需要配合zip编译模式使用。
❑ xxtea_chunk模式会使用XXTEA算法加密每一个编译后的脚本文件,默认签名为XXTEA。
加密之后只需要在程序初始化时,调用LuaStack的setXXTEAKeyAndSign()方法设置密钥和签名,即可使用加密后的脚本,如果将脚本编译后打包成一个zip压缩包,需要调用LuaStack的loadChunksFromZIP()方法来加载压缩包中的脚本。在loadChunksFromZIP()方法中会判断zip包是否经过了XXTEA加密,如果是则进行解密,并取出里面的文件,逐个调用luaLoadBuffer()方法加载脚本文件。在luaLoadBuffer()方法中会判断要加载的脚本是否经过了XXTEA加密,如是则进行解密,然后载入Lua虚拟机中。
bool AppDelegate::applicationDidFinishLaunching() { … LuaStack *pStack = pEngine->getLuaStack(); //如果设置了 -e和 -ek需要调用setXXTEAKeyAndSign设置密钥 //pStack->setXXTEAKeyAndSign("mypassword", strlen("mypassword")); //如果设置了 -e和 -ek -es需要调用setXXTEAKeyAndSign设置密钥和签名 pStack->setXXTEAKeyAndSign("mypassword", strlen("mypassword"), "mysign", strlen("mysign")); pStack->loadChunksFromZip("res/game.zip"); pStack->executeString("require 'main'"); return true; }
在某些情况下,将Lua脚本编译会导致一些问题,如iOS下的兼容性问题,在另外一些情况下将脚本编译好打包成zip也会导致一些其他的问题,如无法使用热更新。
这种情况下希望能够不编译脚本、不打包成zip,只是加密脚本,那么应该怎么做呢?可以使用cocos.py来打包,它支持在打包的时候加密且不编译Lua脚本,可以输入cocos compile-h命令来查看cocos.py编译相关的帮助信息,如图2-7所示。
图2-7 cocos.py的帮助信息
在编译的时候使用--compile-script选项,指定参数为0可以关闭Lua和JS脚本的编译,而使用--lua-encrypt选项可以开启Lua脚本的加密,然后结合--lua-encrypt-key选项可以设置密钥。
在打包时加密可以大大简化操作流程,正常而言每次打包都需要手动将脚本加密,然后将源码删除,只保留加密后的脚本,打包结束之后又要撤销回来,因为需要继续开发,所以在开发时需要对Lua源码进行编辑。而cocos.py则将我们从这个烦琐的流程中解放了出来,只需要在打包的时候指定一下参数就可以了。
2.3.3 自定义Lua脚本加密解密
前面介绍的两种都是用通用的方法进行加密,然后使用Cocos2d-x内置的方法进行解密,而且有一定的局限性,接下来介绍如何在Cocos2d-x中进行自定义的加密解密。在Cocos2d-x中自定义加密解密最关键的并不是使用何种方法来加密解密,而是在什么地方执行解密操作,我们需要尽量让业务逻辑层不知道解密操作的存在,以及尽量不修改引擎。对配置文件等资源,可以对加载配置操作进行一个简单的封装,在FileUtils的getData之后执行解密,再解析配置。大部分的资源都可以通过简单的封装之后,实现自动解密。
对Lua脚本,可以在LuaEngine中设置一个lua_loader回调函数来实现Lua脚本的加载规则,当Lua每次require一个脚本时,就会调用设置的lua_loader回调方法,在lua_loader回调中需要执行加载脚本以及脚本的功能,可以在加载脚本之后,执行脚本之前对加密后的脚本进行解密。Cocos2d-x默认的lua_loader回调是cocos2dx_lua_loader()函数,位于Cocos2dxLuaLoader.cpp中,可以定义一个my_lua_loader()函数,在函数中的stack->luaLoadBuffer之前实现解密的功能,把解密后的脚本内容传入,代码大致如下。
extern "C" { int cocos2dx_lua_loader(lua_State *L) { static const std::string BYTECODE_FILE_EXT = ".luac"; static const std::string NOT_BYTECODE_FILE_EXT = ".lua"; std::string filename(luaL_checkstring(L, 1)); size_t pos = filename.rfind(BYTECODE_FILE_EXT); if (pos ! = std::string::npos) { filename = filename.substr(0, pos); } else { pos = filename.rfind(NOT_BYTECODE_FILE_EXT); if (pos == filename.length() - NOT_BYTECODE_FILE_EXT.length()) { filename = filename.substr(0, pos); } } pos = filename.find_first_of("."); while (pos ! = std::string::npos) { filename.replace(pos, 1, "/"); pos = filename.find_first_of("."); } //search file in package.path unsigned char* chunk = nullptr; ssize_t chunkSize = 0; std::string chunkName; FileUtils* utils = FileUtils::getInstance(); lua_getglobal(L, "package"); lua_getfield(L, -1, "path"); std::string searchpath(lua_tostring(L, -1)); lua_pop(L, 1); size_t begin = 0; size_t next = searchpath.find_first_of("; ", 0); do { if (next == std::string::npos) next = searchpath.length(); std::string prefix = searchpath.substr(begin, next); if (prefix[0] == '.' && prefix[1] == '/') { prefix = prefix.substr(2); } pos = prefix.find("? .lua"); chunkName=prefix.substr(0, pos) +filename+BYTECODE_FILE_EXT; if (utils->isFileExist(chunkName)) { chunk = utils->getFileData(chunkName.c_str(), "rb", &chunkSize); break; } else { chunkName = prefix.substr(0, pos) + filename + NOT_BYTECODE_ FILE_EXT; if (utils->isFileExist(chunkName)) { chunk = utils->getFileData(chunkName.c_str(), "rb", &chunkSize); break; } } begin = next + 1; next = searchpath.find_first_of("; ", begin); } while (begin < (int)searchpath.length()); if (chunk) { LuaStack* stack = LuaEngine::getInstance()->getLuaStack(); //在这里添加解密的代码 my_decrypt_fun(chunk, chunkSize); stack->luaLoadBuffer(L, (char*)chunk, (int)chunkSize, chunkName.c_str()); free(chunk); } else { CCLOG("can not get file data of %s", chunkName.c_str()); return 0; } return 1; } }
需要注意的是,只有在Lua中执行require,才会回调到设置的lua-Loader函数,如果在C++中直接调用executeScriptFile是不会执行到lua-Loader回调的。
2.3.4 自定义图片加密解密
对图片资源的解密要稍微麻烦一些,由于Cocos2d-x中所有的纹理都缓存在TextureCache中,所以可以在使用纹理之前手动将纹理加载并放到TextureCache中,这样后面所有使用纹理的地方都不需要有任何改动,大部分游戏在进入场景之前都会预加载场景中的资源,将这个操作放在预加载这里是最合适的。具体的方法是先调用FileUtils的getData,获取加密后的图片,然后对内容进行解密,创建一个Image对象,将解密后的内容传入到Image的initWithImageData()方法中,最后调用TextureCache的addImage()方法将Image对象添加到TextureCache中(缺点是不能使用TextureCache的异步加载,但是可以自己编写多线程进行异步加载),代码大致如下。
bool loadEncryptTexture(const std::string& file) { auto fullPath = FileUtils::getInstance()->fullPathForFilename(file); auto data = FileUtils::getInstance()->getDataFromFile(fullPath); //使用自己的解密函数进行解密 my_decrypt_fun(data.getBytes(), data.getSize()); Image* img = new Image(); if (! img->initWithImageData(data.getBytes(), data.getSize())) { img->release(); return false; } TextureCache::getInstance()->addImage(img, fullPath); return true; }
由于所有的文件都要通过FileUtils的getDataFromFile()方法加载(笔者曾尝试了各种方法,都难以在不修改引擎源码的前提下改写getDataFromFile()方法,就算实现了也比直接修改FileUtils的源码更加难以维护),所以可以在FileUtils中添加少量代码来实现,这样就需要修改FileUtils、FileUtilsWin32以及FileUtilsAndroid的getDataFromFile()方法。
首先在FileUtils的头文件中定义一个接口类FileDelegate,接口类中提供一个文件处理函数,传入打开的文件以及文件的Data对象,可以在处理函数中对Data执行解密处理,处理完之后返回给FileUtils。
class CC_DLL FileDelegate : public Ref { public: FileDelegate() {} virtual ~FileDelegate() {} virtual Data fileProcess(const std::string& file, Data& data) = 0; };
接下来将FileDelegate设置为FileUtils的保护成员变量,并为FileUtils添加一个setFileDelegate()方法,然后在FileUtils的构造函数和析构函数中对该变量进行初始化以及释放。
//在头文件中为FileUtils添加setFileDelegate()方法 inline void setFileDelegate(FileDelegate* fileDelegate) { CC_SAFE_RELEASE_NULL(_fileDelegate); _fileDelegate = fileDelegate; CC_SAFE_RETAIN(_fileDelegate); } //在源文件中调整FileUtils的构造函数和析构函数 FileUtils::FileUtils() : _writablePath("") , _fileDelegate(nullptr) { } FileUtils::~FileUtils() { CC_SAFE_RELEASE_NULL(_fileDelegate); }
最后调整所有FileUtils的getDataFromFile()方法,添加一个简单的判断,如果_fileDelegate不为空,则将获取的文件传给_fileDelegate进行处理,代码如下。
Data FileUtils::getDataFromFile(const std::string& filename) { if (_fileDelegate) { return _fileDelegate->fileProcess(filename, getData(filename, false)); } return getData(filename, false); }
最后可以在自己的源码中,继承FileDelegate实现一个MyFileDelegate,在fileProcess()方法中实现对指定文件的解密处理,将MyFileDelegate设置到FileUtils中即可生效。我们可以使用DES、3DES、AES、XXTEA(位于引擎的external/xxtea目录下)等常用的加密算法,也可以使用自己实现的简单加密算法。自己实现加密算法可以灵活地使用异或、交换等手段,天马行空地制定规则。例如,下面这个自定义的加密算法,会将数据的前256个字节使用指定的Key进行加密,解密也是使用这个方法。
void myencrypt(char* data, unsigned int len, int key) { unsigned int maxLen = 256 / sizeof(int); len /= sizeof(int); for (unsigned int i = 0; i < len && i < maxLen; ++i) { *(int*)data ^= key; data += sizeof(int); } }
下面这段代码验证了这个简单的加密算法,随便设置了一个加密密钥,将一段文本进行加密,然后输出加密后的密文,接下来解密,并输出解密后的明文。
char str[1024]; memset(str, 0, sizeof(str)); strcpy(str, "hello world, ~~~~~~~~~~, !!!!!!!"); int key = 1314666; unsigned int len = strlen(str); myencrypt(str, len, key); CCLOG("%s", str); myencrypt(str, len, key); CCLOG("%s", str);
运行这段代码会输出以下结果:
jxl/cocp, Jqj~qj~qj, J.5! K.5! hello world, ~~~~~~~~~~, !!!!!!!
接下来演示一下如何将这个自定义的加密解密应用到Cocos2d-x中。首先需要编写一段简单的程序对要加密的文件进行加密,假设将游戏中所有的png都进行了加密,可以在MyFileDelegate中只对png文件进行解密,代码如下所示。
class MyFileDelegate : public FileDelegate { virtual Data fileProcess(const std::string& file, Data& data) { if (FileUtils::getInstance()->getFileExtension(file) == ".png") { myencrypt((char*)data.getBytes(), data.getSize(), 1314666); } return data; } };
然后调用FileUtils的setFileDelegate()方法将MyFileDelegate的对象设置进去即可。
MyFileDelegate* dlg = new MyFileDelegate(); dlg->autorelease(); FileUtils::getInstance()->setFileDelegate(dlg);