【RPG Maker MV插件编程】【实例教程6】存档的加密解密与保护

  • 作者:Mandarava(鳗驼螺)
  • 微博:@鳗驼螺pro

这篇文章前半部分将研究MV游戏的存档、读档过程,从而实现一个MV游戏存档修改器。后半部分则是实现一个防止存档被修改的MV存档保护插件。

找出MV存档和读档的方式

DataManager 类用于管理数据库和游戏对象,包括游戏的存档、读档。DataManager 使用DataManager.saveGame() 方法来存档,用DataManager.loadGame() 方法来读档。在存档过程中,它会实际调用DataManager.saveGameWithoutRescue() 来保存存档数据。看一下这个方法的具体实现:

DataManager.saveGameWithoutRescue = function(savefileId) {
    var json = JsonEx.stringify(this.makeSaveContents());
    if (json.length >= 200000) {
        console.warn('Save data too big!');
    }
    StorageManager.save(savefileId, json);
    this._lastAccessedId = savefileId;
    var globalInfo = this.loadGlobalInfo() || [];
    globalInfo[savefileId] = this.makeSavefileInfo();
    this.saveGlobalInfo(globalInfo);
    return true;
};

首先,它会先用DataManager.makeSaveContents() 方法将需要存入存档的数据(包括 $gameSystem,$gameScreen,$gameTimer,$gameSwitches,$gameVariables,$gameSelfSwitches,$gameActors,$gameParty,$gameMap,$gamePlayer 等10个全局变量的数据)合并成一个对象contentsDataManager.makeSaveContents的实现代码如下:

DataManager.makeSaveContents = function() {
    // A save data does not contain $gameTemp, $gameMessage, and $gameTroop.
    var contents = {};
    contents.system       = $gameSystem;
    contents.screen       = $gameScreen;
    contents.timer        = $gameTimer;
    contents.switches     = $gameSwitches;
    contents.variables    = $gameVariables;
    contents.selfSwitches = $gameSelfSwitches;
    contents.actors       = $gameActors;
    contents.party        = $gameParty;
    contents.map          = $gameMap;
    contents.player       = $gamePlayer;
    return contents;
};

然后使用JsonEx.stringify 方法将这个对象进行json序列化转换成json字符串。(说句题外话,从这里也可以看出,如果我们要保存自定义的变量、数据到存档中,只需要以属性的方式添加给这10个全局对象中的任意一个即可,非常简单。)然后再调用StorageManager.save(savefileId, json) 方法将json字符串保存到存档文件中(在读档时,这个json字符串会被反序列化成那10个全局对象)。

再看一下StorageManager.save 方法的实现(如下面的代码)。对于本地数据,它会实际调用saveToLocalFile 方法去保存数据。

StorageManager.save = function(savefileId, json) {
    if (this.isLocalMode()) {
        this.saveToLocalFile(savefileId, json);
    } else {
        this.saveToWebStorage(savefileId, json);
    }
};

下面的代码是StorageManager.saveToLocalFile 方法的实现。在正式保存前它会用LZString.compressToBase64 方法将json字符串编码成Base64字符串。

StorageManager.saveToLocalFile = function(savefileId, json) {
    var data = LZString.compressToBase64(json);
    var fs = require('fs');
    var dirPath = this.localFileDirectoryPath();
    var filePath = this.localFilePath(savefileId);
    if (!fs.existsSync(dirPath)) {
        fs.mkdirSync(dirPath);
    }
    fs.writeFileSync(filePath, data);
};

类似的,对于读档过程,我们最终也会追踪到一个类似的方法,StorageManager.loadFromLocalFile 方法。在这个方法里,它会将存档中的内容使用LZString.decompressFromBase64 方法来还原成json字符串。

StorageManager.loadFromLocalFile = function(savefileId) {
    var data = null;
    var fs = require('fs');
    var filePath = this.localFilePath(savefileId);
    if (fs.existsSync(filePath)) {
        data = fs.readFileSync(filePath, { encoding: 'utf8' });
    }
    return LZString.decompressFromBase64(data);
};

所以,实际上MV的存档内容就是使用LZString.compressToBase64 方法编码过的Base64字符串,而存档的解密方法就是用LZString.decompressFromBase64 方法进行反向解码操作。

制作MV存档的修改器

经过以上分析,现在只需要将LZString 的代码复制出来,简单的用HTML+Javascript技术就能做出一个MV存档的解密、加密工具,这个工具我放在github上,有兴趣的可以从 这里 下载。
用这个工具来测试一下MV的存档数据,效果如下图,真实数据都被解密出来了,只需要将真实数据进行一下修改,然后再重新加密,将加密的内容复制回存档保存就完成了存档的修改。

MV存档测试

如何保护存档?

为防止存档被随意修改,可以对存档内容进行加密,在读档时也要相应的作解密操作。通过分析,进行加密操作的最佳位置是在DataManager.saveGameWithoutRescue 方法中进行,当全局对象被序列化成json字符串后,立即对json字符串进行加密。而解密过程相应的放在DataManager.loadGameWithoutRescue 中进行。LZString的作用是对字符串进行压缩,当然你也可以只重写LZString.compressToBase64LZString.decompressFromBase64方法,在实现压缩/还原的时候同时实现字符串的加密与解密,本质上没有差别,但直接修改LZString 影响面会比较广,所有调用这二个方法的代码都会有影响,包括global.rpgsave 的数据也会被加密。

制作一个存档保护插件

接下来就来制作一个存档保护插件。这里只需要重写DataManager.saveGameWithoutRescue方法,实现json字符串加密,重写DataManager.loadGameWithoutRescue方法,实现json字符串的解密还原即可。完整的代码如下(本插件的最新版本可以在这里下载)。其中encryptdecrypt方法是字符串的加密、解密方法。加密时,它会先对json字符串先进行一次LZString压缩,然后用凯撒加密算法(本算法修改自 这里)对压缩过的字符串进行加密,解密时就是反向操作。凯撒加解密算法简单、强度不高,好处是不会增加字符串长度,这里 还有个相对高强度的版本,可以设定字符串密码,但缺点是会增加存档内容的长度。你也可以用自己的算法(比如DES, AES等)来代替(PS:如果要更换算法,注意验证算法是否支持对中文的加密解密,如果不支持中文,你可以像这里一样先用LZString对它进行一次压缩操作)。

//==============================
// MND_ProtectProfile2.js
// Copyright (c) 2017 Mandarava
// Homepage: www.popotu.com
//==============================

/*:
 * @plugindesc 用于加密存档的插件,可指定加密密码。(v1.0)
 * @author Mandarava(鳗驼螺)
 * @version 1.0
 *
 * @param Password
 * @text 存档密码
 * @desc 任意数字,通常取0~26之间的数字。
 * @type Number
 * @default 66
 *
 * @help
 * 使用时请修改存档密码,不要使用默认值哦!
 * 本插件采用凯撒加密算法,强度较低,好处是不会增加存档内容长度。可以采取的提高
 * 算法强度的方法,包括:对几偶数上的字符采用不同的偏移量,在特定位置添加混淆字
 * 符或字符串等。要使用加密强度较高的版本请使用 MND_ProtectProfile.js 插件。
 *
 * by Mandarava(鳗驼螺)
 */

(function($){

    var params=PluginManager.parameters("MND_ProtectProfile2");
    var password=Number(params["Password"]) || 66;

    DataManager.saveGameWithoutRescue = function(savefileId) {
        var json = JsonEx.stringify(this.makeSaveContents());
        if (json.length >= 200000) {
            console.warn('Save data too big!');
        }
        json=encrypt(json, password); //对json字符串进行加密
        StorageManager.save(savefileId, json);
        this._lastAccessedId = savefileId;
        var globalInfo = this.loadGlobalInfo() || [];
        globalInfo[savefileId] = this.makeSavefileInfo();
        this.saveGlobalInfo(globalInfo);
        return true;
    };

    DataManager.loadGameWithoutRescue = function(savefileId) {
        var globalInfo = this.loadGlobalInfo();
        if (this.isThisGameFile(savefileId)) {
            var json = StorageManager.load(savefileId);
            json=decrypt(json, password); //对加密过的json字符串进行解密
            this.createGameObjects();
            this.extractSaveContents(JsonEx.parse(json));
            this._lastAccessedId = savefileId;
            return true;
        } else {
            return false;
        }
    };

    //===字符串加密解密算法=========
    //凯撒加密算法改自:https://github.com/bukinoshita/caesar-encrypt
    function numToChar(num){
        return String.fromCharCode(97 + num);
    }
    function charToNum(char){
        return char.charCodeAt(0) - 97;
    }
    function caesar(char, shift){
        return numToChar(charToNum(char) + (shift % 26));
    }
    function caesarDec(char, shift){
        return numToChar(charToNum(char) - (shift % 26));
    }
    function encryptByCaesar(value, shift){
        var letters = value.split('');
        return letters.map(function (letter) { return caesar(letter, shift); }).join("");
    }
    function decryptByCaesar(value, shift){
        var letters = value.split('');
        return letters.map(function (letter) { return caesarDec(letter, shift); }).join("");
    }

    /**
     * 加密字符串
     * @param text 要加密的字符串
     * @param shift 解密密码(任意数字,通常取0~26之间的数字)
     * @returns {*}
     */
    function encrypt(text, shift) {
        var result=LZString.compressToBase64(text);
        result=encryptByCaesar(result, shift);
        return result;
    }

    /**
     * 解密字符串
     * @param text 要解密的字符串
     * @param shift 解密密码(任意数字,通常取0~26之间的数字)
     */
    function decrypt(text, shift) {
        var result=decryptByCaesar(text, shift);
        result=LZString.decompressFromBase64(result);
        return result;
    }
    //===========================

})();

现在,可以运行一下游戏,然后保存游戏,退出游戏再加载游戏,一切都没有问题,说明存档、读档都是正常的。然后,再用前面做的MV存档修改工具测试一下存档数据是否能被解密。在开发期间,存档会保存到[项目目录]\save 文件夹下,用记事本打开该文件夹下的名称类似file1.rpgsavefile2.rpgsave 的存档文件,复制其内容,粘贴到存档修改工具的密文框中,点击“解密”,解出来的数据仍然是加过密的字符串,根本无法修改。这样,这个存档保护插件就完成了。

存档解密测试

PS:在DataManager.saveGame 方法中,在存档时,如果玩家是以覆盖旧存档的方式进行新存档的,那么MV会使用StorageManager.backup 方法对被覆盖的旧存档进行一次备份,以便在存档失败时通过StorageManager.restoreBackup 方法恢复。在StorageManager.backup 方法中看似对存档数据又进行了一次LZString.compressToBase64压缩,但际上它在使用StorageManager.loadFromLocalFile 方法读取旧存档数据时,那个方法会对数据进行一次LZString.decompressFromBase64解压。所以,二相抵消,实际上它并没有改变任何数据。所以StorageManager.backupStorageManager.restoreBackup方法不需要重写。

by Mandarava(鳗驼螺)2017.08.15

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容