- 精通Cocos2d-x游戲開發(進階卷)
- 王永寶
- 3307字
- 2020-11-28 22:36:58
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);