【Quick 3.3】资源脚本加密及热更新(二)资源加密

【Quick 3.3】资源脚本加密及热更新(二)资源加密

注:本文基于Quick-cocos2dx-3.3版本编写

一、介绍

在前一篇文章中介绍了代码加密,加密方式是XXTEA。对于资源文件来说,同样可以使用XXTEA来加密,因此之前那套加密模块可以通用了。

加密脚本:compile_pack_files.bat、compile_pack_files.sh
使用方法和前一篇的脚本差不多

考虑到不是不是所有的资源都需要加密,所以这里不使用这个脚本。

二、资源加密

1、加密参数文件

涉及文件目录:项目根目录

首先我们定义一个php文件,里面定义了加密所需要的参数

<?php
    ini_set('memory_limit','1024M');
    return array(
        'src'      => 'res', 
        'output'   => 'packres',
        'prefix'   => '',
        'excludes' => '',
        'pack'  => 'files',//files or zip
        'key'      => 'ilovecocos2dx',
        'sign'     => 'XXTEA',
        'whitelists' => 'jpg,png,tmx,plist',
    );
?> 
  • src 需加密的脚本所在目录
  • output 加密后文件输出目录
  • pack files、zip 分开文件加密还是打包成zip加密
  • sign 签名,用来识别代码、文件是不是加密的,一般填为"XXTEA"即可
  • key 密钥,简单来说就是加密和解密的钥匙,确保只有你自己知道
  • whitelists 需要加密的文件格式,根据文件的扩展名。

注意,whitelists这个参数是官方版本没有的

将文件保存为PackRes.php,放在项目根目录(res、src同级目录)

2、修改加密资源脚本的文件

涉及文件目录:引擎目录/quick/bin/lib、引擎目录/quick/bin/lib/quick

因为增加了参数whitelists,所以这里需要修改加密的脚本。
涉及的文件有两个,/quick/bin/lib/pack_files.php和/quick/bin/lib/quick/FilesPacker.php

  • pack_files.php文件

可以看到array里面定义了所有参数,往array的最后加上新增的参数whitelists

//pack_files.php
<?php
	//...

$options = array(
    //...
    //...
    array('c',   'config',     1,      null,        'load options from config file'),
    array('q',   'quiet',      0,      false,       'quiet'),
    array('w',   'whitelists',   1,      null,        'whitelists extension, use "," to splite array, example "jpg,png"'),
);

	//...
  • FilesPacker.php文件

定位到validateConfig方法,加入对whitelist的解析

//FilesPacker.php
<?php

//...

class FilesPacker
{
	//...

    function validateConfig()
    {

    	//...

        if (!empty($this->config['excludes']))
        {
            $excludes = explode(',', $this->config['excludes']);
            array_walk($excludes, function($value) {
                return trim($value);
            });
            $this->config['excludes'] = array_filter($excludes, function($value) {
                return !empty($value);
            });
        }
        else
        {
            $this->config['excludes'] = array();
        }

        //--------------add code begin--------------

        if (!empty($this->config['whitelists']))
        {
            $whitelists = explode(',', $this->config['whitelists']);
            array_walk($whitelists, function($value) {
                return trim($value);
            });
            $this->config['whitelists'] = array_filter($whitelists, function($value) {
                return !empty($value);
            });
        }

        //----------add code end--------------

        if ($this->config['pack'] != self::COMPILE_ZIP
            && $this->config['pack'] != self::COMPILE_FILES
            && $this->config['pack'] != self::COMPILE_C
            )
        {
            printf("ERR: invalid pack mode %s\n", $this->config['pack']);
            return false;
        }

定位到prepareForPack方法,加入whitelist的文件过滤

    protected function prepareForPack(array $files)
    {

    	//...

        foreach ($this->config['excludes'] as $key => $exclude)
        {
            if (substr($moduleName, 0, strlen($exclude)) == $exclude)
            {
                unset($files[$key]);
                $skip = true;
                break;
            }
        }

        if ($skip) continue;

        //--------------add code begin--------------

        if (!empty($this->config['whitelists'])) {
            $isDirty = false;
            foreach ($this->config['whitelists'] as $key => $whitelist)
            {

                if (end(explode('SPLIT_CHAR', $moduleName)) == $whitelist)
                {
                    $isDirty = true;
                    break;
                }
            }

            if (!$isDirty) {
                unset($files[$key]);
                continue;
            }
        }

        //--------------add code end--------------

        $bytesName = 'lua_m_' . strtolower(str_replace(array('.', '-'), '_', $moduleName));

        //...

3、生成加密资源脚本

还是上个教程的脚本,这次加了packRes方法

#coding=utf-8
#!/usr/bin/python
import os 
import os.path 
import sys, getopt  
import subprocess
import shutil 
import time,  datetime
import platform
from hashlib import md5
import hashlib  
import binascii

def removeDir(dirName):
    if not os.path.isdir(dirName): 
        return
    filelist=[]
    filelist=os.listdir(dirName)
    for f in filelist:
        filepath = os.path.join( dirName, f )
        if os.path.isfile(filepath):
            os.remove(filepath)
        elif os.path.isdir(filepath):
            shutil.rmtree(filepath,True)

def initEnvironment():
    global APP_ROOT #工程根目录 
    global APP_ANDROID_ROOT #安卓根目录
    global QUICK_ROOT #引擎根目录
    global QUICK_BIN_DIR #引擎bin目录
    global APP_RESOURCE_ROOT #生成app的资源目录
    global APP_RESOURCE_RES_DIR #资源目录

    global APP_BUILD_USE_JIT #是否使用jit

    global PHP_NAME #php
    global SCRIPT_NAME #执行脚本文件名

    global BUILD_PLATFORM #生成app对应的平台

    SYSTEM_TYPE = platform.system() #当前操作系统

    APP_ROOT = os.getcwd() #当前目录
    APP_ANDROID_ROOT = APP_ROOT + "/frameworks/runtime-src/proj.android"
    QUICK_ROOT = os.getenv('QUICK_V3_ROOT')

    if QUICK_ROOT == None: #quick引擎目录未指定,可手动指定路径或者运行引擎目录下相应脚本
        print "QUICK_V3_ROOT not set, please run setup_win.bat/setup_mac.sh in engine root or set QUICK_ROOT path"
        return False

    if(SYSTEM_TYPE =="Windows"): 
        QUICK_BIN_DIR = QUICK_ROOT + "quick/bin"
        PHP_NAME = QUICK_BIN_DIR + "/win32/php.exe" #windows
        BUILD_PLATFORM = "android" #windows dafault build android
        SCRIPT_NAME = "/compile_scripts.bat"
    else:
        PHP_NAME = "php"
        BUILD_PLATFORM = "ios" #mac default build ios
        QUICK_BIN_DIR = QUICK_ROOT + "/quick/bin" #mac add '/'
        SCRIPT_NAME = "/compile_scripts.sh"


    if(BUILD_PLATFORM =="ios"):
        APP_BUILD_USE_JIT = False
        APP_RESOURCE_ROOT = APP_ROOT + "/Resources" 
        APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"

    else:
        APP_BUILD_USE_JIT = True
        APP_RESOURCE_ROOT = APP_ANDROID_ROOT + "/assets" #default build android
        APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"

    print 'App root: %s' %(APP_ROOT)
    print 'App resource root: %s' %(APP_RESOURCE_ROOT)
    return True

def compileScriptFile(compileFileName, srcName, compileMode):
    scriptDir = APP_RESOURCE_RES_DIR + "/code/"
    if not os.path.exists(scriptDir):
        os.makedirs(scriptDir)
    try:
        scriptsName = QUICK_BIN_DIR + SCRIPT_NAME
        srcName = APP_ROOT + "/" + srcName
        outputName = scriptDir + compileFileName
        args = [scriptsName,'-i',srcName,'-o',outputName,'-e',compileMode,'-es','XXTEA','-ek','ilovecocos2dx']

        if APP_BUILD_USE_JIT:
            args.append('-jit')

        proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
        while proc.poll() == None:  
            outputStr = proc.stdout.readline()
            print outputStr,
        print proc.stdout.read(),
    except Exception,e:  
        print Exception,":",e

def packRes():
    removeDir(APP_ROOT + "/packres/") #--->删除旧加密资源

    scriptName = QUICK_BIN_DIR + "/lib/pack_files.php"
    try:
        args = [PHP_NAME, scriptName, '-c', 'PackRes.php']
        proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
        while proc.poll() == None:  
            print proc.stdout.readline(),
        print proc.stdout.read()
    except Exception,e:  
        print Exception,":",e

if __name__ == '__main__': 
    isInit = initEnvironment()

    if isInit == True:
        #加密资源
        packRes()

        #将src目录下的所有脚本加密打包成game.zip
        compileScriptFile("game.zip", "src", "xxtea_zip")

使用方法:

  1. 保存为compileScript.py文件
  2. 放到项目根目录(res、src同级目录)
  3. 运行命令行工具,cd到该目录,执行python compileScript.py
  4. 若屏幕输出 "create output files in xx/xx/packres ." 说明执行成功

4、修改引擎文件

由于涉及到的资源格式比较多,每个涉及到的类修改读取文件的方式太麻烦。而现在cocos2dx3.x版本统一了资源读取的接口,所以只需要修改读取文件的接口即可。

具体步骤:

  • 加入解密XXTEA文件
    1、把frameworks/cocos2d-x/external/xxtea文件夹里面的xxtea.h和xxtea.cpp复制到frameworks/cocos2d-x/cocos/base目录中

2、引用文件,也就是让编译器知道xxtea文件在哪

  • windows

打开vs,打开lubcocos2d项目,展开base,把xxtea两个文件拖到base里面即可

  • android

打开frameworks/cocos2d-x/cocos/Android.mk文件
在LOCAL_SRC_FILES中加入xxtea.cpp文件

#Android.mk

#...

LOCAL_SRC_FILES := \
cocos2d.cpp \

#...

base/ObjectFactory.cpp \
base/xxtea.cpp \ #加入此项
renderer/CCBatchCommand.cpp \

#...
  • mac

打开xcode,打开cocos2dlib.xcodeproj,展开base,把xxtea两个文件拖到base里面即可

  • 文件读取 CCFileUtils

因为避免和引擎旧的接口混淆,所以这里是新增两个方法而不是修改旧的方法

头文件CCFileUtils.h加入两个public方法声明

//CCFileUtils.h

//...
class CC_DLL FileUtils
{
public:

	//...
    static unsigned char* getDecryptFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize);

    static Data getDecryptDataFromFile(const std::string& filename);

protected:
	//...

Cpp文件CCFileUtils.cpp中加入头文件和方法定义

//CCFileUtils.cpp

//...
#include "xxtea/xxtea.h" 

//..
Data FileUtils::getDecryptDataFromFile(const std::string &filename)
{
    unsigned long sz;
    unsigned char * buf = FileUtils::getDecryptFileData(filename.c_str(), "rb", &sz);
    if (!buf) {
        return Data::Null;
    }
    Data data;
    data.fastSet(buf, sz);
    return data;
}

unsigned char* FileUtils::getDecryptFileData(const char* pszFileName, const char* pszMode, unsigned long * pSize)
{
    ssize_t size;
    unsigned char* buf = FileUtils::getInstance()->getFileData(pszFileName, pszMode, &size);
    if (NULL == buf || size<1) return NULL;

    const char *xxteaKey = "ilovecocos2dx";
    int xxteaKeyLen = strlen(xxteaKey);
    const char *xxteaSign = "XXTEA";
    int xxteaSignLen = strlen(xxteaSign);
    unsigned char* buffer = NULL;

    bool isXXTEA = true;
    for (int i = 0; isXXTEA && i<xxteaSignLen && i<size; ++i) {
        isXXTEA = buf[i] == xxteaSign[i];
    }

    if (isXXTEA) { // decrypt XXTEA
        xxtea_long len = 0;
        buffer = xxtea_decrypt(
            buf + xxteaSignLen,
            (xxtea_long)size - (xxtea_long)xxteaSignLen,
            (unsigned char*)xxteaKey,
            (xxtea_long)xxteaKeyLen,
            &len);
        delete[]buf;
        buf = NULL;
        size = len;
    }
    else {
        buffer = buf;
    }

    if (pSize) *pSize = size;
    return buffer;
}
  • 资源读取
    涉及加密的资源有图片文件(png、jpg),xml文件(plist、tmx)

图片文件

//CCImage.cpp
//目录:frameworks/cocos2d-x/cocos/platform/

//...
bool Image::initWithImageFile(const std::string& path)
{
	//...
	    SDL_FreeSurface(iSurf);
	#else
		//修改此处即可
	    Data data = FileUtils::getInstance()->getDecryptDataFromFile(_filePath);

	    if (!data.isNull())
	    {

	//...
}

xml文件

在windows/android平台下,xml解析使用CCSAXParser,在ios平台下则是用系统类NSDictionary来解析,所以这里要修改两处地方

对于windows/android平台

//CCSAXParser.cpp
//目录:frameworks/cocos2d-x/cocos/platform/

//...
bool SAXParser::parse(const std::string& filename)
{
    bool ret = false;
    //修改此处即可
    Data data = FileUtils::getInstance()->getDecryptDataFromFile(filename);
    if (!data.isNull())
    {
        ret = parse((const char*)data.getBytes(), data.getSize());
    }

    return ret;
}
//...

对于ios平台

//CCFileUtils-apple.mm
//frameworks/cocos2d-x/cocos/platform/apple

//...

ValueMap FileUtilsApple::getValueMapFromFile(const std::string& filename)
{
    std::string fullPath = fullPathForFilename(filename);

    //注释下面两句代码
//    NSString* path = [NSString stringWithUTF8String:fullPath.c_str()];
//    NSDictionary* dict = [NSDictionary dictionaryWithContentsOfFile:path];
    
    //------add code begin-------
    unsigned long fileSize = 0;
    unsigned char* pFileData = FileUtils::getDecryptFileData(fullPath.c_str(), "rb", &fileSize);
    NSData *data = [[[NSData alloc] initWithBytes:pFileData length:fileSize] autorelease];
    delete []pFileData;
    NSPropertyListFormat format;
    NSString *error;
    NSMutableDictionary *dict = (NSMutableDictionary *)[
                                                         NSPropertyListSerialization propertyListFromData:data
                                                         mutabilityOption:NSPropertyListMutableContainersAndLeaves
                                                         format:&format
                                                         errorDescription:&error];

    //------add code end-----------

    ValueMap ret;

    if (dict != nil)
    {
        for (id key in [dict allKeys])
        {
            id value = [dict objectForKey:key];
            addValueToDict(key, value, ret);
        }
    }
    return ret;
}

三、总结

至此资源加密已经完成,总体来说分两个步骤,一个是资源加密生成,另一个是资源加密读取。由于涉及跨平台文件读取,所以修改后需要在各个平台测试资源是否能正确读取

资源文件见:https://github.com/chenquanjun/Cocos2dxResourceEncrypt

posted @ 2015-10-30 12:28  CreeperChange  阅读(1138)  评论(0编辑  收藏  举报