Cocos2d-x 資源加密解密實踐總結

峻峰飛陽發表於2017-03-13

本文乃Siliphen原創,轉載請註明出處:http://blog.csdn.NET/stevenkylelee 


 

本文針對的是cocos2d-x 3.4 版本進行研究。


 

做加密解密的思路

 

 

加密解密演算法的簡單介紹

 

首先,加密解密應該是一個單獨的話題,一般不會涉及具體使用的引擎、框架和技術。

加密演算法有Base64,DES等。

Base64的原理類似於凱撒密碼,啥是凱撒密碼呢,就是一個字元用另一個字元來代替。

比如:a用i代替,b用k代替,以此類推。加密和解密的過程就是一個互相對映的過程。

DES是一種使用金鑰的加密演算法。

DES和Base64這種無金鑰的演算法在使用上的區別是:

DES演算法本身是可以不保密的,只要保密金鑰即可,金鑰才是解密的關鍵。

Base64如果用於加密的話,演算法本身就是金鑰。要保密演算法。

 

MD5,SHA等是摘要演算法。

Base64,DES都可以把密文還原出明文。

而MD5,SHA則不能對演算法作用後的輸出還原出原始資料。

摘要演算法一般做身份驗證。

 

我們做資源加密是要選擇能把密文還原出明文的演算法。

 

設計自己的加密解密演算法

 

這屬於密碼學的內容了,可深可淺。

可以借鑑凱撒密碼的原理自己對每個位元組的內容進行對映來加密。

也可以對1位元組,2位元組,4位元組的資料,用某個數進行異或運算來加密。

異或的特性是,用數A對內容B進行異或得到內容C,再用數A對內容C異或可以得到內容B。

這個特性可以被用來加密。同時,數A的作用就是金鑰了。

 

設計的加密演算法不同,加密演算法的使用的介面也不同。

如果是用上面的2種思路做加密演算法的話,加密演算法的實現可以“in place”操作,

就是可以直接在密文所在的記憶體進行解密,不單獨分配記憶體來儲存解密後的資料。

如果加密演算法很複雜,無法“就地還原”,就需要新申請記憶體來儲存解密資料。

當然,執行速度一般是”就地還原“快。


我覺得,如果可以的話,最好是自己設計加密演算法。

使用現成的DES之類的演算法,金鑰是關鍵。

如果是自己設計一個用金鑰加密的演算法,金鑰和演算法本身都是不公開的。

破解者要獲取資源明文要經過如下步驟:

1.在程式中找到金鑰的常量。

2.看懂反出來的彙編還原出演算法,或者想辦法利用加密演算法的彙編程式碼。

其中步驟2會加大解密者解密的難度,這是自己設計加密演算法比用現成的DES之類著名演算法的好處。


 

加密解密的運用機制

 

我見過有一些人加密是對欄位內容進行加密。

比如,他用xml儲存資料,他只對儲存的值進行加密。

內容類似這樣 <Entity Hp = "密文" AttackValue = "密文" >

這並不是一種好的運用加密的方式,因為這種方式會:

1.透露程式的配置檔案結構。XML、json等。

2.透露了程式可能使用的資料結構。看以上內容可猜想Entity實體類有2個欄位,Hp(血量),AttackValue(攻擊力)

 

更徹底的加密方式是,對整個xml、json等檔案進行加密。

這樣別人就不會知道關於你的資料的一丁點資訊。

這種做法不是直接用資料解析庫封裝的類似LoadFile的函式直接讀取檔案。

而是,先把加密檔案用檔案讀取方法讀入記憶體,在記憶體解密後,再把解密資料傳給解析庫的解析函式。

這種做法要求解析庫有對記憶體進行解析的介面。

比如,若用XML解析庫,就會棄用LoadFile,要求有Parse方法。

 

有一種情況可能無法對整個資料檔案進行加密。

就是使用sqlite等資料庫的時候,這時候只能對欄位進行加密了。

 

有些人設計加密機制會要求配置程式本身。

比如:程式需要一個配置(一個開關變數或者條件編譯)來決定是否對資源進行解密。

釋出時,把資源都加密好,開啟程式對資源的解密。

開發時,資源不必加密,遮蔽程式對資源的解密。

這種做法需要來回設定解密開關,比較繁瑣。

 

有一種實現思路更有擴充套件性、更靈活、使用更方便。

那就是自己設計檔案頭。

熟悉Win32程式設計的人可能會知道PE檔案格式。

Windows執行一個exe之前,會檢查PE檔案頭,

取得EXE檔案的資料常量、程式碼區塊等資訊後,系統才能正確執行一個EXE。

我們可以設計一個加密檔案頭,加密檔案頭有一個欄位標識該檔案是否是被加密過的。

解密演算法可以通過判斷加密頭的識別符號來決定是否進行解密、採用哪種解密演算法等。

 

這是一種資料驅動式的解密機制,解密資訊都儲存在加密資料本身中。

對於沒有被加密過的資料,程式判斷沒有檔案加密頭標識,就不執行解密。

這樣,程式可以同時讀取未加密的資料和已經加密的資料,而不需要對程式進行任何修改。

 

注意:加密頭的識別符號應該足夠特殊,保證和未加密的資料不會產生衝突。

若未加密的資料剛好出現了加密頭的內容,那麼程式就會對未加密的資料進行解密。

 

 

在Cocos2d-x 中做加密解密的研究過程

 

 

我的專案配置是用CSV檔案。解析器,用的是我自己寫的CSV解析器:

CSV檔案格式解析器的實現:從字串Split到FSM

在遊戲中這部分的解密,比較容易實現,就是對整個檔案加密,

遊戲啟動時把加密檔案讀入記憶體進行解密,然後把解密資料傳到解析器進行解析。

 

一些由遊戲產生的需要儲存的資料,我用XML進行儲存。加解密思路也如上。

 

但是,對於圖片、動畫,粒子、cocostudio1.6匯出的UI 等資料,加密就不那麼簡單了。

因為,像 Sprite::create 這樣的介面,並沒有提供一個從記憶體獲取紋理資料來初始化精靈的方式,

而是直接從磁碟檔案中讀。使用者在外部是無法干預檔案資料的讀取載入的。

 

遊戲資源的加密要怎麼做?——只有修改引擎了。- -!

我覺得,修改引擎並不是一個好的做法。

引擎應該由引擎提供商來維護,而不是靠使用者自己來動手。

使用者自己動手會造成專案維護升級困難。

自己把引擎改得亂七八糟,想升級引擎到新的版本會變成很麻煩。

就算是修改了一點,升級引擎版本一次,就要做一次修改,這樣也很麻煩。

 

要修改引擎,首先要跟蹤除錯,

以cocos2d-x 3.4 作為試驗版本,從Sprite::create入口,一步步跟下去,

看看引擎是在哪裡載入檔案資料的。

經過除錯跟蹤,Sprite::create的呼叫堆疊如下:

getData

FileUtilsWin32::getDataFromFile

Image::initWithImageFile

TextureCache::addImage

Sprite::initWithFile

Sprite::create


在windows上,精靈載入圖片,是呼叫到了

CCFileUtils-win32.cpp 這個檔案中的 

static Data getData(const std::string& filename, bool forString) 函式。

這個函式是一個非類成員靜態函式。

 

再除錯下其他的資料載入,cocostudio1.6做的動畫的呼叫堆疊是:

cocos2d::FileUtilsWin32::getFileData
cocostudio::DataReaderHelper::addDataFromFile
cocostudio::ArmatureDataManager::addArmatureFileInfo


經過了多種檔案型別載入的跟蹤除錯後,總結,

基本上檔案的載入函式有2個,都在CCFileUtils-win32.cpp檔案中,

1.FileUtilsWin32::getFileData

2.getData

 

要做解密的話,就是修改這2個函式了。

值得注意的一點是:getData函式的簽名是:

static Data getData(const std::string& filename, bool forString)

forString這個引數,用於標識讀入的檔案是不是一個文字檔案。

 

如果forString為真的話,buffer就要多分配一個位元組,用來放置C語言的字串結束符 \0 

截圖一下引擎的程式碼:

 

 


要做解密,我們的解密資料記憶體也要用forString來判斷是否多分配一個位元組的記憶體。

 

FileUtils單件類,很多函式是虛擬函式。FileUtils::getInstance,

在Windows平臺上,返回的是 FileUtilsWin32子類物件。

Android平臺上,返回FileUtilsAndroid子類物件。

Cocos2d-x 3.4 實現在Windows和Android平臺上的解密需要修改2個檔案:

Windows 平臺的:cocos\platform\win32\CCFileUtils-win32.cpp

Android 平臺的:cocos\platform\android\CCFileUtils-android.cpp

這2個檔案裡面都有 getData 和 getFileData 函式,都需要修改之。

 

檔案資料的讀取在不同平臺上有不同的實現,

如果要在目標平臺上做資源加密,就需要修改目標平臺上的讀取實現。

FileUtils是一個檔案讀取基類,提供有預設的實現,一些平臺會提供特定的實現。

通過多型,FileUtils讓我們的遊戲客戶端程式碼不需要對特定平臺做特定處理,

也不需要關心FileUtils子類的型別。


比較遺憾的是,由於時間關係,目前我沒有找到對音訊檔案解密的方法。

除錯跟蹤發現,Cocos2d-x 在Windows上播放音訊是直接呼叫了Win32 API,這些介面API是輸入一個檔案路徑來播放音訊的。

之前有提到,做加密解密需要應用資料的介面從記憶體中獲取資料。

介面直接讀磁碟檔案,就沒有辦法干預檔案的載入進行資料解密了。


我覺得引擎的設計者,可以提供一個介面,利用觀察者模式,

讓引擎使用者可以在不修改引擎本身的情況下,干預資料的讀取進行資料變換來實現解密。

畢竟自己改引擎不是一個好的做法。

 

 

加密解密支援工具

 

 

一般要做資源資料加密,大體會做2件事:

1.寫一個工具,能對磁碟檔案進行加密。這個工具,可能需要遍歷資料夾、可批量選擇檔案等功能。

2.在軟體程式中加入解密功能,對工具加密後的資料進行解密。

 

工具能方便操作、提高生產力,很重要。

 

這裡,我自己做了2個工具。

1.對cocos2d-x引擎進行修改和恢復的工具,使其具備解密功能。如下:

 

 

 

2.對磁碟檔案進行加密解密的工具,方便打包時的批量篩選操作。如下:

 


這個工具能識別出檔案是否已經被加密,原理是用了之前說的”加密檔案頭“設計。

這樣可以防止對已被加密後的檔案再次執行加密,防止對未被加密的檔案執行解密。

降低誤操作的可能性同時,也直觀地顯示檔案的加密狀態。

如果沒有”加密檔案頭“,工具程式就無法識別一個檔案到底是否被加密過。



加密解密的額外開銷



做了資源加密會降低程式的效能,原因很簡單,需要對檔案資料進行額外的處理。

稍微複雜一點的加密演算法,可能要申請記憶體空間來儲存解密資料。delete , new 都是耗時操作。


資源的加密也可能會使得apk包體體積增大。這是為什麼呢?

原因是,用Cocos2d-x 開發,資源會有很多*.plist *.ExportJson *.tmx 等檔案。

plist 可能是配置,也可能是粒子。

*.ExportJson 等檔案是 Cocostudio1.6 做出的動畫、UI 匯出檔案。

*.tmx 是Tilemap輸出的地圖格式。

以上說的這些檔案,都是用文字儲存資料的,可以用Windows記事本開啟檢視內容。


apk包實際上是一個壓縮包,壓縮演算法對於有大量冗餘資料的檔案壓縮率是很高的。

我之前寫過一篇關於壓縮演算法的文章《檔案壓縮與解壓:哈夫曼編碼有興趣的同學可以看看。

我做過一個試驗,600多KB的 UI *.plist *.ExportJson 檔案,打成apk包後,實際上只佔60多KB。

這是因為壓縮演算法能對這些配置的文字檔案有很高的壓縮率。


如果我們的加密演算法會降低冗餘資料。

比如:一個檔案的內容是“111111....",10萬個1。

我們的加密演算法為了增加安全性,會把這些內容加密成為:"sfsadfsa...."。

解密者很難從無規律的不重複的資料推出原先的”111111....“的內容,

但同時對這樣的資料,壓縮率也會很低。


我之前設計了一個使用金鑰加密的演算法,會消除冗餘資料,

對於都是同樣的一個字元的內容,可以加密成一個看似無規律的亂七八糟的內容,

其實還是有規律可言,只是無規律迴圈節很大而已。

然後,這些加密後的資料,幾乎就不能壓縮了。

加密資源之後輸出的apk包相比沒加密資源的包,體積增加不少。


對於這種情況,可以僅對圖片等關鍵資源加密,而不對非關鍵性的文字檔案加密。

或者降低演算法的加密強度,保留資料冗餘性。


相關文章