Gamemaker Studio 2 存档系统/安全设计指北

Author Avatar
くきふゆ 1月19日
  • 在其它设备中阅读本文章

数据结构嵌套存储、加密、编码、混淆。不得不说GMS2是个十分方便却傻逼的工具。

基本存档引入

使用映射表数据结构,即 ds_map 。对 ds_map 添加键值之后,可以将 ds_map 存储在文件中。

GMS2提供两个基础函数ds_map_write()ds_map_secure_save()将映射表存储在字符串或文件中。

ds_map_write

ini_open("map.ini");
var t_string;
t_string = ds_map_write(inventory);
ini_write_string("Saved", "0", t_string);
ini_close();

上面的例子中,函数返回一个人类不可读的字符串(数据结构内存中内容),然后将其存储在文件map.ini中。

ds_map_secure_save

ds_map_secure_save(purchase_map, "p_data.dat")

上面的例子中,函数将purchase_map用“安全”的方式“加密”存储在p_data.dat中,使得文件不可被修改。

ds_map_secure_save 并没有想象中的安全。参见一个旧主题,函数的实现方式为将一个二进制hash码与该map的JSON纯文本形式接在一起用base64编码储存。二进制hash码或为阻止文件被修改或文件被跨设备移动。

与上方两个函数相对的也有ds_map_secure_loadds_map_read。这部分请自行查阅GMS2文档。

HMAC-SHA1

HMAC是哈希运算消息认证码 (Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。HMAC-SHA1签名算法是一种常用的签名算法,用于对一段信息进行生成签名摘要。

HMAC-SHA1或许能够成为理想的防止文件被修改的哈希算法。你可以在这里下载到可用于GMS2的hmac_sha1版本的ds_map_secure_save。(你可以在这里访问发布者的原帖)

注意到其中的函数string_build(),给定数个参数后返回将其拼接在一起的字符串。主要是为了避免字符串参数在生成的程序内以明文方式储存。

实现方式大体与ds_map_secure_save类似,获取ds_map的字符串形式,将其用HMAC-SHA1方式哈希后,输出哈希码和未经混淆的映射表字符串。

官方也有一篇使用HMAC-SHA1来保护存档的教程,你可以在这里查阅到。

文件加密/混淆

RC4

在密码学中,RC4(来自Rivest Cipher 4的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法。RC4是有线等效加密(WEP)中采用的加密算法,也曾经是TLS可采用的算法之一。 由美国密码学家罗纳德·李维斯特(Ronald Rivest)在1987年设计的。由于RC4算法存在弱点,2015年2月所发布的 RFC 7465 规定禁止在TLS中使用RC4加密算法。 RC4由伪随机数生成器和异或运算组成。RC4的密钥长度可变,范围是$[1,255]$。RC4一个字节一个字节地加解密。给定一个密钥,伪随机数生成器接受密钥并产生一个S盒。S盒用来加密数据,而且在加密过程中S盒会变化。
由于异或运算的对合性,RC4加密解密使用同一套算法。

RC4 用于游戏存档中或许是一个方便的选择(虽然该算法在多次传输重复内容的情况下已经不再安全)。

你可以访问这个github页面来获取RC4在GMS2下的可用版本。将你想要存储的字符串先用RC4方式加密,再用HMAC-SHA1以防止修改文件即可。但这里提供的RC4实现事实上效率较为低下。你可以手动修改其源代码,将取模运算更改为位运算,并使用外置DLL来避免GMS的傻逼效率问题。

我修改了上方提供的github页面中的RC4在GML下的实现,供参考:

/**
Encrypt the buffer in-place using RC4.
*/

function rc4(buffer, key, offset, length){
    var i, j, k, s, temp, keyLength, pos;
    keyLength = string_byte_length(key);
    for (i = 255; i >= 0; --i) {
        s[i] = i;
    }
    j = 0;
    k = 0;
    for (i = 0; i <= 255; ++i) {
        j = (j + s[i] + string_byte_at(key, k)) & ((1<<8)-1);
        temp = s[i];
        s[i] = s[j];
        s[j] = temp;
        k = (k+1==keyLength) ? 0 : k+1;
    }
    i = 0;
    j = 0;
    pos = 0;
    buffer_seek(buffer, buffer_seek_start, offset);
    var currentByte;
    repeat (length) {
        i = (i+1) & ((1<<8)-1);
        j = (j+s[i]) & ((1<<8)-1);
        temp = s[i];
        s[i] = s[j];
        s[j] = temp;
        currentByte = buffer_peek(buffer, buffer_tell(buffer), buffer_u8);
        buffer_write(buffer, buffer_u8, s[(s[i]+s[j]) & ((1<<8)-1)] ^ currentByte);
    }
    buffer_seek(buffer, buffer_seek_start, offset);
}

AES

高级加密标准(英语:Advanced Encryption Standard,缩写:AES),又称Rijndael加密法(荷兰语发音:[ˈrɛindaːl],音似英文的“Rhine doll”),是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。现在,高级加密标准已然成为对称密钥加密中最流行的算法之一。

与RC4同理,但比RC4要更加安全。GMS2上AES目前没有很好的免费支持。你可以在YYG的Marketplace中搜索AES,目前提供了三个相关加密插件。由于我完全是密码学未入门... 基于免费的AES For Gamemaker的低效率和不明晰的bug,这可能并不适合用于含大量内容的加密(如存档文件)。

混淆

如果无需高强度的加密方式,你也可以自己创造一些加密与混淆方式。

常用的简单加密/混淆可以是异或加密、凯撒密码或维吉尼亚密码等,简单且较易于实现。它虽然不能有效保护存档的不可读性,但足以阻止大部分玩家轻易读取到存档内容。配合HMAC-SHA1等方式则可以达成有效保护存档的目的。

数据结构嵌套存储

比如,ds_map里套ds_list,ds_map里套ds_map。

一个显然的事实是,ds_map中储存的ds_map是一个数字,即ds_map被临时分配的一个独有ID。所以如果直接将ds_map不加修饰地写入文件(如ds_map_write()),那么存储下来的ID并不包含map中的内容,在下一次读取的时候只会读取到一个已经没有什么用处的ID,或者出现更加不可预料的后果。

GMS2 提供了几种可以配合JSON格式编码的函数,如ds_map_add_map()ds_map_add_list()。如果你使用了这个函数,数据结构的ID将会被存储到ds_map,并打上对应数据结构的标记,表明这个ID的数据结构类型。如果你想在list中套map或list,那你也可以使用ds_list_mark_as_map()ds_list_mark_as_list()来手动为list的某一部分打上标记。

对你想要储存的map使用函数json_encode(map)进行JSON格式编码,它会将其中被打上标记的数据结构递归编码并返回一个字符串。

对你想要读取的JSON使用函数json_decode(),它会自动将JSON解码成包含一系列子map和list的map。

注意:如果你在读取存档之后想要对临时用map释放内存,注意map的释放内存会释放map中包含的子map和list的内存,因此如果想要避免这种情况,将包含子map的id改为undefined等无关内容。

值得一提的是,ds_map_secure_save()使用的已经是JSON格式的map,因此支持递归存储。如果想使用自己的hash和加密方式,那就可以使用json_encode()json_decode()

JSON 官方文档解释

JSON (JavaScript Object Notation) 是一个易于人或机器读写的轻量数据交换结构。它基于两种基本结构:

  • 一对键与值的配对,在GMS中也被称作ds_map,或“映射”、“词典”。
  • 一个值的有序序列,在GMS中也被称作ds_list,或“列表”、“数组”。

最后

建议使用YYC(Yoyo Compiler)以起到更好的反编译效果和更高的加密效率。

以及GMS2真是个方便又傻逼的工具。

(GMS2初心者。如果文章有任何错误欢迎在下方指出。)

本文链接:https://acxblog.site/archives/gms2-save-games.html
博客以 CC BY-NC-SA 4.0 协议进行许可。