ARTICLE DETAIL

资讯详情

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

Cocos Creator资源加密工具:构建到运行时的防拆包完整方案

Cocos Creator资源加密工具:构建到运行时的防拆包完整方案 简介面向 Cocos Creator 开发者的资源加密工具用于在项目构建发布阶段对游戏资源进行一键加密可降低原始资源被解包、提取与二次使用的风险。工具以脚本与命令行方式运行对现有构建流程侵入较小适合需要保护美术素材、预制体、配置表等内容的中小型团队与独立开发者。压缩包内共包含 383 个文件整体大小约为 10.03MB文件类型以 TypeScript、JavaScript、JSON 与 Markdown 文档为主另有若干 PowerShell 与命令行脚本、map 辅助文件及 lcl 附加文件。其中 TypeScript 与 JavaScript 脚本承载核心加密逻辑和运行入口JSON 文件用于配置加密参数Markdown 文档提供使用说明命令行脚本便于一键触发目录结构清晰可按需取用。当前已有 193 人浏览学习对于不熟悉资源加密流程的开发者这份工具包提供了可直接运行的脚本、配置示例与说明文档能省去从零搭建加密模块的重复工作同时借助文档和脚本注释可以较快理解加密参数的设置与调用方式方便结合具体项目做二次调整适配不同发布版本或发布渠道。1. cocoscreator 资源加密工具你先要搞清楚加密挡的是谁一个把 Creator 打包出来的包拖进解包工具三分钟后 assets 目录下所有图片、音频、预制体、JSON 配置全部明文躺着美术资源可以原样换皮搬运游戏逻辑里藏的数值和运营配置也一览无遗。你今天要认识的 cocoscreator 资源加密工具解决的就是这个问题在构建完成后把资源密文写回包内运行时在引擎读取文件的那一刻解开让明文永远不落盘。它防的是拆包党和换皮党不是逆向专家这个预期先立住。适合的小团队和独立开发者从零接入大概两三天能铺完。2. 加密链路设计构建产物长什么样引擎在哪里读文件2.1 从构建产物开始哪些文件值得动手哪些是废动作Cocos Creator 构建完成后产物目录结构大致是这样build/web-mobile/ assets/ main/ ... resources/ ... settings.json src/ settings.js ... application.js对 2.x 来说assets/main和assets/resources下面就是全部游戏资源对 3.x 来说结构差别在于多了 Asset Bundle 的概念每个 bundle 是一个子目录目录下有.json索引bundle 内的资源散落在bundle 名/下资源和配置混在一起。加密之前先回答一个问题哪些文件值得加密。图片、音频这类美术资源加密收益高且对性能影响小因为它们是构建产物里体积最大的一块预制体和 JSON 这类逻辑资产价值最高数量少解密开销可以忽略脚本是另一条路线通常走 JS 压缩加混淆不在资源加密的范畴。废动作包括.DS_Store、.git占位文件、src/里的启动脚本、cocos-js引擎库。引擎库本身就是压缩过的 JS加密它意义不大还把启动流程搞复杂。我一般建议配置文件里用白名单机制只对指定文件扩展名做处理而不是对所有文件无脑加密。2.2 引擎加载资源的完整路径找到那个真正需要改写的 hook 点不管 2.x 还是 3.xCocos Creator 加载资源时都会经过一个下载器加解析器的管线引擎接到一个 URL 请求下载器去读文件、返回 ArrayBuffer 或文本解析器再把这段数据变成本地可用的cc.Texture2D、cc.AudioClip、cc.SpriteFrame。你要下手的点就是这个下载器。以 2.x 为例cc.loader提供了注册自定义下载器的入口可以在引擎读文件之后、交给解析器之前插入自己的解密逻辑。这个方法叫cc.loader.registerDownloader传入的下载器函数接收url作为参数你在这个函数里用cc.loader.getRes或者原生文件系统读出密文解密后返回明文。3.x 下对应的是assetManager.downloader注册方式类似但回调形式和错误处理更严格。这里有个重要的设计决策不要自己重写整个文件读取要在引擎已经完成读文件这个动作之后做一次数据转换。这样做的原因是 Cocos Creator 内部对远程资源、本地资源的读取路径做了大量缓存和分支处理你从源头接管文件读取容易踩到缓存失效和引用路径不一致的坑。正确的思路是搭一个解密转换器而不是替换文件读取器。2.3 加密工具形态的选型构建后处理脚本和运行时解密器为什么缺一不可资源加密工具天然由两部分拼起来构建后加密脚本和运行时解密器。前者跑在构建服务器上负责把 build 目录下的明文资源替换成密文后者跑在玩家设备上负责在引擎读资源时还原。这两个部分缺了任何一个都跑不通只加密不配运行时解密游戏启动黑屏只写运行时解密不加密等于没做。很多刚上手的朋友会尝试改 Creator 引擎源码来达成加密效果这种做法不推荐升级引擎版本时合并成本极高而且这等于把引擎从官方包里拆出来之后一切版本更新都要对着 diff 打补丁。另一个相对少见的做法是修改项目里的cc-loader或bundle相关的 js 文件直接在构建产物里改代码。这种方式对单一 Hot Update 场景有效但碰到 Assets Bundle 多个包的情况下维护成本会滚雪球。我一般推荐的方式是构建后脚本负责加密和生成配置文件引擎侧复制一份自定义下载器代码进入项目启动时先注册解密器之后引擎的加载流程完全不自知地跑在解密后的数据上。3. 最小可用实现构建后加密脚本加运行时解密器3.1 构建后加密脚本遍历 build 目录把白名单资源全部加密写回下面这个脚本用 Node.js 跑可以在 Creator 构建完成之后手动执行也可以挂到构建插件的 afterBuild 回调里。它做的事情是遍历构建产物中指定的目录对白名单扩展名的文件做 AES-256-CBC 加密生成统一格式的密文文件。密文格式先约定好后面解密器要用前 4 个字节是 magicCRES标记这是加密资源接着 2 个字节是版本号再接下来 16 字节是 IV最后是密文本体。一个加密文件头就占了 22 字节。const crypto require(crypto); const fs require(fs); const path require(path); const KEY Buffer.from(0123456789abcdef0123456789abcdef, utf8); // 32字节密钥换成自己随机生成的 const WHITE_LIST [.json, .png, .jpg, .plist, .mp3, .prefab]; const SKIP_DIRS [src, jsb-adapter]; const BUILD_DIR path.join(__dirname, ../build/web-mobile); const ASSETS_DIR path.join(BUILD_DIR, assets); function walk(dir) { if (SKIP_DIRS.some((d) dir.includes(d))) return; const list fs.readdirSync(dir, { withFileTypes: true }); for (const entry of list) { if (entry.isDirectory()) { walk(path.join(dir, entry.name)); } else if (WHITE_LIST.includes(path.extname(entry.name).toLowerCase())) { encryptFile(path.join(dir, entry.name)); } } } function encryptFile(filePath) { const raw fs.readFileSync(filePath); const iv crypto.randomBytes(16); const cipher crypto.createCipheriv(aes-256-cbc, KEY, iv); const encrypted Buffer.concat([cipher.update(raw), cipher.final()]); const header Buffer.from(CRES); // 4字节识别头 const version Buffer.from([0x01, 0x00]); // 加密格式版本 1.0 const out Buffer.concat([header, version, iv, encrypted]); fs.writeFileSync(filePath, out); console.log(encrypted:, path.relative(BUILD_DIR, filePath), ${raw.length} - ${out.length}); } walk(ASSETS_DIR);这段脚本的核心逻辑是walk加encryptFilewalk递归遍历 assets 目录跳过当前不需要加密的目录encryptFile先随机生成 IV用 AES-256-CBC 加密原始数据再加上文件头写回原路径。加密后文件名不变路径不变只是内容变成密文。这样所有引用资源的路径查找都不用改包括resources目录下的动态加载。参数说明里值得留意的是密钥例子里写死的0123456789abcdef0123456789abcdef是 32 字节刚好对应 AES-256。实际使用必须自己生成随机密钥并且把密钥写进项目的配置文件里供运行时解密器使用。另外SKIP_DIRS用来跳过src目录避免引擎启动脚本被破坏。加密工具只会处理白名单文件这个设计可以防止把settings.json也加密掉——Cocos 引擎在初始化阶段就要读这些索引文件明文保留能大幅度降低接入难度。3.2 运行时解密器在下载器注册处解开别等解析器报错解谜器的核心是在引擎加载管线里注册自定义下载器。2.x 和 3.x 注册方式不同先看 2.x// 2.x 写法放在 main.js 中 cc.game.onStart 之前 const cryptoJs require(crypto-js); function decryptBuffer(arrayBuffer) { const bytes new Uint8Array(arrayBuffer); const magic String.fromCharCode(...bytes.slice(0, 4)); if (magic ! CRES) { return arrayBuffer; // 不是加密资源原样返回 } const version (bytes[4] 8) bytes[5]; // 版本号 const iv bytes.slice(6, 22); // 16字节IV const encrypted bytes.slice(22); // 密文 // crypto-js 需要 WordArray做一次转换 const key cryptoJs.enc.Utf8.parse(0123456789abcdef0123456789abcdef); const decrypted cryptoJs.AES.decrypt( { ciphertext: cryptoJs.lib.WordArray.create(encrypted) }, key, { iv: cryptoJs.lib.WordArray.create(iv) } ); // 转回 ArrayBuffer const words decrypted.words; const result new Uint8Array(decrypted.sigBytes); for (let i 0; i decrypted.sigBytes; i) { result[i] (words[i 2] (24 - (i % 4) * 8)) 0xff; } return result.buffer; } cc.loader.registerDownloader(.json, function (url, callback) { cc.loader.load({ url: url, type: arraybuffer }, (err, data) { if (err) return callback(err); callback(null, decryptBuffer(data)); }); });这里把解密器和引擎的下载管线接起来引擎请求.json文件时先以 arraybuffer 格式读出密文解密后作为结果交给引擎。回调签名是约定好的引擎内部解析器拿到的已经是明文。3.x 的写法相似但 API 移到assetManager.downloader注册时要做存在性检查// 3.x 写法 import { assetManager } from cc; function decryptBuffer(arrayBuffer) { // 与 2.x 相同的解密函数 } assetManager.downloader.register(.json, (url, options, onComplete) { assetManager.downloader.download(url, { responseType: arraybuffer }, (err, data) { if (err) return onComplete(err); onComplete(null, decryptBuffer(data)); }); });参数说明register的第二个参数是onComplete成功时传入解密后的数据失败时传入错误对象。3.x 里下载器注册前要确保assetManager.downloader已初始化放在project.assetManager初始化之后比较稳妥。两种版本里decryptBuffer都做了 magic 判断这意味着加密脚本只处理指定文件未处理的文件原样通过不会因为漏掉了某个纯文本配置而暴毙。3.3 密钥存哪抬高破解门槛的三种藏法加密强度取决于密钥藏得有多深。最低门槛是明文写在main.js里防君子不防小人中等做法是拆成多段拼接还带一点异或混淆高一点的方案是把密钥拆进不同文件再按偏移量取出来组合成真正的密钥。下面是三种常见做法的对比做法实现成本破解难度维护成本明文写在代码最低极低低字符串分段异或后拼接低略高低拆文件存储启动时组合中较高中我一般用第二种把密钥拆成三段每段做一个简单的异或或者 base64 反转运行时再拼起来。这么做不是为了挡住专门逆向的人这层保护的目标是把纯小白拦在门外用最简单的手段让解包工具直接失效——而现实中九成拆包用户用的就是现成解包工具根本不会动运行时调试。4. 参数设计白名单、密钥、性能预算、热更包怎么配4.1 一个可落地的配置文件示例字段逐个说清楚实际落地时加密工具最好带一个encrypt-config.json把策略放出来方便不同项目改。以下是我常用的最小一套配置{ key: 你的32字节随机密钥字符串, algorithm: aes-256-cbc, whiteList: [.json, .png, .jpg, .plist, .mp3, .prefab, .wav], skipDirs: [src, jsb-adapter, assets/import, assets/main/import], skipFiles: [settings.json, config.json, version.json, project.json], headerMagic: CRES, version: [1, 0], decryptOnLoad: true }字段说明key就是加密脚本和解密器共用的密钥一旦配置就要两边同步whiteList是扩展名白名单凡是列表中的文件都会被加密skipDirs用来排除目录比如 Creator 生成的assets/main/import下面有一堆.json是素材导入映射表加密它们会导致资源不匹配的报错skipFiles是文件级排除settings.json和config.json是引擎初始化阶段必须明文读取的配置加密后启动阶段还拿不到解密器所以务必排除。这个文件放项目根目录加密脚本读取它来做处理解密器从build/src/settings.js或项目配置里读取密钥。配置千万别用resources目录里放明文配置文件的方式因为那样等于是把密钥和资源放在同一个包里。4.2 加密粒度与包体、性能预算图片和音频到底要不要加密加密粒度需要权衡。预制体.prefab和 JSON 必须加密这是游戏的核心逻辑数据数量少且解密开销可忽略属于高优先级。图片和音频是主要包体加密后解密耗时稍长但收益在于防止素材被直接替换尤其适合做换皮对抗。性能预算方面建议控制在首场景加载解密总耗时不超过 80ms。AES-256-CBC 解密 1MB 数据在手机浏览器上的耗时大致是 10-20ms在小游戏环境稍慢一点。如果包体巨大图片音频全加密会让首屏加载有明显卡顿。一个常见取舍图片切到压缩纹理格式后加密Android 上直接走 GPU 能解析的格式解密后再交给压缩纹理解压整体延迟可以接受。音频优先处理mp3和wav这两个格式解密后解码效率尚可ogg要看目标平台支不支持。包体膨胀也得多说一句。AES 加密不会让资源体积显著变大增加的开销只有加密头 22 字节加 PKCS7 padding 每一块的补齐量。真正的包体变化来自你配合压缩纹理格式替换了原始图片存储这个变化是加密方案带来的连带效果得算进整体包体预算里。4.3 热更资源怎么加解密版本号、密钥轮换与缓存策略热更新是资源加密里最容易翻车的一块。Creator 的热更流程是把新资源打进remote-assets或自定义目录然后用AssetsManager拉取到本地。这个目录里的资源同样是普通文件同样需要密文写入。常见的方案有两个第一套是热更资源用和包内资源一样的密钥机制完全复用代价是一旦密钥泄露历史所有热更包都被解开第二套是按大版本轮换密钥每次发新版换一次密钥热更包里附带本次版本密钥同时保留旧版本解密能力。我一般选第二套理由很直接热更包更新频率高且都落在本地文件系统里攻破成本低。密钥轮换时要注意更新器要在合适时机触发解密器重注册。实际编码里常见做法是在热更初始化完成后把新密钥注入到当前解密器上下文用引用切换完成热替换——旧的资源用旧密钥解新的用新密钥解。缓存策略也要留心Creator 加载器会对部分资源做内存缓存同一份资源第二次加载走的不是下载器而是内存缓存这意味着解密只会发生一次。如果你的场景里资源在运行期会被裁剪再重新加载文件缓存命中的逻辑才会走下载器路径。加密之后缓存逻辑不会有感知因为它读到的是解密后的明文。5. cocoscreator 资源加密避坑清单五个高频翻车现场5.1 黑屏控制台资源 404现象加密完打开游戏黑屏控制台报找不到xxx.json。原因这个大概率是 2.x 下把引擎初始化需要读的 JSON 文件加密了比如settings.json或config.json引擎在自定义下载器注册之前就去读它们拿到密文以后解析器直接报错。解决把settings.json、config.json、project.json全部加进skipFiles并确认白名单里没有把json一刀切地全加密。可以在加密脚本里输出一份清单构建后人工看一遍确认启动文件保持明文。5.2 Assets Bundle 重新下载后全部加载失败现象用 Asset Bundle 分包的项目加密后网络下载的 bundle 加载失败本地 bundle 正常。原因3.x 的 Bundle 下载完成后内部走的是assetManager.loadBundle的独立解析链路部分版本不会走你已经注册的下载器或走了但不是同一个 bundle 实例。解决把loadBundle的参数对象里加一个ext字段指向 bundle 子目录同时确保注册下载器时对 bundle 相关扩展名同样生效。可以在loadBundle成功回调里再做一次文件解密替换不过这属于兜底做法慎用最稳的还是沿主线下载器逻辑补全注册入口。5.3 微信小游戏平台解密失败现象同一套加密逻辑Web 端正常微信小游戏端图片显示白屏JS 报ArrayBuffer相关错误。原因小游戏运行时没有浏览器完整的Uint8Array和Buffer兼容层crypto-js在某些版本的 engine adapter 下行为不一致尤其当你依赖String.fromCharCode(...bytes)这样展开大数组的方式时运存容易爆。解决解密函数里避免大数组展开循环逐字节做转换同时用wx.getFileSystemManager().readFile接口读取本地文件返回 ArrayBuffer 后进入同一个解密函数。另外小游戏平台的内存偏紧张建议只对首包资源做全量加密热更包里的图片可以降低加密覆盖率。5.4 纹理紫屏图片解密后仍无法显示现象图片资源加了加密解密逻辑没报错但所有Texture2D显示成紫屏。原因纹理资源在创建时就要能识别文件头png解密后如果多了一段加密头残留或解密结果被crypto-js转成带符号的字节引擎解析就会失败。并且如果 packable 贴图被打进图集一个图集里的图片解密后错位整张图集紫屏。解决解密函数里去掉CRES头再返回纯明文数据同时检查字节转换逻辑确保Uint8Array内全是无符号值。图集资源建议在配置里把.plist和关联的.png列为同一批加密文件要么一起加密要么都不加密避免图集被半套流程处理。5.5 新版本密钥轮换后老玩家热更失败现象发了一版新包热更资源用新密钥加密。老玩家点击更新热更数据全部加载失败只能重新下载全量包。原因热更脚本还在用旧密钥解密新资源密钥不匹配导致密文解出乱码资源解析时校验失败。解决更新器必须带着版本号切换密钥。在本地持久化一份密钥版本记录启动时读取资源版本号调用对应版本的密钥做解密。密钥轮换要向前兼容保留旧版本密钥至少一个完整迭代周期。另外改密钥后必须跑一次全量构建加解密验证不能让半旧半新的资源流进测试包。6. 进阶把加密做成构建流水线的一个环节并加一道可逆性验证6.1 明文解密自检在 CI 里验证每个资源能还原加密工具要长期可用必须在构建脚本里加一个自检步骤加密完成后对所有密文文件做一次逆操作解回明文校验文件头是原始格式、文件大小和原文件一致。这个步骤能提前暴露密钥错配、白名单漏配、图集错位等批量问题。// 自检脚本片段紧接加密后执行 function verifyFile(filePath) { const data fs.readFileSync(filePath); const magic data.slice(0, 4).toString(); if (magic ! CRES) return true; // 未加密 const iv data.slice(6, 22); const encrypted data.slice(22); const decipher crypto.createDecipheriv(aes-256-cbc, KEY, iv); const decrypted Buffer.concat([decipher.update(encrypted), decipher.final()]); // 检查解密后的文件头或魔数对应扩展名 const ext path.extname(filePath).toLowerCase(); const headerOk checkHeader(decrypted, ext); return headerOk; }自检通过后构建脚本才执行压缩、打包流程。我会把这段验证放在加密脚本的verifyAll函数里失败直接抛异常让 CI 挂掉。6.2 将工具拆成三个独立模块便于团队维护工具不是单个文件建议拆成三个模块encrypt-core算法和文件遍历、build-hookCreator 构建钩子、runtime-decrypt供项目源码引用的解密器。这样的好处是 Creator 升级时只需要改 build-hook 层的 API 调用算法和运行时不受影响。6.3 最后一锤换密钥时必须跑一次全量解密验证我踩过最深的坑就是改完密钥忘了同步到解密器导致包内资源解不开。现在每次换密钥我都会强制走一遍构建流水线加密后立刻跑自检然后启动游戏验证首屏和主场景资源加载。这个习惯让很多玄学黑匣子问题在进测试前就暴出来。尽量把密钥配置集中到项目根目录的一个文件里脚本读同一个配置文件不要散落在加密器和解密器中。这套方向值得投入资损很小收益是稳定的拆包对抗能力。希望帮到你。本文还有配套的精品资源点击获取
返回列表