Cocos 3.x 引擎解析 / [微信/抖音小游戏] 资源加密与解析·实践|征稿活动V5

Cocos 3.x 引擎解析 / [微信/抖音小游戏] 资源加密与解析·实践|征稿活动V5

本文写的插件, 全部思路代码已融合进去了, 可以 pkg 一个工具包出来独立使用
建议大家多思考, 看完文章的话, 应该也可以实现, 这个插件 ~忒贵的~ :neutral_face:
Cocos Creator 构建后加密资源[插件] | Cocos Store

前述:

实践是检验真理的唯一标准

最近 Cocos Store 好多大佬分享小游戏的源码(微信比较多),
但是在 Cocos Store 里面没看到有关于 Cocos 3.x 的小游戏资源加密的开源插件
正好赶上 Cocos 论坛征稿, 所以写一点点实践的内容, 本文操作为主, 原理讲解之类的就不多概述了

本文概要 :laughing:, 核心内容在 2.资源解析 里面
主要讲两个内容 ::= 1.资源加密 + 2.资源解析
因为时间的关系, 所以只讲下关于图片的部分, 看了下论坛的帖子,
论坛里面 .jpg 格式的图片没多少大佬讲解, 所以本文就从这个格式开始讲(使用微信小游戏做示例)
微信小游戏里面 XMLHttpRequest 是很难用,
所以本文不会用到这个来解析, 也不需要

基于 Cocos Creator 3.5.2 版本开发 · 微信小游戏做示例

1. 资源加密(使用简单正则替换 Base64 的方式来加密 .jpg 资源文件)

大多数的资源文件, 基本上都可以转换成 Base64 的文件格式, 因此着手从这个上面来加密

1.1 先看下这个 .jpg 文件的16进制的样子

PS: 所有的资源都是数字的计算
如下所示, 基本上每个图片都有头部标识(例如这个: Adobe), 这个可以用来做水印版权之类的,
这些 00 之类的还可以写点其它的内容, 当然也能把数据隐写到图片里面去

有些大佬会用这个方式来手动解密, 计算出加密的逻辑 :no_mouth:


1.1.1 先写个简单的工具对图片资源编码做个大概的认知

采取简单的 Snip 小代码 方式来运行在浏览器里面
关于浏览器内使用 Snip 的介绍可参考个人之前写的这两个帖子
1.1 在任何网页上运行 JavaScript 的代码片段
1.8.4 在浏览器上使用 Snip 片段代码

[Snip 小代码·加密还是解密16进制]

var JiaJieMi=confirm("加密还是解密16进制? 默认加密成16进制");
console.clear();

var getAllStr="",inputStr;
var getCharCont=("N").charCodeAt().toString(16);
if(JiaJieMi){
    getAllStr="";
    inputStr=prompt("请输入要加密转成16进制的字符串!","PNG");
    for(var ii=0;ii<inputStr.length;ii++){
        getCharCont=(inputStr[ii]).charCodeAt().toString(16);
        if(getCharCont.match(/[a-zA-Z]/g)){
            getCharCont=getCharCont.replace(/[a-zA-Z]/g,getCharCont.match(/[a-zA-Z]/g)[0].toUpperCase());
        }else{
            getCharCont=getCharCont.replace(/[a-zA-Z]/g,getCharCont.match(/[a-zA-Z]/g));
        };
        getAllStr+=getCharCont+" ";
    };
}else{
    getAllStr="";
    var inputStr=prompt("请输入需要解密的16进制的字符串\n请用空格隔开!","50 4E 47 ");
    inputStr=inputStr.split(" ");
    var tempNum="";
    for(var jj=0;jj<inputStr.length;jj++){
        if(inputStr[jj].length<1){continue;};
//         console.log("Number(inputStr[jj]),",Number(inputStr[jj]));
        if(inputStr[jj].match(/[a-zA-Z]/g)){
            tempNum=inputStr[jj].replace(/[a-zA-Z]/g,inputStr[jj].match(/[a-zA-Z]/g)[0].toLowerCase());
            console.log(inputStr,"tempNum,",tempNum,String.fromCharCode(parseInt(Number(tempNum),16)));
            getCharCont=String.fromCharCode(parseInt((tempNum),16));
        }else{
            tempNum=inputStr[jj];
        getCharCont=String.fromCharCode(parseInt(Number(tempNum),16));
        };
        getAllStr+=getCharCont+"";
    };
};

prompt("转换后的结果为",getAllStr);
console.log("%c字符串\t["+inputStr+"]\n转换后的结果为::=>\t["+getAllStr+"]","color:green;font-size:23px;");

1.1.2 使用上面的工具来思考下图片编码的转换

如下, 由此可知, 图片资源的每个字符串都有对应的数字(16进制),
简单理解的话, 加解密就是在改改数字之类的

1.2 为方便演示, 此加密采取简单的 Base64 字符串加密

开始用的 Bs64 字符串全量加密, 后面发现效率实在有点低, 就改成了这个版本的正则替换加密,
话不多说, 上代码

[JS 代码·建议安装 NodeJs 来运行]

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////// 代码耗时开始计算 ////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 代码耗时开始计算
var timeBeginNum = new Date().getTime();
var timeEndNum = new Date().getTime();
var jsUsingTimeNum = timeEndNum - timeBeginNum;

/**
 * 计算耗时
 * @param {*} timeNumber 如:142857(单位:ms)
 * 返回:"00:02:22,857"
 */
function calcTime(timeNumber) {
    var hUnit = 60 * 60 * 1000;
    var mUnit = 60 * 1000;
    var sUnit = 1000;
    var h = Math.trunc(timeNumber / hUnit);
    var m = Math.trunc((timeNumber % hUnit) / mUnit);
    var s = Math.trunc(((timeNumber % hUnit) % mUnit) / sUnit);
    var ms = Math.trunc(((timeNumber % hUnit) % mUnit) % sUnit);
    var calcResult = `${(Array(2).join(0) + h).slice(-2)}:${(Array(2).join(0) + m).slice(-2)}:${(Array(2).join(0) + s).slice(-2)},${(Array(3).join(0) + ms).slice(-3)}`;
    calcResult = `${(Array(2).join(0) + h).slice(-2)}时:${(Array(2).join(0) + m).slice(-2)}分:${(Array(2).join(0) + s).slice(-2)}秒:${(Array(3).join(0) + ms).slice(-3)}毫秒`;
    return calcResult;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////// 代码耗时结束计算 ////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// png 的公因式 "iVBORw0KGgoAAAANSUhEUgAAA" 开头
// 自行设计的New图片(最小)
var base64Code0 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAIAgMAAABvxIx9AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAADSgAwAEAAAAAQAAABgAAAAAekYNPAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAlQTFRFAKLo////+Pf/ZskxbwAAAC9JREFUCNdjYIAAEZZQV5EABlGXAFYQFRLqGQqmXEMdGERCAhhDgUpEQh1coMoZANIYB3ynZiTBAAAAAElFTkSuQmCC";
// 自行压缩设计的最小纯颜色色块(4个颜色)
var base64Code1 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAFUlEQVR4AWNxcflfvpuB4T8Q/XcBAChRBf8ySR2zAAAAAElFTkSuQmCC";
// 自行压缩设计的最小纯颜色色块(1个颜色)
var base64Code2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4AWP6z8AAAAMJAQIVljauAAAAAElFTkSuQmCC";
var base64Code3 = "data:image/jpeg;base64,/9j/2wBDAAcFBQYFBAcGBgYIBwcICxILCwoKCxYPEA0SGhYbGhkWGRgcICgiHB4mHhgZIzAkJiorLS4tGyIyNTEsNSgsLSz/2wBDAQcICAsJCxULCxUsHRkdLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCz/wAARCAABAAEDASEAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCEA5SL/9k="
var base64Code4 = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwCCiiiu4+ZP/9k="

var getFs = require('fs');
var rqpathGet = require('path');
var inputFilesPath = "52ef29ed-bd92-6e94-ab2f-0ebc91bf3a60.jpg" || "cocos.jpg" || "52ef29ed-bd92-6e94-ab2f-0ebc91bf3a60.jpg";
var cmdExecute = "0" || "1" || "0" || "0";
var dataImageTypes = {
    ".jpg": "image/jpeg",
    ".png": "image/png",
};
/**
 * 公共的日志输出函数
 * @param  {...any} msg 传入日志的参数
 */
function cclog(...msg) {
    console.log(...msg);
};
/**
 * 公共的读取文件的函数
 * @param filePath 文件路径
 * @param readType 读取的类型 (utf-8)(binary)
 */
function FsReadFile(filePath, readType = "utf-8") {
    var temp_file = getFs.readFileSync(filePath, readType, (err, data) => { });
    var bufferShow = new Buffer.from(temp_file, 'binary');
    let getBase64Files = bufferShow.toString('base64');
    return getBase64Files;
};
/**
 * 保存文件内容
 * @param {*} filePath 
 * @param {*} fileContent 
 * @returns 
 */
function FsWriteFile(filePath, fileContent, readType = "utf8") {
    var temp_fileEnd = getFs.writeFileSync(filePath, fileContent, readType);
    return temp_fileEnd;
};
/**
 * 执行清屏命令, 清除之前的命令
 */
function screen_clear() {
    process.stdout.write(process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H')
};
screen_clear();


var filesPath = inputFilesPath.replace(new RegExp(/\\/, 'g'), '/');
filesPath = filesPath.replace(new RegExp(/\"/, 'g'), '');
var newPathSave0 = rqpathGet.dirname(filesPath);
var newPathSave1 = rqpathGet.basename(filesPath);
var newPathSave2 = rqpathGet.extname(filesPath);
var newPathSaveNoExtName = newPathSave1.split(newPathSave2)[0];
var endNewPathFile22 = newPathSave0 + "/" + newPathSaveNoExtName + "." + "加密" + "." + Math.floor(Math.random() * (new Date().getTime() / 1234567)) + newPathSave2;
var matCharCode = "c" || "a" || "c";
var replaceEndCode = "+" || "无" || "无" || "C";

cclog("[CC] 开始执行程序....");
cclog(`[CC] 输入文件路径= ${filesPath} \n[CC] 输入的操作指令= ${cmdExecute}\n正在更改......`);
timeBeginNum = new Date().getTime();
if (rqpathGet.extname(filesPath) == ".png" || rqpathGet.extname(filesPath) == ".jpg") {
    if (cmdExecute == "0") {
        endNewPathFile22 = newPathSave0 + "/" + newPathSaveNoExtName + "." + "加密" + "." + Math.floor(Math.random() * (new Date().getTime() / 1234567)) + newPathSave2;
        var base64Head = `data:${dataImageTypes[rqpathGet.extname(filesPath)]};base64,`;
        // 获取 base64 的无头部函数定义的字符串(可在此处进行加解密)
        var getBase64Cont = FsReadFile(filesPath, "binary");
        var getImgEnCodeStr = getBase64Cont.split("");
        var getMathAll = getBase64Cont.match(new RegExp(matCharCode, "g"));
        var getAllOBJArr = [], tempOBJ = { char: "", index: 0 };
        // 此项匹配的是大小写字符c,所以后续需要做个过滤
        var matRegExp = new RegExp(matCharCode, "i");
        for (var ii = 0; ii < getMathAll.length; ii++) {
            // 做个限制, 不要处理太多字符串
            if (getAllOBJArr.length > 36 * 7) {
                break;
            };
            if (getImgEnCodeStr.indexOf(matCharCode)) {
                tempOBJ = { char: "", index: 0 };
                // 匹配到的字符串
                tempOBJ.char = matCharCode || getImgEnCodeStr.match(matRegExp)[0];
                // 匹配到的索引位置
                tempOBJ.index = getImgEnCodeStr.indexOf(matCharCode) || getImgEnCodeStr.match(matRegExp).index;
                getImgEnCodeStr[getImgEnCodeStr.indexOf(matCharCode)] = replaceEndCode;
                getAllOBJArr.push(tempOBJ);
            };
        };
        getImgEnCodeStr = getImgEnCodeStr.join("");
        var imgAfterBuffer = new Buffer.from(getImgEnCodeStr, 'base64');
        FsWriteFile(endNewPathFile22, imgAfterBuffer, "utf8");
        timeEndNum = new Date().getTime();
        jsUsingTimeNum = timeEndNum - timeBeginNum;
        cclog(`[CC] 耗时::=> ${jsUsingTimeNum}ms 共计::=> ` + calcTime(jsUsingTimeNum));
        cclog("[CC] 执行完毕, 正在退出程序....");

        // 输出解密数组
        var encodeKeyArr = [];
        for (var jj = 0; jj < getAllOBJArr.length; jj++) {
            encodeKeyArr.push(getAllOBJArr[jj].index);
        };
        getAllOBJArr.push({ decodeKey: encodeKeyArr });
        FsWriteFile(endNewPathFile22 + ".encodeArr.json", JSON.stringify(getAllOBJArr), "utf8");
    } else if (cmdExecute == "1") {
        var decodeKey = [537, 683, 791, 1010, 1013, 1094, 1097, 1140, 1144, 1163, 1306, 1429, 1458, 1520, 1584, 1627, 1668, 1770, 1797, 1812, 1829, 1838, 1940, 1944, 2007, 2010, 2124, 2144, 2215, 2216, 2260, 2344, 2363, 2443, 2498, 2504, 2542, 2547, 2647, 2703, 2716, 2753, 2787, 2884, 2885, 2898, 2901, 2929, 3058, 3100, 3126, 3137, 3185, 3258, 3268, 3331, 3379, 3438, 3577, 3829, 3845, 3924, 4082, 4089, 4137, 4214, 4220, 4283, 4340, 4470, 4496, 4497, 4520, 4533, 4807, 4839, 4947, 4958, 4966, 5024, 5097, 5142, 5152, 5174, 5185, 5325, 5376, 5596, 5650, 5660, 5757, 5798, 5852, 5939, 6012, 6027, 6037, 6074, 6136, 6153, 6264, 6291, 6300, 6378, 6382, 6415, 6433, 6487, 6562, 6581, 6582, 6588, 6609, 6737, 6742, 6962, 7014, 7062, 7093, 7319, 7388, 7430, 7513, 7520, 7564, 7639, 7644, 7745, 7761, 7854, 7878, 7911, 7938, 8058, 8078, 8132, 8144, 8216, 8292, 8326, 8389, 8443, 8523, 8559, 8685, 8712, 8776, 8781, 8786, 8810, 8906, 8940, 9091, 9181, 9200, 9230, 9270, 9279, 9331, 9401, 9423, 9482, 9601, 9638, 9668, 9675, 9692, 9850, 9888, 9965, 10048, 10070, 10099, 10202, 10235, 10373, 10419, 10434, 10603, 10624, 10627, 10637, 10825, 10859, 10893, 10901, 11096, 11140, 11304, 11465, 11472, 11533, 11564, 11710, 11734, 11784, 11877, 11886, 12051, 12155, 12162, 12190, 12234, 12283, 12550, 12643, 12691, 12701, 12783, 12979, 13041, 13096, 13112, 13122, 13143, 13158, 13342, 13373, 13407, 13524, 13645, 13741, 13763, 13792, 13981, 13992, 14007, 14047, 14064, 14077, 14098, 14122, 14177, 14199, 14246, 14414, 14575, 14579, 14605, 14643, 14715, 14726, 14745, 14770, 14994, 15119, 15145, 15163, 15420, 15455, 15511, 15541, 15569];

        endNewPathFile22 = newPathSave0 + "/" + newPathSaveNoExtName + "." + "解密" + "." + newPathSave2;
        // var base64Head = `data:${dataImageTypes[rqpathGet.extname(filesPath)]};base64,`;
        // 获取 base64 的无头部函数定义的字符串(可在此处进行加解密)
        var getBase64Cont = FsReadFile(filesPath, "binary");
        var getImgEnCodeStr = getBase64Cont.split("");
        var getTmpInd = 0;
        for (var kk = 0; kk < decodeKey.length; kk++) {
            getTmpInd = decodeKey[kk];
            // 开始解密
            getImgEnCodeStr[getTmpInd] = matCharCode;
        };

        getImgEnCodeStr = getImgEnCodeStr.join("");
        var imgAfterBuffer = new Buffer.from(getImgEnCodeStr, 'base64');
        FsWriteFile(endNewPathFile22, imgAfterBuffer, "utf8");
        timeEndNum = new Date().getTime();
        jsUsingTimeNum = timeEndNum - timeBeginNum;
        cclog(`[CC] 耗时::=> ${jsUsingTimeNum}ms 共计::=> ` + calcTime(jsUsingTimeNum));
        cclog("[CC] 执行完毕, 正在退出程序....");
    };
} else {
    cclog("....错误执行....");
};

1.2.1 关于加密代码的介绍

前面的代码耗时统计应该不用多说, 还有个关于控制台日志清除的也可以用下,
后面跟着的就是几个 Base64 字符串是个人设计的, 压缩到最小的图片编码, 可以用于测试之类的,
上面写的代码主要原理就是::
=>读取图片文件的Base64字符串=>
=>采取正则替换指定字符(限制长度)=>
=>生成替换的index数组(方便还原)=>
=>生成对应的解密 JSON , 方便在 decodeKey 解密还原图片原来的样子=>

1.2.2 使用加解密工具的过程介绍

就是 CMD 执行 NodeJs 就行,
一般来说, 加密后, 图片也是可以打开的, 不影响查看, 就是让人看不懂图片内容而已

[NodeJS 加密处理]

[NodeJS 解密处理]

1.2.3 加密对应的 JSON 数组展示 (示例)

1.3 对比下两个加解密图片方法的效率

测试图片大小为 21.4 MB
对于大图片加密来说
Base64 的间隔加密方法的效率比 XOR 异或加密字节效率要低很多

XOR 异或字节加密 21.4MB 的图片耗时约为 \color{green}{56.738 \ ms}
优化版的间隔 Base64 加密21.4MB 的图片耗时约为 \color{red}{1926 \ ms}
本文只讲下 Base64 的版本, XOR 的留给大家做个思考吧, 实在想看的话,
本文关联的插件版本里面也有代码内容, 很贵呢, :laughing:所以大家思考下吧 ~~

2. 资源解析(结合微信开发者工具去调试阅读引擎源码)

2.1 关于资源解析的部分, 更多内容不再赘述, 可以参考如下链接

可参考大佬 宝爷 的帖子=> 资源管理系统剖析【二:资源管线与资源下载】

2.2 打开两个版本的引擎源码大概查看下

引擎安装目录
Creator/3.4.0/resources/resources/3d/engine/cocos/core/asset-manager/asset-manager.ts
Creator/3.5.2/resources/resources/3d/engine/cocos/core/asset-manager/asset-manager.ts
如下所示, 不同的引擎版本使用的代码都有点区别, 而且代码是有点多的, 感觉不太好理解


2.3 捋清思路(只是想简单改改解析 .jpg 的部分代码而已)

所以, 个人的想法就很简单, 直接加密图片,
然后在微信里面调试和解析, 调试引擎这个部分,
感觉最快的方式就是直接改, 然后看报错, 然后追踪代码

2.4 打包构建微信小游戏版本

构建微信小游戏版本, 然后用微信开发者工具打开


2.5 开始加密 .jpg 文件, 准备解读 .jpg 文件解析流程

用上文写到的 .jpg 加密工具来对这个 .jpg 图片进行一次加密,
很明显可以看到, 虽然没报错, 但是这个加载流程就已经走不通了, 接下来看看警告,
\color{red}{WebGL: INVALID_VALUE: texSubImage2D: bad image data}


2.6 写个简单的压缩和格式化的工具来辅助阅读引擎

展开警告可以发现, 这就是引擎的代码, 但是发布时, 引擎代码是压缩过的版本,
所以, 再来个工具吧, 老规矩, 上代码~
这样操作一下, 不是简单直观得多 ? 妙呀 ~~

[NodeJs-格式化代码的工具]

var getFs = require('fs');
var babe_parse = require("@babel/parser"); //解析为ast
var generator = require('@babel/generator').default;//ast解析为代码
var rqpathGet = require('path');
/**
 * 公共的读取文件的函数
 * @param filePath 文件路径
 * @param readType 读取的类型 (utf-8)(binary)
 */
function FsReadFile(filePath, readType = "utf-8") {
    var temp_file = getFs.readFileSync(filePath, readType, (err, data) => { });
    return temp_file;
};
/**
 * 保存文件内容
 * @param {*} filePath 
 * @param {*} fileContent 
 * @returns 
 */
function FsWriteFile(filePath, fileContent, readType = "utf8") {
    var temp_fileEnd = getFs.writeFileSync(filePath, fileContent, readType);
    return temp_fileEnd;
};
// 采取 AST 的一部分方法来进行格式化吧
var filesPath="cocos-js/cc.3c416.js";
var getJsContStr = FsReadFile(filesPath);
var geJsAst=babe_parse.parse(getJsContStr);
var geShiHuaJS=generator(geJsAst, { compact: false},).code;
FsWriteFile(filesPath, endToCode, "utf8");


2.7 有点收不住呀,简单点操作吧, 这样追踪有点费力

疑惑: 已知要解析 .jpg 的图片, 已格式化引擎, 求具体逻辑位置 ?
答: 微信开发者工具和 Cocos 编辑器一样, 可看做一个 Vscode, 直接正则搜素关键词就行
解: 正则过滤(.js,.ts) 搜索匹配 .jpg
请大概阅读并理解 libs/common/engine/AssetManager.js

2.8 根据阅读理解, 继续搜索 .parse 的解析部分

因为是格式化的原因, 所以这次正则匹配注意空格
搜索 [.parse =], 有五个匹配, 好, 逐次观察, 最后定位到这里=>

也可以用 Vscode 的正则匹配搜索下方:point_down:这个来定位

.parse=function\([\w],[\w],[\w],[\w],[\w]\)\{

[Cocos 3.5.2 引擎的关键部分-解析文件][注入一点点日志文件吧]

t.parse = function (e, t, n, i, r) {
          var o = this,
            a = xl.get(e);
            console.log("[CC][引擎注入][parse]", [e, t, n, i, r]);
          if (a) r(null, a);else {
            var s = this._parsing.get(e);
            console.log("[CC][引擎注入][this._parsing]", [s,this._parsing]);
            if (s) s.push(r);else {
              var c = this._parsers[n];
              c ? (this._parsing.add(e, [r]), c(t, i, function (t, n) {
                t ? yl.remove(e) : Fl(n) || xl.add(e, n);
                for (var i = o._parsing.remove(e), r = 0, a = i.length; r < a; r++) i[r](t, n);
              })) : r(null, t);
            }
          }
        }, e;

2.9 保存后观察日志

很明显, 就是这个地方开始解析 .jpg 文件的,
继续注入引擎代码日志, 然后先恢复下 .jpg 图片, 看看解析流程是怎么样的
很明显, 所有的资源文件基本上都在这里进行处理了, 这样就很好处理了,

[Cocos 3.5.2 引擎-过滤日志注入]

t.parse = function (e, t, n, i, r) {
          var o = this,
            a = xl.get(e);
            if(n==".jpg"){console.log("[CC][引擎注入][parse]", [e, t, n, i, r]);};
          if (a) r(null, a);else {
            var s = this._parsing.get(e);
            if (s) s.push(r);else {
              var c = this._parsers[n];
              if(n==".jpg"){console.log("[CC][引擎注入][this._parsing]", [s,this._parsers]);};
              c ? (this._parsing.add(e, [r]), c(t, i, function (t, n) {
                t ? yl.remove(e) : Fl(n) || xl.add(e, n);
                for (var i = o._parsing.remove(e), r = 0, a = i.length; r < a; r++) i[r](t, n);
              })) : r(null, t);
            }
          }
        }, e;


2.10 查看的关键解析部分代码如下

观察搜索日志就可以看到=>
.jpg,.bmp,.gif,.png,.ico,.tiff,.jpeg,.image,.webp 这些文件都是用的 function NO 这个函数的
所以要处理 .jpg 的解析, 就需要定位改下 function NO 函数

.ExportJson: ƒ parseJson(url, options, onComplete)
.astc: ƒ parseASTCTex(file, options, onComplete)
.atlas: ƒ parseText(url, options, onComplete)
.bin: ƒ parseArrayBuffer(url, options, onComplete)
.binary: ƒ parseArrayBuffer(url, options, onComplete)
.bmp: ƒ NO(e, t, n)
.ccon: ƒ (e, t, n)
.cconb: ƒ (e, t, n)
.dbbin: ƒ parseArrayBuffer(url, options, onComplete)
.eot: ƒ loadFont(url, options, onComplete)
.fnt: ƒ parseText(url, options, onComplete)
.font: ƒ loadFont(url, options, onComplete)
.fsh: ƒ parseText(url, options, onComplete)
.gif: ƒ NO(e, t, n)
.ico: ƒ NO(e, t, n)
.image: ƒ NO(e, t, n)
.jpeg: ƒ NO(e, t, n)
.jpg: ƒ NO(e, t, n)
.m4a: ƒ loadAudioPlayer(url, options, onComplete)
.mp3: ƒ loadAudioPlayer(url, options, onComplete)
.ogg: ƒ loadAudioPlayer(url, options, onComplete)
.pkm: ƒ parsePKMTex(file, options, onComplete)
.plist: ƒ parsePlist(url, options, onComplete)
.png: ƒ NO(e, t, n)
.pvr: ƒ parsePVRTex(file, options, onComplete)
.skel: ƒ parseArrayBuffer(url, options, onComplete)
.svg: ƒ loadFont(url, options, onComplete)
.tiff: ƒ NO(e, t, n)
.tmx: ƒ parseText(url, options, onComplete)
.tsx: ƒ parseText(url, options, onComplete)
.ttc: ƒ loadFont(url, options, onComplete)
.ttf: ƒ loadFont(url, options, onComplete)
.txt: ƒ parseText(url, options, onComplete)
.vsh: ƒ parseText(url, options, onComplete)
.wav: ƒ loadAudioPlayer(url, options, onComplete)
.webp: ƒ NO(e, t, n)
.woff: ƒ loadFont(url, options, onComplete)
.xml: ƒ parseText(url, options, onComplete)
import: ƒ (e, t, n)

2.10.1 正则匹配定位 function NO

观察这个代码可以知道, 其实就是改改 .src 的值,
好, 那就开改, 先用之前写的简单的 .jpg 的 base64 位字符串来测试下,
对于这个 3300 报错啥的, 不用管,
因为测试的 base64 字符串对应的文件时不存在, 所以才报错的
Error 3300, please go to https://github.com/cocos-creator/engine/blob/develop/EngineErrorMap.md#3300 to see details. Arguments:

[Cocos 3.5.2 引擎注入和魔改]

function NO(e, t, n) {
        console.log("[CC][引擎注入][function NO]", [e.substr(e.lastIndexOf(".")),e, t, n]);
        var i = new Image();
        function r() {
          i.removeEventListener("load", r), i.removeEventListener("error", o), n && n(null, i);
        }
        function o() {
          i.removeEventListener("load", r), i.removeEventListener("error", o), n && n(new Error(B(4930, e)));
        }
        if(e.substr(e.lastIndexOf("."))==".jpg"){
          console.log("[CC][引擎注入][function NO][.jpg]", [e, t, n]);
          var changeBs64Src="data:image/jpeg;base64,/9j/2wBDAAcFBQYFBAcGBgYIBwcICxILCwoKCxYPEA0SGhYbGhkWGRgcICgiHB4mHhgZIzAkJiorLS4tGyIyNTEsNSgsLSz/2wBDAQcICAsJCxULCxUsHRkdLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCz/wAARCAABAAEDASEAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCEA5SL/9k=";
          return "file:" !== window.location.protocol && (i.crossOrigin = "anonymous"), i.addEventListener("load", r), i.addEventListener("error", o), i.src = changeBs64Src, i;
        }else{
          return "file:" !== window.location.protocol && (i.crossOrigin = "anonymous"), i.addEventListener("load", r), i.addEventListener("error", o), i.src = e, i;
        };
      }



2.10.2 开始加密 .jpg 文件

因为魔改过 .jpg 文件的解析部分, 所以这次可以正常执行,
可以看到, 图片已经按照想要的方式加密过了, 现在就只剩下把解密的部分写入代码了

2.10.3 解密 .jpg 文件, 思路解释

从之前的几个步骤操作下来, 可以发现,
function NO 里面的参数 e 就是 .jpg 文件的路径,
所以, 原理就是::=>
=> 读取路径的文件的 base64 格式 =>
=> 代入解密公式 =>
=> 解密出原 base64 =>
=> 传入 src =>

必不可少了解下, 微信开发者文档
获取全局唯一的文件管理器 wx.getFileSystemManager().readFileSync

2.10.4 注入解密代码, 查看效果

如图所示, 注入后, 微信开发工具提示的 3300 报错就消失了,
因为这个 base64 对应的文件是存在的(这个之前困惑了好久 :rofl: :rofl:)

function NO(e, t, n) {
        console.log("[CC][引擎注入][function NO]", [e.substr(e.lastIndexOf(".")),e, t, n]);
        var i = new Image();
        function r() {
          i.removeEventListener("load", r), i.removeEventListener("error", o), n && n(null, i);
        }
        function o() {
          i.removeEventListener("load", r), i.removeEventListener("error", o), n && n(new Error(B(4930, e)));
        };
        var extNAme=e.substr(e.lastIndexOf("."));
        if(extNAme==".jpg"){
          console.log("[CC][引擎注入][function NO][.jpg]", [e, t, n]);
          var changeBs64Src="data:image/jpeg;base64,/9j/2wBDAAcFBQYFBAcGBgYIBwcICxILCwoKCxYPEA0SGhYbGhkWGRgcICgiHB4mHhgZIzAkJiorLS4tGyIyNTEsNSgsLSz/2wBDAQcICAsJCxULCxUsHRkdLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCz/wAARCAABAAEDASEAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABgj/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCEA5SL/9k=";
          var getWxBase64=wx.getFileSystemManager().readFileSync(e, "base64");
          var decodeKey = [537, 683, 791, 1010, 1013, 1094, 1097, 1140, 1144, 1163, 1306, 1429, 1458, 1520, 1584, 1627, 1668, 1770, 1797, 1812, 1829, 1838, 1940, 1944, 2007, 2010, 2124, 2144, 2215, 2216, 2260, 2344, 2363, 2443, 2498, 2504, 2542, 2547, 2647, 2703, 2716, 2753, 2787, 2884, 2885, 2898, 2901, 2929, 3058, 3100, 3126, 3137, 3185, 3258, 3268, 3331, 3379, 3438, 3577, 3829, 3845, 3924, 4082, 4089, 4137, 4214, 4220, 4283, 4340, 4470, 4496, 4497, 4520, 4533, 4807, 4839, 4947, 4958, 4966, 5024, 5097, 5142, 5152, 5174, 5185, 5325, 5376, 5596, 5650, 5660, 5757, 5798, 5852, 5939, 6012, 6027, 6037, 6074, 6136, 6153, 6264, 6291, 6300, 6378, 6382, 6415, 6433, 6487, 6562, 6581, 6582, 6588, 6609, 6737, 6742, 6962, 7014, 7062, 7093, 7319, 7388, 7430, 7513, 7520, 7564, 7639, 7644, 7745, 7761, 7854, 7878, 7911, 7938, 8058, 8078, 8132, 8144, 8216, 8292, 8326, 8389, 8443, 8523, 8559, 8685, 8712, 8776, 8781, 8786, 8810, 8906, 8940, 9091, 9181, 9200, 9230, 9270, 9279, 9331, 9401, 9423, 9482, 9601, 9638, 9668, 9675, 9692, 9850, 9888, 9965, 10048, 10070, 10099, 10202, 10235, 10373, 10419, 10434, 10603, 10624, 10627, 10637, 10825, 10859, 10893, 10901, 11096, 11140, 11304, 11465, 11472, 11533, 11564, 11710, 11734, 11784, 11877, 11886, 12051, 12155, 12162, 12190, 12234, 12283, 12550, 12643, 12691, 12701, 12783, 12979, 13041, 13096, 13112, 13122, 13143, 13158, 13342, 13373, 13407, 13524, 13645, 13741, 13763, 13792, 13981, 13992, 14007, 14047, 14064, 14077, 14098, 14122, 14177, 14199, 14246, 14414, 14575, 14579, 14605, 14643, 14715, 14726, 14745, 14770, 14994, 15119, 15145, 15163, 15420, 15455, 15511, 15541, 15569];
          var matCharCode = "c";
          var replaceEndCode = "+";
          var AllImgTypes = {
            ".png": "image/png",
            ".jpg": "image/jpeg",
          };
          var getImgEnCodeStr = getWxBase64.split("");
          var getTmpInd = 0;
          for (var kk = 0; kk < decodeKey.length; kk++) {
              getTmpInd = decodeKey[kk];
              // 开始解密
              getImgEnCodeStr[getTmpInd] = matCharCode;
          };
          getImgEnCodeStr = getImgEnCodeStr.join("");
          changeBs64Src=`data:${AllImgTypes[extNAme]};base64,`+getImgEnCodeStr;

          return "file:" !== window.location.protocol && (i.crossOrigin = "anonymous"), i.addEventListener("load", r), i.addEventListener("error", o), i.src = changeBs64Src, i;
        }else{
          return "file:" !== window.location.protocol && (i.crossOrigin = "anonymous"), i.addEventListener("load", r), i.addEventListener("error", o), i.src = e, i;
        };
      }


2.10.5 [小总结]

至此, 全流程已经解释完毕,
综上所述, 类似这种就是, 多观察, 多调试, ~~
对了, 每个图片的解密秘钥都不一样的呀, 注意看加密时生成的 JSON 文件

版权声明

本文为原创, 如需转发转载之类的, 请标明来源和原作者, 也可以来咨询下本人

[:stopwatch:] 章后彩蛋 (图片数据隐藏)

下方这个 .jpg 的图片, 如果下载后重命名为 .zip 那就是个加密的压缩包(可打开,密码是cocos),
也可以直接右键点击下方图片另存为 .zip 就可以看到压缩包了,
这个就是把多余数据合并到图片的一个方式,
这个图片的 .zip 包里面就是一个用上面工具加密的图片和生成的对应 JSON 文件,
实现方式很简单的, 就是用的下方的代码里面说的方法,
这个方法用零宽字符写到 ccBinary 里面去了, 供大家思考吧

[零宽加密版-代码-大家可以思考一番]

var ccBinaryZero="实现方法是::[‍‍‌‌‌‍‍​‍‍‌‍‍‍‍​‍‍‍‌‌‌‌​‍‍‍‍‌‌‍​‍‌‌‌‌‌​‍‌‍‍‍‍​‍‍‌‌‌‍‌​‍‌‌‌‌‌​‍‍‌‌‌‌‍​‍‌‍‍‍‌​‍‍‌‍‌‍‌​‍‍‍‌‌‌‌​‍‍‌‌‍‍‍​‍‌‍‌‍‍​‍‍‌‌‌‍‌​‍‌‍‍‍‌​‍‍‍‍‌‍‌​‍‍‌‍‌‌‍​‍‍‍‌‌‌‌​‍‌‌‌‌‌​‍‍‌‌‌‍‍​‍‌‍‍‍‌​‍‍‌‍‌‍‌​‍‍‍‌‌‌‌​‍‍‌‌‍‍‍];";
var ccBinaryOne="实现方法是::[];";
console.log(ccBinaryZero.length,ccBinaryOne.length,ccBinaryZero.length==10);
console.log(ccBinaryZero.length==ccBinaryOne.length);

cocosCreator.zip.jpg

[END]

另外还想请教一下各位大佬:

异或字节加密是否只能加密全部的 Byte 字节 ?

能否只异或加密一部分 Byte ?

之前加密一部分 Byte 发现报错了, 然后解密也没成功 .

3赞

终于写完了, 有点小赶呀 :smile: :smile:
其实抖音小游戏整体流程也差不多 :laughing: :laughing:
其实我觉得, 大家看了这个之后, 应该手动写个 \color{red}{加密解密} 资源的逻辑, 差不多 ??

另外章末彩蛋的控制台打印结果如下,
具体的内容, 如果没有大佬来揭晓的话,
这个会在 征稿活动 V5 结束后解密 , 这个也不是很复杂的 :smile:

image

这里贴一下V5征稿的地址, 方便直达

大佬666

1赞

大佬666

只能说完全没有意义。

大佬,为啥说没有什么意义呢?

因为只要使用了解密。在任意使用到最终数据的地方都能拿到解密后的数据。

1赞

压缩代码展开可以点这个,不需要自己去格式化

1赞

感谢大佬, 不过这个功能倒是很少使用 :joy:

大佬说的有理, 不过稍微加密下的话防止小白用户应该可行 ? :sweat_smile: