- 可用信息变换方法概述
- 加密、哈希和数据打包:CryptEncode
- 数据解密与解压:CryptDecode
加密、哈希和数据打包:CryptEncode
MQL5 中负责数据加密、哈希和压缩的函数是 CryptEncode。它通过指定的方法将所传递源数组 data 的数据转换到目标数组 result 中。
int CryptEncode(ENUM_CRYPT_METHOD method, const uchar &data[], const uchar &key[], uchar &result[])
加密方法还需要传递一个带有私有(秘密)密钥的字节数组 key:其长度取决于具体方法,并在上一节的 ENUM_CRYPT_METHOD 方法表格中指定。如果 key 数组的大小更大,则仍将仅使用所需数量的第一个字节作为密钥。
哈希或压缩不需要密钥,但对于 CRYPT_ARCH_ZIP 有一个注意事项。事实是,终端内置的 "deflate" 算法的实现在结果数据中添加了几个字节以控制完整性:最初的 2 个字节包含 "deflate" 算法的设置,末尾的 4 个字节包含 Adler32 校验和。由于此特性,生成的打包容器与 ZIP 存档为存档的每个单独元素生成的容器不同(ZIP 标准在其头部存储含义相似的 CRC32)。因此,为了能够基于 CryptEncode 函数打包的数据创建和读取兼容的 ZIP 存档,MQL5 允许你使用 key 数组中的特殊值来禁用其自身的完整性检查和额外字节的生成。
uchar key[] = {1, 0, 0, 0};
|
可以使用任何长度至少为 4 字节的密钥。获得的 result 数组可以根据标准 ZIP 格式添加标题(这个问题超出了本书的范围),以创建其他程序可以访问的存档。
函数返回放置在目标数组中的字节数,如果出错则返回 0。错误代码将照常存储在 _LastError 中。
我们使用 CryptEncode.mq5 脚本来检查函数性能。它允许用户输入文本 (Text) 或指定一个文件 (File) 进行处理。要使用文件,你需要清除 Text 字段。
你可以选择一个特定的 Method,或者一次性遍历所有方法,以直观地查看和比较不同的结果。对于这样的审查循环,请在 Method 参数中保留默认值 _CRYPT_ALL。
顺便说一句,为了引入这样的功能,我们再次需要扩展标准枚举(这次是 ENUM_CRYPT_METHOD),但由于 MQL5 中的枚举不能像类那样被继承,这里实际上声明了一个新的枚举 ENUM_CRYPT_METHOD_EXT。这样做的一个额外好处是,我们为元素添加了更友好的名称(在注释中,带有将在设置对话框中显示的提示)。
enum ENUM_CRYPT_METHOD_EXT
|
默认情况下,Text 参数填充了要加密的消息。你可以将其替换为自己的内容。如果我们清除 Text,程序将处理文件。至少有一个参数(Text 或 File)应包含信息。
由于加密需要密钥,其他两个选项允许你直接以文本形式输入它(尽管密钥不必是文本,并且可以包含任何二进制数据,但输入中不支持这些数据),或根据加密方法生成所需的长度。
enum DUMMY_KEY_LENGTH
|
最后,有一个选项 DisableCRCinZIP 用于启用 ZIP 兼容模式,这仅影响 CRYPT_ARCH_ZIP 方法。
input bool DisableCRCinZIP = false; |
为了简化检查方法是否需要加密密钥或是否计算哈希(不可逆的单向转换),定义了 2 个宏。
#defineKEY_REQUIRED(C) ((C) ==CRYPT_DES|| (C) ==CRYPT_AES128|| (C) ==CRYPT_AES256) #defineIS_HASH(C) ((C) ==CRYPT_HASH_MD5|| (C) ==CRYPT_HASH_SHA1|| (C) ==CRYPT_HASH_SHA256) |
OnStart 的开头部分包含了所需变量和数组的描述。
void OnStart()
|
根据 GenerateKey 设置,我们从 CustomKey 字段获取密钥,或者仅仅用单调递增的整数值填充 key 数组。实际上,密钥应该是一个秘密的、非平凡的、任意选择的值块。
if(GenerateKey == DUMMY_KEY_CUSTOM)
|
此处及下文,请注意在 StringToCharArray 之后使用 ArrayResize。务必将数组大小减少 1 个元素,因为如果 StringToCharArray 函数将字符串转换为字节数组时包含了末尾的 0,这可能会破坏程序的预期执行。特别是在这种情况下,我们的秘密密钥中会有一个额外的零字节,如果在接收端没有使用带有类似人为处理的程序,那么它将无法解密消息。这种额外的零也可能影响与数据交换协议的兼容性(如果 MQL 程序与“外部世界”进行某种集成)。
接下来,我们以十六进制格式记录下最终密钥的原始表示:这是由 ByteArrayPrint 函数完成的,该函数曾在 以简化模式读写文件一节中使用过。
if(ArraySize(key))
|
根据 Text 或 File 是否可用,我们用文本字符或文件内容填充 data 数组。
if(StringLen(Text))
|
最后,我们遍历所有方法或使用特定方法执行一次转换。
const int n = (Method == _CRYPT_ALL) ?
|
当我们转换文本时,我们会记录结果,但由于除了 CRYPT_BASE64 方法外,结果几乎总是二进制数据,它们的显示将完全是乱码(说实话,二进制数据不应该被记录,但我们这样做是为了清晰起见)。不可打印的符号和代码大于 128 的符号在不同语言的计算机上显示不同。因此,为了统一所有读者的示例显示,在 CharArrayToString 中形成行时,我们使用显式代码页(1252,西欧语言)。诚然,出版书籍时使用的字体很可能会影响某些字符的显示方式(字体中的字形集可能有限)。
务必要注意,我们仅在显示方法中控制代码页的选择,而 result 数组中的字节不会因此改变(当然,以这种方式获得的字符串不应再发送到任何地方;它仅用于可视化,数据交换应使用 result 本身的字节)。
然而,我们仍然希望为用户提供一些保存加密结果以便以后解码的机会。最简单的方法是使用 CRYPT_BASE64 方法重新转换二进制数据。
关于文件编码,我们只需将结果保存在一个新文件中,其名称是在原始文件名上添加方法名称中最后一个单词的扩展名。例如,将 CRYPT_HASH_MD5 应用于文件 Example.txt,我们将得到输出文件 Example.txt.MD5,其中包含源文件的 MD5 哈希。请注意,对于 CRYPT_ARCH_ZIP 方法,我们将得到一个带有 ZIP 扩展名的文件,但它不是标准的 ZIP 存档(由于缺少包含元信息的头部和目录表)。
我们使用默认设置运行该脚本:它们对应于在循环中检查文本 "Let's encrypt this message" 的所有方法。
CustomKey=My top secret key is very strong / ok Key (bytes): [00] 4D | 79 | 20 | 74 | 6F | 70 | 20 | 73 | 65 | 63 | 72 | 65 | 74 | 20 | 6B | 65 | [16] 79 | 20 | 69 | 73 | 20 | 76 | 65 | 72 | 79 | 20 | 73 | 74 | 72 | 6F | 6E | 67 | Text=Let's encrypt this message / ok StringToCharArray(Text,data,0,-1,CP_UTF8)=26 / ok - 0 CRYPT_BASE64, key required: false CryptEncode(method,data,key,result)=36 / ok TGV0J3MgZW5jcnlwdCB0aGlzIG1lc3NhZ2U= [00] 54 | 47 | 56 | 30 | 4A | 33 | 4D | 67 | 5A | 57 | 35 | 6A | 63 | 6E | 6C | 77 | [16] 64 | 43 | 42 | 30 | 61 | 47 | 6C | 7A | 49 | 47 | 31 | 6C | 63 | 33 | 4E | 68 | [32] 5A | 32 | 55 | 3D | - 1 CRYPT_AES128, key required: true CryptEncode(method,data,key,result)=32 / ok ¯T* Ë[3hß Ã/-C }¬ŠÑØN¨®Ê† ‡Ñ [00] 01 | 0B | AF | 54 | 2A | 12 | CB | 5B | 33 | 68 | DF | 0E | C3 | 2F | 2D | 43 | [16] 19 | 7D | AC | 8A | D1 | 8F | D8 | 4E | A8 | AE | CA | 81 | 86 | 06 | 87 | D1 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok Try to decode this with CryptDecode.mq5 (CRYPT_AES128): base64:'AQuvVCoSy1szaN8Owy8tQxl9rIrRj9hOqK7KgYYGh9E=' - 2 CRYPT_AES256, key required: true CryptEncode(method,data,key,result)=32 / ok ø‘UL»ÉsëDC‰ô ¬.K)ŒýÁ LḠ+< !Dï [00] F8 | 91 | 55 | 4C | BB | C9 | 73 | EB | 44 | 43 | 89 | F4 | 06 | 13 | AC | 2E | [16] 4B | 29 | 8C | FD | C1 | 11 | 4C | E1 | B8 | 05 | 2B | 3C | 14 | 21 | 44 | EF | CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok Try to decode this with CryptDecode.mq5 (CRYPT_AES256): base64:'+JFVTLvJc+tEQ4n0BhOsLkspjP3BEUzhuAUrPBQhRO8=' - 3 CRYPT_DES, key required: true CryptEncode(method,data,key,result)=32 / ok µ b &“#ÇÅ+ýº'¥ B8f¡rØ-Pè<6âì‚Ë£ [00] B5 | 06 | 9D | 62 | 11 | 26 | 93 | 23 | C7 | C5 | 2B | FD | BA | 27 | A5 | 10 | [16] 42 | 38 | 66 | A1 | 72 | D8 | 2D | 50 | E8 | 3C | 36 | E2 | EC | 82 | CB | A3 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok Try to decode this with CryptDecode.mq5 (CRYPT_DES): base64:'tQadYhEmkyPHxSv9uielEEI4ZqFy2C1Q6Dw24uyCy6M=' - 4 CRYPT_HASH_SHA1, key required: false CryptEncode(method,data,key,result)=20 / ok §ßö*©ºø �|)bËbzÇÍ Û� [00] A7 | DF | F6 | 2A | A9 | BA | F8 | 0A | 80 | 7C | 29 | 62 | CB | 62 | 7A | C7 | [16] CD | 0E | DB | 80 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=28 / ok Try to decode this with CryptDecode.mq5 (CRYPT_HASH_SHA1): base64:'p9/2Kqm6+AqAfCliy2J6x80O24A=' - 5 CRYPT_HASH_SHA256, key required: false CryptEncode(method,data,key,result)=32 / ok ÚZ2š�»”¾7 �… ñÄÁ´˜¦“ome2r@¾ô®³” [00] DA | 5A | 32 | 9A | 80 | BB | 94 | BE | 37 | 0C | 80 | 85 | 07 | F1 | 96 | C4 | [16] C1 | B4 | 98 | A6 | 93 | 6F | 6D | 65 | 32 | 72 | 40 | BE | F4 | AE | B3 | 94 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok Try to decode this with CryptDecode.mq5 (CRYPT_HASH_SHA256): base64:'2loymoC7lL43DICFB/GWxMG0mKaTb21lMnJAvvSus5Q=' - 6 CRYPT_HASH_MD5, key required: false CryptEncode(method,data,key,result)=16 / ok zIGT… Fû;―3þèå [00] 7A | 49 | 47 | 54 | 85 | 1B | 7F | 11 | 46 | FB | 3B | 97 | 33 | FE | E8 | E5 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=24 / ok Try to decode this with CryptDecode.mq5 (CRYPT_HASH_MD5): base64:'eklHVIUbfxFG+zuXM/7o5Q==' - 7 CRYPT_ARCH_ZIP, key required: false CryptEncode(method,data,key,result)=34 / ok x^óI-Q/VHÍK.ª,(Q(ÉÈ,VÈM-.NLO [00] 78 | 5E | F3 | 49 | 2D | 51 | 2F | 56 | 48 | CD | 4B | 2E | AA | 2C | 28 | 51 | [16] 28 | C9 | C8 | 2C | 56 | C8 | 4D | 2D | 2E | 4E | 4C | 4F | 05 | 00 | 80 | 07 | [32] 09 | C2 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=48 / ok Try to decode this with CryptDecode.mq5 (CRYPT_ARCH_ZIP): base64:'eF7zSS1RL1ZIzUsuqiwoUSjJyCxWyE0tLk5MTwUAgAcJwg==' |
在这种情况下,该密钥的长度足以满足所有三种加密方法的需求,而其他不需要密钥的方法则直接忽略它。因此,所有函数调用均已成功完成。
在下一节中,我们将学习如何解码加密内容,并可以检查 CryptDecode 函数是否返回原始消息。请注意这部分日志。
启用的 DisableCRCinZIP 选项将使 CRYPT_ARCH_ZIP 方法的结果减少几个开销字节。
- 7 CRYPT_ARCH_ZIP, key required: false CryptEncode(method,data,key,result)=28 / ok óI-Q/VHÍK.ª,(Q(ÉÈ,VÈM-.NLO [00] F3 | 49 | 2D | 51 | 2F | 56 | 48 | CD | 4B | 2E | AA | 2C | 28 | 51 | 28 | C9 | [16] C8 | 2C | 56 | C8 | 4D | 2D | 2E | 4E | 4C | 4F | 05 | 00 | CryptEncode(CRYPT_BASE64,result,dummy,readable)=40 / ok Try to decode this with CryptDecode.mq5 (CRYPT_ARCH_ZIP): base64:'80ktUS9WSM1LLqosKFEoycgsVshNLS5OTE8FAA==' |
现在,我们将编码实验转移到文件上。为此,再次运行脚本并清除 Text f字段中的文本。结果,程序将多次处理 MQL5Book/clock10.htm 文件,并将创建几个具有不同扩展名的派生文件。
File=MQL5Book/clock10.htm / ok
|
你可以从文件管理器中查看所有文件的内部,并确保与原始内容已无共同之处。许多文件管理器都有计算哈希和的命令或插件,以便可以将它们与打印到日志中的 MD5、SHA1 和 SHA256 十六进制值进行比较。
如果我们尝试编码文本或文件而未提供正确长度的密钥,我们将收到 INVALID_ARRAY(4006) 错误。例如,对于默认的文本消息,我们在 method 参数中选择 AES256(需要 32 字节的密钥)。使用 GenerateKey 参数,我们预订一个长度为 16 字节的密钥(或者你可以部分或完全删除 CustomKey 字段中的文本,保留 GenerateKey 为默认值)。
Key (bytes):
|
你还可以使用 CRYPT_ARCH_ZIP 方法或使用常规压缩软件压缩同一个文件(就像我们对 clock10.htm 所做的那样)。如果你稍后使用二进制查看器实用程序(通常内置于文件管理器中)查看,那么两个结果都将显示一个共同的打包块,差异仅在于围绕它的元数据。
使用 CRYPT_ARCH_ZIP 方法压缩的文件(左)与包含它的标准 ZIP 存档(右)的比较
它显示存档的中间和主要部分是一个字节序列(以深色突出显示),与 CryptEncode 函数产生的字节序列相同。
最后,我们将展示图形文件 clock10.png 的 Base64 文本表示是如何生成的。为此,清除字段 Text 并在 File 参数中写入 MQL5Book/clock10.png。在下拉列表 Method 中选择 Base64。
File=MQL5Book/clock10.png / ok
|
结果创建了 clock10.png.BASE64 文件。在其中,我们将看到插入到网页代码中 img 标签里的那一行文本。
顺便说一句,"deflate" 压缩方法是 PNG 图形格式的基础,因此我们可以使用 CryptEncode 将资源位图保存为 PNG 文件。随书附带了头文件 PNG.mqh,其中包含描述图像所需内部结构的最小支持:建议独立试验其源代码。使用 PNG.mqh,我们编写了一个简单的脚本 CryptPNG.mq5,它将终端附带的 "euro.bmp" 文件中的资源转换为 "my.png" 文件。未实现 PNG 文件的加载。
#resource "\\Images\\euro.bmp"
|