ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cocos Creator 资源加密工具实战:AES-CBC 加密与 native 解密全解析

Cocos Creator 资源加密工具实战:AES-CBC 加密与 native 解密全解析 简介面向 Cocos Creator 开发者的资源加密工具定位是在发布前对项目资源做一键批量加密帮助保护美术、配置和脚本内容适合中小游戏团队或个人开发者。资源包共 383 个文件约 10.03MB核心由 TypeScript、JavaScript 脚本与 JSON 配置组成负责加密逻辑和参数设置另有 lcl 加密产物、cmd/ps1 命令脚本、md 文档与 license 授权文件覆盖从调用、执行到查看结果的完整链路。使用时直接运行工具自带脚本即可完成加密并生成 lcl 等格式的产物。目前已有 193 人学习下载。工具内置 ts-node、tsc、tsserver 等运行与编译脚本可在命令行中快速完成加密流程也便于接入现有构建打包流程压缩包内文件划分清晰既可直接套用也可按脚本逻辑改造为定制化方案。对不想从零编写加密逻辑的开发者来说这是一套低门槛、可落地的资源保护工具。1. Cocos Creator 资源加密先搞清楚你的包为什么一扒就裸做 Cocos Creator 开发的大概都经历过这种场面自己熬夜调了半个月的 Spine 动作、UI 切图、音效素材发出去不到一周就在某个游戏交流群里看到了自己的美术资源被原封不动地拆出来连目录结构都没改。原因很简单——Cocos Creator 构建出的 APK/IPA 里的 assets 目录就是明文存放图片、音频、Prefab、JSON 配置全裸着躺在那里。拿个解包工具把包一解把 assets 拖出来你项目的底裤就没了。这套《cocoscreator资源加密工具_creator》解决的就是这个痛点在构建完成后对资源做加密同时在引擎 native 层接入解密逻辑让打进包里的资源不再是一抓一大把的明文。整套方案面向的是一线 Cocos Creator 从业者尤其是做中重度小游戏、有热更新、担心美术素材和关卡配置被白嫖的团队。它不解决代码保护的问题——那是另一套混淆体系的活——但能把资源这道门先焊死。下面我把这套工具的选型逻辑、落地步骤和踩过的坑全部拆开来讲。2. 加密方案选型为什么是 AES-CBC 而不是改后缀名资源加密这件事网上能搜到一堆「方案」真正常用的其实就三档改后缀、压缩混淆、真正的对称加密。选型之前先把这三档的区别弄清楚你就知道为什么直接改后缀名属于自欺欺人。2.1 资源裸奔的三个层次以及这把工具的加密边界第一档是改后缀名。比如把.png改成.bin把.json改成.dat这能防住完全不懂行的普通人但防不住任何会用解包工具的人。Cocos Creator 的资源加载并不完全依赖扩展名引擎内部对很多类型有按内容识别或者固定路径约定你改后缀之后加载逻辑也要跟着改而反编译者直接看文件头就能认出真实格式改后缀等于白改。第二档是整体打包加压缩比如把所有资源塞进一个自定义格式的包再对包做 zip 压缩。这一档的防护力度比改后缀强一些因为至少需要正向逆向对得上格式才能解开。但 zip 本身是公开格式反编译者把包解出来之后资源依然是明文该被扒还是被扒。第三档才是真正的加密对资源文件内容做对称加密密钥内置在客户端里运行时解密内存里加载。这一档能挡住绝大多数「解包党」因为拿到的文件头是被替换过的直接打开是乱码。这把工具走的就是第三档核心思路用一句话描述构建后把所有需要保护的非脚本资源用 AES 加密native 层重写 FileUtils 的读取接口加载时先解密再交给引擎。加密边界上要注意.js和.jpg/.png的处理方式不一样。.js文件不能做常规加密——引擎要直接执行它加密了你就得在运行时用eval解出来执行这既影响性能又容易被反调试抓到。所以这套方案里.js走代码混淆那条路不参与资源加密真正加密的是图片、音频、Prefab、JSON、Spine 的.json/.skel这些数据类资源。2.2 AES-CBC 与密钥管理为什么密钥要埋在 native 层确定要加密之后加密算法反而没什么好纠结的——工程上清一色选 AES。AES 是硬件级支持的算法主流手机 CPU 都有 AES 指令集加解密速度快安全性够用。剩下的问题是用 AES 的哪种模式密钥放哪。先看模式。AES 有 ECB、CBC、GCM 等模式其中 ECB 模式不需要 IV同样的明文永远得到同样的密文这在密码学上是大忌反编译者通过对比同图多次加密结果就能推断规律。CBC 模式引入了 IV初始向量同样的明文配合不同 IV 得到不同密文安全性高一个档次。GCM 模式带认证标签能防篡改但工程复杂度更高密钥和 IV 的管理也更麻烦。对于资源加密这个场景——不是网络传输不需要防中间人只需要防静态解包——CBC 模式是性价比最高的选择。密钥长度用 128 位就够了256 位不会带来实质性的安全提升反而在个别老设备上会有性能损耗。填充方式用 PKCS7这是 OpenSSL 和大多数密码库默认的避免自己在填充逻辑上出问题。再看密钥放哪。这是整个加密方案里最核心的决策。如果你把密钥写在 JS 层比如写在某个Settings.js里那跟没加密没区别——反编译者搜一下 AES 算法的特征字符串比如aes-128-cbc、CryptoJS两分钟就能把你的密钥翻出来。所以密钥必须埋到 native 层在 Android 上写进 C 代码编进.so在 iOS 上写进.mm编进二进制。反编译者想拿密钥就要 IDA 去逆向你整个 so成本完全不同。密钥放置还有一个工程细节值得多说一句不要把整个密钥作为一个完整字符串写死在代码里。常见的做法是拆成两段或者做一层异或变换比如把密钥拆成三段分布在不同的文件里运行时拼接。这不能从根本上防住铁了心逆向你的人——他终究能找到——但能把自动挖掘密钥的成本抬到一个普通扒资源的人不愿意付出的高度。对攻防来说让对手觉得「不划算」就已经赢了。3. 落地实操构建钩子加密脚本与 native 解密改造选型定了剩下的就是动手。整套落地分两块构建侧把资源加密运行时侧把资源解密。我按 Cocos Creator 3.x 的工程结构来讲2.x 的差异会单独标注。3.1 构建后加密脚本hook 住构建流程一键批量加密第一步是先写一个构建插件来 hook 构建流程。Cocos Creator 的构建插件扩展包机制允许你在构建完成后触发自定义脚本这个扩展点叫after-build。加密脚本挂在这个时机遍历构建输出目录里的assets文件夹对每个需要加密的文件做 AES-CBC 加密并把加密结果写回原路径或者写到另一个目录。先看加密脚本的核心逻辑我这里用 Node.js 写因为 Cocos Creator 的构建插件本身就是跑在 Node 环境里的// encrypt-assets.js const crypto require(crypto); const fs require(fs); const path require(path); // 与 native 层约定好的密钥128位即16字节 const AES_KEY Buffer.from(c0c0sCreAt0rKey!, utf8); // 实际项目建议拆分布置 const AES_IV Buffer.from(0123456789abcdef, utf8); // 固定IV16字节 // 需要加密的资源扩展名js不在此列 const EXT_WHITELIST new Set([ .png, .jpg, .webp, // 纹理 .mp3, .wav, .ogg, // 音频 .json, .plist, // 配置与图集 .skel, .atlas, // Spine .prefab, .anim, // 场景与动画 .bundle, // 子包 ]); function encryptFile(filePath) { const raw fs.readFileSync(filePath); const cipher crypto.createCipheriv(aes-128-cbc, AES_KEY, AES_IV); const encrypted Buffer.concat([cipher.update(raw), cipher.final()]); // 写入自定义后缀 .enc并把原文件删除 fs.writeFileSync(filePath .enc, encrypted); fs.unlinkSync(filePath); } function walkDir(dir) { const entries fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath path.join(dir, entry.name); if (entry.isDirectory()) { walkDir(fullPath); } else { const ext path.extname(entry.name).toLowerCase(); if (EXT_WHITELIST.has(ext)) { encryptFile(fullPath); console.log([encrypt] ${fullPath}); } } } } // 入口遍历构建产物的 assets 目录 walkDir(path.join(__dirname, build, assets));这段代码的逻辑不复杂但有三个参数值得展开说。第一是EXT_WHITELIST扩展名白名单。这套加密方案故意不做「全量加密」因为不该加密的硬塞进去只会给自己找麻烦。.js不加密、.cconbCocos Creator 3.x 的序列化二进制格式按需决定、.md/.txt之类不涉及商业价值的也不加密。这个白名单你完全可以按项目裁剪但原则是美术资产、关卡数值、音频视频这些「被扒了会肉疼」的东西必须进名单。第二是AES_KEY和AES_IV的写法。示例代码里我图省事写成了明文常量实际工程里强烈建议把密钥拆成三段分布到构建脚本的不同位置或者从环境变量注入避免密钥直接出现在构建脚本的源码里。IV 用固定值可以省去「把 IV 传给 native 层」的通信成本虽然密码学上固定 IV 有风险但在这个场景下威胁模型就是静态解包固定 IV 是可接受的权衡。第三是加密后的产物处理。示例里是直接在同目录生成.enc文件并删掉原文件。正常来说构建产物里同一份资源可能有多个引用路径删掉原文件后需要确保引擎侧解密完能正确找到对应文件。所以实际工程中我更推荐的做法是加密后的文件保留原文件名和后缀只修改文件内容这样引擎侧解包时路径感知完全不变。但这样做有个副作用——反编译者在你包里看到avatar.png会以为资源没加密等他打开发现是乱码反而多了一层迷惑性。这个取舍看你对「加密后资源命名是否可读」的要求。脚本写完要在package.json里注册构建插件{ name: asset-encrypt-plugin, version: 1.0.0, creator: { build: { hooks: { after-build: ./scripts/encrypt-assets.js } } } }注册完之后把这个插件目录放进项目的extensions文件夹重新打开 Cocos Creator构建项目时就会自动执行加密脚本。验证的快捷方式构建完成后随便打开一个构建目录下的.png文件如果内容是乱码说明加密已经生效如果还能看到图片说明钩子没触发优先检查插件路径和 hooks 配置。3.2 native 解密层重写 FileUtils让解密发生在加载线程资源加密只是一半另一半是运行时能正确加载。Cocos Creator 引擎读取资源时Android 和 iOS 底层都走同一个抽象类——cc::FileUtils。这个类负责把物理文件的内容读进内存引擎上层拿到的就是字节流。所以解密嵌入的正确位置就是重写FileUtils::getData方法在文件内容返回给引擎之前先做一次 AES 解密。Android 侧的本地代码用 C 写核心实现长这样// CocosFileUtils.cpp #include cocos/platform/android/CCFileUtils-android.h #include openssl/aes.h #include cstring static const unsigned char AES_KEY[16] { 0x63, 0x30, 0x63, 0x30, 0x73, 0x43, 0x72, 0x65, 0x61, 0x74, 0x30, 0x72, 0x4b, 0x65, 0x79, 0x21 }; static const unsigned char AES_IV[16] { 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66 }; class EncryptedFileUtils : public cc::FileUtilsAndroid { public: virtual cc::Data getData(const std::string filename, bool forString) override { // 先按原逻辑拿原始文件数据 cc::Data data cc::FileUtilsAndroid::getData(filename, forString); if (data.isNull()) return data; // 判断是否需要解密扩展名在白名单内才走解密 if (shouldDecrypt(filename)) { data aesDecrypt(data); } return data; } private: bool shouldDecrypt(const std::string filename) { // 按扩展名过滤与构建侧的 EXT_WHITELIST 保持一致 static const std::vectorstd::string whitelist { .png, .jpg, .webp, .mp3, .wav, .json, .plist, .skel, .atlas, .prefab }; for (const auto ext : whitelist) { if (filename.size() ext.size() filename.compare(filename.size() - ext.size(), ext.size(), ext) 0) { return true; } } return false; } cc::Data aesDecrypt(const cc::Data data) { const unsigned char* ciphertext data.getBytes(); int len data.getSize(); // CBC 模式要求密文长度是 16 的倍数这里做防御性检查 if (len 0 || len % 16 ! 0) { return data; // 非加密文件返回原始内容 } unsigned char* plaintext new unsigned char[len]; AES_KEY aesKey; AES_set_decrypt_key(AES_KEY, 128, aesKey); // CBC 每次解密一块IV 只用于第一块 AES_cbc_encrypt(ciphertext, plaintext, len, aesKey, AES_IV, AES_DECRYPT); // 去掉 PKCS7 填充 int padLen plaintext[len - 1]; int realLen len - padLen; cc::Data result; result.copy(plaintext, realLen); delete[] plaintext; return result; } };这段代码有四个关键点要展开解释。第一个是getData的调用时机。Cocos Creator 里所有资源文件加载最终都会汇聚到 FileUtils 的getData包括贴图上传、音频解码、JSON 解析。在这里拦截是最全面的不需要逐个资源类型去打补丁。但是要注意性能——getData是在加载线程同步执行的贴图和音频这种大文件解密会阻塞线程所以后面第 4 章的避坑部分会专门讲缓存优化。第二个是shouldDecrypt的白名单必须和构建侧的EXT_WHITELIST完全对齐。两边列表不一致是加密方案里最常见的翻车原因——构建侧加密了.prefabnative 侧忘记加上这个扩展名运行时就解密不回来报一堆解析错误。我一般会把这份白名单单独抽成一个公共配置构建脚本读一份、native 代码读一份但两边同步还是靠人肉保证所以每次改白名单之前先全局搜一遍两个文件。第三个是 AES-CBC 的 IV 复用问题。代码里我用了固定 IV这在密码学上不是最优的但在这个场景下是可接受的——因为我们不是防密码分析而是防直接解包。真要说风险就是反编译者拿到了密钥后能通过密钥 固定 IV 批量解密所有资源。但在对方连 so 都已经逆向到这种程度的前提下他已经不是普通扒资源的玩家了那是专业安全团队做对抗是另一套游戏规则。第四个是 PKCS7 填充的还原。CBC 模式要求明文长度也是 16 字节的倍数所以加密前会做 padding解密后要把 padding 去掉。代码里我取了最后一个字节作为 padding 长度这要求你的加密侧必须严格使用 PKCS7——如果构建脚本用的 OpenSSL 默认就是 PKCS7那没问题如果你自己手写了填充逻辑必须保证填充值是「填充字节数」本身否则plaintext[len - 1]取到的就不是 padding 长度。iOS 侧的思路完全一样只是文件后缀从.cpp变成.mm且 OpenSSL 换成 CommonCrypto系统自带框架。核心差异在于 FileUtils 的类名是cc::FileUtilsIOS方法签名相同。需要注意 iOS 的 App Store 审核对私有 API 敏感CommonCrypto 是公开框架直接用没问题。到这里为止你的 Android 包和 iOS 包都已经能加密加载了。下一步解决热更新场景——如果你只在本地构建时加密热更服务器上拉下来的新资源还是明文等于只堵住了一扇门。3.3 热更新资源怎么处理加密逻辑要同时覆盖本地包和远程包有热更新的项目加密工作要覆盖两条路径打进 APK 的本地资源以及从热更服务器拉下来的远程资源。远程资源的加密方式和本地资源完全相同只是触发时机不同。Cocos Creator 3.x 热更新通常走 Asset Bundle 机制把需要热更的模块打成.bundle后缀的子包。这个 bundle 本身就是一个资源容器里面可能包含图片、JSON、Prefab 等。热更流程通常这样设计第一步在构建热更包时对 bundle 目录单独执行加密脚本。实际做法是在 CI 流程里分两步——先打 bundle再跑加密最后把加密后的 bundle 上传到资源服务器。第二步客户端热更下载完成后把 bundle 文件写入本地存储目录比如沙盒的cache目录。这一步不需要解密因为读取解密发生在getData里native 层按扩展名识别.bundle解密后内部资源再交给引擎。第三步版本切换时引擎加载 bundle 的路径要指向沙盒目录而不是内置资源路径。这里有个常见坑Cocos Creator 的resources会和内置 asset bundle 冲突需要手动配置remote bundle的地址和优先级。下面这个表是热更场景下加密工具的参数对照帮你理解不同路径的加载和解密行为场景资源路径是否加密解密层备注首次安装APK 内assets/是native FileUtils构建钩子自动加密热更下载沙盒cache/是native FileUtils上传前必须跑加密调试模式本地文件系统否不参与dev模式跳过钩子远程 Bundleremote/配置表是native FileUtils注意版本号对齐这个配置表是我自己项目里的真实落法核心原则是所有发给用户的资源无论从哪个路径加载都必须经过 native 解密。调试模式下为了开发效率可以跳过加密但发布构建和热更产线必须强制加密。从工程管理角度我会在 CI 里分别给「本地构建」和「热更产线」设置两个开关——BUILD_ENVrelease时加密钩子强制打开设为debug时跳过。用环境变量去驱动加密开关比每次手动改代码要靠谱得多。4. 避坑指南加密后加载失败、黑屏、性能劣化的五个真实排查加密工具最坑的地方不在于写不出来而在于写完之后运行时一片混乱你根本不知道是解密没生效、加载路径错了、还是性能崩了。以下五条全部是我在这套方案落地过程中真实踩过的坑按「现象 → 原因 → 解决」的格式记录照方抓药即可。4.1 场景一图集加载黑屏、Spine 骨骼错乱但单个图片能正常加载现象把Spine动画资源加密后运行游戏时骨骼动画的挂点错乱图集显示为黑块但是单个.png贴图却能正常显示。原因Spine 的图集文件.atlas内部记录的是图片的原始文件名和尺寸运行时 Spine 渲染器会按照图集描述去加载对应的贴图纹理。如果加密后你把资源文件名改了比如char_01.png变成了char_01.png.enc图集描述里写的还是char_01.png引擎拿着这个名字去找文件找到的是解密后的字节流但名字对不上加载就失败了。解决加密时保留原文件名和后缀只加密内容。回到上文 3.1 节那个取舍问题——如果当初选择了「改名 生成.enc文件」的路线Spine 这类内部有资源引用关系的模块就会连环爆炸。我在项目里后来强制改为「同名覆盖内容」模式这类问题直接消失。4.2 场景二所有加密资源首次加载卡顿UI 切换掉帧严重现象游戏冷启动后第一次打开主界面界面加载要等 23 秒跨场景切换时短暂黑屏掉帧明显。但第二次进入同样的界面就很快。原因这是典型的同步解密阻塞加载线程。Cocos Creator 的资源加载是单线程串行机制getData里解密大文件比如一个 2MB 的.png耗时约 2050ms如果一帧里同时加载几十个资源主线程直接卡死。第二次进入快是因为引擎有自己的缓存资源已经驻留在内存里不用重新解密了。解决把解密从同步改成异步缓存。我采用的方案是在 native 层维护一个 LRU 内存缓存——解密后的结果缓存起来下次getData时直接返回缓存字节流。首次加载的卡顿依然存在但不会重复发生。更进一步的做法是启动时专门开一个低优先级线程预解密当前场景要用的大资源。预判逻辑用场景名 bundle 路径作为 key在场景切换动画期间把资源提前解开。4.3 场景三iOS 上 JSON 配置文件读取为 nilAndroid 却完全正常现象同一套加密逻辑Android 端跑得好好的iOS 端resources目录下的 JSON 配置读出来是 null日志里报 Parse Error。原因iOS 的 FileUtils 里getData方法对字符串类型文件有一个特殊分支——它不走Data返回而是直接返回std::string。问题出在我当初重写的时候只重写了getData没有重写getStringFromFile方法。JSON 配置在 Cocos Creator 内部读的是字符串接口绕过了解密逻辑拿到的是密文解析自然失败。Android 的 FileUtils 实现里字符串和 Data 走的是同一个底层接口所以没暴露这个问题。解决在 iOS 的EncryptedFileUtils里同步重写getStringFromFile方法把getData的解密结果再转成字符串返回。另外注意.plist文件——iOS 上有些引擎路径读 plist 走的是NSDictionary的快捷方式不走 FileUtils这种情况要么绕开引擎的快捷读取要么把 plist 内容改成走自定义解密接口。4.4 场景四构建钩子没触发release 包里的资源还是明文现象配置好after-build钩子后开发模式构建一切正常但 CI 上的 release 构建产出的包资源没有加密解包后图片直接能看。原因Cocos Creator 的构建扩展机制在 debug 和 release 两种构建模式下会走不同的构建流程某些版本下after-build钩子在 release 模式不被识别或者插件没有被正确加载。另一个常见原因是 CI 环境里用的构建命令是命令行模式--build命令行模式对扩展的加载方式和编辑器 GUI 模式不完全一致。解决不要依赖编辑器 GUI 的构建扩展机制在 CI 脚本里显式调用加密脚本。稳妥的做法是在构建命令后面直接追加一行node encrypt-assets.js把加密步骤从「钩子」降级为「命令行步骤」这样不管构建模式怎么变只要 CI 脚本执行到那里就一定会加密。我在项目里最终采用的就是这种双保险GUI 钩子留着方便本地开发CI 里强制走命令行加密。4.5 场景五密钥被反编译者直接从 so 里捞出现象上线一个月后在某个资源泄露渠道看到自己项目的加密素材被完整解开了文件结构和原工程一模一样。原因密钥以完整字符串明文写在 C 代码里反编译者用 IDA Pro 加载 so搜字符串交叉引用两步到位。什么固定 IV、白名单策略都防不住这一步——密钥本身被找出来一切加密都是纸糊的。解决三层联动。第一层密钥拆分 变换存储比如把密钥拆成三段每段做一次异或运行时拼装。第二层把密钥的读取逻辑和引擎的初始化绑定不提供独立的获取函数——让反编译者无法快速定位。第三层对 native 代码做 OLLVM 混淆提高 IDA 静态分析的难度。要做到这样才算构建了基本的防线。一个实际执行的技巧在代码里故意埋入多个假密钥和假解密函数让逆向者分不清哪条路径是真的干扰成本显著增加。5. 验证与性能优化解密耗时、缓存策略与最终检查清单加密工具部署完下一步不是庆祝而是验证。我给团队定的规矩是每次发版前强制完成三步验证缺一步就驳回。第一步是解包验证。用解包工具把 APK 解开手动进assets目录抽查至少 20 个文件确认所有白名单扩展名的文件内容都是乱码。这一步快速、暴力、有效能直接拦截「钩子没生效」这种最低级的错误。第二步是运行时日志验证。在 native 解密层加一行日志输出[decrypt] filename跑完整个核心流程确认每个关键场景进出战斗、打开商城、切换角色的资源都实际走了解密路径。第三步是性能验证。用 Xcode Instruments 和 Android Profiler 抓加载时间对照加密前的基线数据确认解密增加的时间在可接受范围内——我个人的标准是单场景加载总耗时增加不超过 15%。性能优化方面如果验证下来发现解密耗时超了优先级最高的手段是缓存和预解密而不是换算法。后面是实际操作中的优化方向手段适用场景效果native LRU 缓存同一资源多次加载二次加载耗时归零启动预解密首场景大图大量加载首场景进入提速 30%50%小文件不加密小于 4KB 的 JSON减少无意义解密开销多线程解密图集 Spine 批量加载利用多核 CPU 并行处理这里面的一个细节你可能已经注意到了小文件不加密。如果一个 JSON 只有 2KB它的文件头泄露对整个项目不会造成实质伤害但每次加载都走一轮 AES 的开销反而拖慢节奏。所以实际部署中我会把加密白名单再细化一层——按文件大小二次过滤大于阈值的才加密。这样既降低了解密总开销又保持了对核心资产的保护力度。最后是这个资源工具使用上的一个小习惯也说说我自己的教训。做这套方案的第一个项目我为了省事把密钥写在 JS 层上线两周就被人破解放到了资源站。从那以后我每次发布前都强制自己走一遍完整验证流先解包看资源是否乱码再在调试器里看解密日志是否打满最后测一次性能基线。这套流程对项目规格、团队规模没有任何要求纯靠自律——但比起被人解包之后再后悔这三步的成本低太多了。希望这次的拆解和踩坑记录能帮到你。加密工具用起来不难难的是在加密力度、加载性能和工程维护之间找到平衡多试几个版本你会找到适合自己项目的节奏。本文还有配套的精品资源点击获取
返回列表