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

Author Avatar
空気浮遊 2021年01月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 初心者。如果文章有任何错误欢迎在下方指出。)