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);