Appearance
密码学
算法交易出现在交易所交易与信息技术的交叉领域,一方面,它能够连接越来越多的新市场参与交易,另一方面,也拓展了交易平台的功能。密码学,或者更广义地说,信息安全,是一种已经渗透到包括交易员工具库在内的大多数活动领域的技术趋势。
MQL5 提供了用于数据加密、哈希处理和压缩的函数:CryptEncode
和 CryptDecode
。我们在本书的一些示例中已经使用过它们:在脚本 EnvSignature.mq5
(将程序绑定到运行时属性)和服务程序 ServiceAccount.mq5
(服务)中。
在本章中,我们将更详细地讨论这些函数。然而,在直接描述它们之前,让我们先回顾一下信息转换方法:这一编程方向的内容非常广泛,而 MQL5 仅支持其中一部分标准。这个标准列表在未来可能会得到扩展,但目前,如果你在帮助文档中找不到所需的加密方法,可以尝试在 mql5.com 网站上(在文章部分或源代码数据库中)寻找现成的实现方案。
可用信息转换方法概述
信息保护可以出于不同的目的来实现,因此会使用不同的方法。特别是,可能需要向外部观察者完全隐藏信息的本质,或者确保信息的传输,同时保证其状态不变,并且信息本身仍然可用。在第一种情况下,我们讨论的是加密,而第二种情况则涉及数字指纹(哈希)。因此,加密和哈希归结为在必要时使用额外参数将原始数据处理为新的表示形式。
加密和哈希都有多种类型。
最常见的分类是将加密分为公钥(非对称)加密和私钥(对称)加密。
非对称加密方案意味着数据交换的每个参与者都有两个密钥——公钥和私钥。公钥和私钥对是使用特殊算法预先生成的。每个私钥仅为其所有者所知。每个人的公钥是公开的。在传输加密数据之前,需要以某种方式交换公钥。接下来,数据提供者使用其仅自己知道的私钥,结合数据接收者的一个或多个公钥进行加密。而接收者则使用他们的私钥和发送者的公钥进行解密。
对称加密方案在加密和解密时使用相同的秘密(私钥)密钥。
MQL5 支持现成的私钥功能(对称加密)。内置的 MQL5 工具不提供使用非对称加密的电子签名功能。
在加密方法中,无密钥的基本算法也很突出。借助这些算法,用户可以实现信息的条件隐藏或信息类型的转换。例如,其中包括 ROT13(将字符的字母数字代码偏移 13 位进行替换,特别是在 Windows 注册表中使用)或 Base64(将二进制文件转换为文本,反之亦然,通常在 Web 项目中使用)。另一个流行的数据转换任务是数据压缩。从某种意义上说,这也可以被视为加密,因为数据会变得无法被人类或应用程序读取。
哈希方法也有很多种。循环冗余校验(CRC)可能是最著名和最简单的一种。与加密不同,加密允许从加密后的消息恢复原始消息,而哈希只是根据原始信息创建一个指纹(一组特征字节),这样在后续重新计算时,其不变的状态(在很大概率上)保证了原始信息的不变性。当然,这是假设信息对相应软件系统的所有参与者/用户都是可用的。通过哈希是不可能恢复信息的。通常,哈希的大小(其中的字节数)对于每种方法都是有限制且标准化的,所以对于一个 80 个字符长的字符串和一个 1MB 的文件,我们会得到相同大小的哈希值。大多数用户会发现哈希在网站和程序对密码的哈希处理方面非常有用,即密码以哈希形式存储,并在登录时使用密码哈希进行验证,而不是使用原始密码。
需要注意的是,我们在上一章中已经遇到了“哈希”这个术语:我们使用哈希函数为经济日历结构建立索引。那个简单的哈希保护程度非常弱,特别是表现为碰撞(不同数据的结果巧合)的概率很高,我们在算法中专门处理了这个问题。它适用于在有限数量的“篮子”上对数据进行均匀伪随机分布的问题。相比之下,工业标准的哈希方法特别注重验证信息的完整性,并使用更为复杂的计算方法。但在这种情况下,哈希的长度是几十字节,而不是单个数字。
MQL 程序可用的信息加密和哈希方法都收集在 ENUM_CRYPT_METHOD
枚举中:
常量 | 描述 |
---|---|
CRYPT_BASE64 | Base64 重新编码 |
CRYPT_DES | 使用 56 位(7 字节)密钥的 DES 加密 |
CRYPT_AES128 | 使用 128 位(16 字节)密钥的 AES 加密 |
CRYPT_AES256 | 使用 256 位(32 字节)密钥的 AES 加密 |
CRYPT_HASH_MD5 | MD5 哈希计算(16 字节) |
CRYPT_HASH_SHA1 | SHA1 哈希计算(20 字节) |
CRYPT_HASH_SHA256 | SHA256 哈希计算(32 字节) |
CRYPT_ARCH_ZIP | 使用“deflate”方法进行压缩 |
上述枚举用于两个加密 API 函数——CryptEncode
(加密/哈希)和 CryptDecode
(解密)。我们将在以下章节中讨论它们。
AES 和 DES 加密方法除了数据之外,还需要加密密钥——一个预定义长度的字节数组(在表格中括号内标明)。如前所述,密钥必须保密,并且仅为程序开发者或信息所有者所知。加密的密码强度,即攻击者的计算机猜测密钥的难度,直接取决于密钥的大小:密钥越大,保护就越可靠。因此,DES 被认为已经过时,在金融领域已被其改进版本三重 DES 所取代:它意味着使用三个不同的密钥连续应用 DES 三次,这在 MQL5 中很容易实现。有一种流行的三重 DES 版本,在第二次迭代时进行解密而不是使用密钥 2 进行加密,也就是说,在最终的第三次 DES 轮之前,将数据恢复到一个中间的、故意错误的表示形式。但三重 DES 也计划在 2024 年之后从行业标准中移除。
同时,密码强度应该与秘密(密钥和信息)的有效期相适应。如果需要快速传输安全消息,定期更新的较短密钥将提供更好的性能。
在哈希方法中,最现代的是 SHA256(SHA-2 标准的一个子集)。SHA1 和 MD5 方法被认为是不安全的,但为了与现有服务兼容,仍然被广泛使用。对于哈希方法,括号内标明了生成的带有数据数字指纹的字节数组的大小。哈希不需要密钥,但在许多应用中,会在哈希数据中附加“盐”——一个秘密组件,这使得攻击者难以重现所需的哈希值(例如,在猜测密码时)。
CRYPT_ARCH_ZIP
元素提供 ZIP 归档以及在互联网上的数据请求传输/接收(请参阅 WebRequest
)。
尽管该方法的名称中包含 ZIP,但压缩后的数据并不等同于通常的 ZIP 归档文件,通常的 ZIP 归档文件除了“deflate”容器外,总是包含元数据:特殊的头部、文件列表及其属性。在 mql5.com 网站的文章和源代码库中,你可以找到将文件压缩成 ZIP 归档文件以及从中提取文件的现成实现。压缩和解压缩是通过 CryptEncode/CryptDecode
函数执行的,并且所有额外必要的 ZIP 格式结构都在 MQL5 代码中进行了描述和填充。
Base64 方法旨在将二进制数据转换为文本,反之亦然。二进制数据通常包含许多不可打印的字符,并且不被编辑和输入工具(如 MQL 程序属性对话框中的输入变量)所支持。例如,在处理流行的 JSON 对象数据交换文本格式时,Base64 可能会很有用。
每 3 个原始字节在 Base64 中编码为 4 个字符,导致数据大小增加三分之一。本书附带了测试文件,我们将在以下示例中使用它们进行实验,特别是网页 MQL5/Files/MQL5Book/clock10.htm
以及其中使用的带有时钟图像的文件 MQL5/Files/MQL5Book/clock10.png
。在这个入门阶段,你可以清楚地看到二进制数据和 Base64 文本在内部表示上的可能性和差异,同时外观保持相同。
嵌入二进制图像和 Base64 格式的网页
嵌入二进制图像和 Base64 格式的网页
同一个带有钟面的图像既作为外部文件 clock10.png
插入到页面中,也以其 Base64 编码形式插入到 img
标签中(在其 src
属性中:这就是“数据 URL”)。直接在网页文本中,它看起来像这样(不一定需要将长的 Base64 字符串按 76 个字符的宽度换行,但标准允许这样做,这里是为了便于发布):
html
<img src="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAQAAAD8x0bcAAAAAXNSR0IArs4c6QAAAAJiS0dEAP+
Hj8y/AAABdUlEQVQoz1WSPUiVYRiGr/f4qaRwyAMOLpVnSGuvBh3SoSnLwclwqQa3CNxaWxwcrS
D/CAQH4ShF4RBnqCWQWiQwAp1C6WBi4B/5Xg7f1+fxgXd4eC5e7ue+nxAFgADABj+Bq1wGIBsRU
ihwwCMa6eE6ga+s0sgLmv5j0ahOOOqxqq98q+qeI86o0ShRfWpFHVC105uq3lOnfa5G0UXn1Ji9
8bw7Ucf9pBbgNX9ZI2TijzjMRBf4zCWeAckbxuijgXV2aadMfd2gl4QVkhXmAegiUGKDlhyRZmC
IxyRFdomUgB1qlDlmtu6vbYqckES+U6SEtNFGjScMcUhz5tAmrTTAiGZupI79cv9crw8sXGM9Dw
WkgwtncQBf6CfoQ8rcpjcfkSPL/OYdSyRwkWHKSDiHBeQ+39hJs9Nb7qmTmdNpTapb9qexpPLu+
NGqqlVXVf1gxcGzgFNswQGn/OOB/9zypXd9n28Z6o/uBxU2kW4GuVJ3dKeW6inhKgZyVwAAAABJ
RU5ErkJggg==" title="Clock 10:00 Base64 built-it" />
很快我们将使用 CryptEncode
函数重现这个字符序列,但目前只需注意,使用类似的技术,我们可以从 MQL5 生成嵌入图形的 HTML 报告。
加密、哈希处理与数据打包:CryptEncode
MQL5 中负责数据加密、哈希处理以及压缩的函数是 CryptEncode
。它会按照指定的方法,将传入的源数组 data
中的数据转换到目标数组 result
中。
cpp
int CryptEncode(ENUM_CRYPT_METHOD method, const uchar &data[], const uchar &key[], uchar &result[])
加密方法还需要传入一个包含私钥(密钥)的字节数组 key
:其长度取决于具体的方法,并且在前一节的 ENUM_CRYPT_METHOD
方法表中有说明。如果密钥数组的大小更大,那么只有所需数量的前几个字节仍会用作密钥。
哈希处理或压缩不需要密钥,但对于 CRYPT_ARCH_ZIP
方法有一个注意事项。事实上,终端内置的 “deflate” 算法的实现会在生成的数据中添加几个字节来控制完整性:开头的 2 个字节包含 “deflate” 算法的设置,结尾的 4 个字节包含 Adler32 校验和。由于这个特性,生成的打包容器与由 ZIP 归档为归档的每个单独元素生成的容器不同(ZIP 标准在其头部存储含义类似的 CRC32 校验和)。因此,为了能够基于由 CryptEncode
函数打包的数据创建和读取兼容的 ZIP 归档,MQL5 允许通过在密钥数组中使用一个特殊值来禁用自身的完整性检查以及额外字节的生成。
cpp
uchar key[] = {1, 0, 0, 0};
CryptEncode(CRYPT_ARCH_ZIP, data, key, result);
可以使用任何长度至少为 4 字节的密钥。得到的结果数组可以根据标准 ZIP 格式添加一个标题(这个问题超出了本书的范围),以创建一个其他程序可访问的归档。
该函数返回放置在目标数组中的字节数,如果出错则返回 0。像往常一样,错误代码将存储在 _LastError
中。
让我们使用脚本 CryptEncode.mq5
来检查该函数的性能。它允许用户输入文本(Text
)或指定一个文件(File
)进行处理。要使用文件,需要清空 Text
字段。
你可以选择一个特定的 Method
,或者一次性遍历所有方法,以便直观地查看和比较不同的结果。对于这样的遍历循环,在 Method
参数中保留默认值 _CRYPT_ALL
。
顺便说一下,为了引入这样的功能,我们再次需要扩展标准枚举(这次是 ENUM_CRYPT_METHOD
),但由于 MQL5 中的枚举不能像类一样继承,所以实际上在这里声明了一个新的枚举 ENUM_CRYPT_METHOD_EXT
。这样做的一个额外好处是,我们为元素添加了更友好的名称(在注释中,带有将在设置对话框中显示的提示)。
cpp
enum ENUM_CRYPT_METHOD_EXT
{
_CRYPT_ALL = 0xFF, // 循环尝试所有方法
_CRYPT_DES = CRYPT_DES, // DES (需要密钥,7 字节)
_CRYPT_AES128 = CRYPT_AES128, // AES128 (需要密钥,16 字节)
_CRYPT_AES256 = CRYPT_AES256, // AES256 (需要密钥,32 字节)
_CRYPT_HASH_MD5 = CRYPT_HASH_MD5, // MD5
_CRYPT_HASH_SHA1 = CRYPT_HASH_SHA1, // SHA1
_CRYPT_HASH_SHA256 = CRYPT_HASH_SHA256, // SHA256
_CRYPT_ARCH_ZIP = CRYPT_ARCH_ZIP, // ZIP
_CRYPT_BASE64 = CRYPT_BASE64, // BASE64
};
cpp
input string Text = "Let's encrypt this message"; // 文本(为空时处理文件)
input string File = "MQL5Book/clock10.htm"; // 文件(仅在 Text 为空时使用)
input ENUM_CRYPT_METHOD_EXT Method = _CRYPT_ALL;
默认情况下,Text
参数中填充了一条应该被加密的消息。你可以用自己的消息替换它。如果我们清空 Text
,程序将处理文件。至少 Text
或 File
其中一个参数应该包含信息。
由于加密需要密钥,另外两个选项允许你直接将其作为文本输入(尽管密钥不一定是文本,可以包含任何二进制数据,但输入中不支持这些),或者根据加密方法生成所需的长度。
cpp
enum DUMMY_KEY_LENGTH
{
DUMMY_KEY_0 = 0, // 0 字节(无密钥)
DUMMY_KEY_7 = 7, // 7 字节(对 DES 足够)
DUMMY_KEY_16 = 16, // 16 字节(对 AES128 足够)
DUMMY_KEY_32 = 32, // 32 字节(对 AES256 足够)
DUMMY_KEY_CUSTOM, // 使用 CustomKey
};
cpp
input DUMMY_KEY_LENGTH GenerateKey = DUMMY_KEY_CUSTOM; // 生成密钥(长度,或来自 CustomKey)
input string CustomKey = "My top secret key is very strong";
最后,有一个 DisableCRCinZIP
选项来启用 ZIP 兼容模式,它只影响 CRYPT_ARCH_ZIP
方法。
cpp
input bool DisableCRCinZIP = false;
为了简化检查方法是否需要加密密钥或是否计算哈希(不可逆的单向转换),定义了 2 个宏。
cpp
#define KEY_REQUIRED(C) ((C) == CRYPT_DES || (C) == CRYPT_AES128 || (C) == CRYPT_AES256)
#define IS_HASH(C) ((C) == CRYPT_HASH_MD5 || (C) == CRYPT_HASH_SHA1 || (C) == CRYPT_HASH_SHA256)
OnStart
的开头包含所需变量和数组的声明。
cpp
void OnStart()
{
ENUM_CRYPT_METHOD method = 0;
int methods[]; // 我们将在这里收集 ENUM_CRYPT_METHOD 的所有元素以便循环遍历
uchar key[] = {}; // 默认为空:适用于哈希处理、ZIP 压缩、Base64 编码
uchar zip[], opt[] = {1, 0, 0, 0}; // ZIP 的“选项”
uchar data[], result[]; // 初始数据和结果
根据 GenerateKey
的设置,我们从 CustomKey
字段获取密钥,或者只是用单调递增的整数值填充密钥数组。实际上,密钥应该是一个秘密的、非平凡的、任意选择的值块。
cpp
if(GenerateKey == DUMMY_KEY_CUSTOM)
{
if(StringLen(CustomKey))
{
PRTF(CustomKey);
StringToCharArray(CustomKey, key, 0, -1, CP_UTF8);
ArrayResize(key, ArraySize(key) - 1);
}
}
else if(GenerateKey != DUMMY_KEY_0)
{
ArrayResize(key, GenerateKey);
for(int i = 0; i < GenerateKey; ++i) key[i] = (uchar)i;
}
在这里和下面,请注意在 StringToCharArray
之后使用 ArrayResize
。一定要将数组减少 1 个元素,因为如果 StringToCharArray
函数将字符串转换为字节数组时包括了结尾的 0,这可能会破坏程序的预期执行。特别是,在这种情况下,我们的秘密密钥中会有一个额外的零字节,如果接收方没有使用具有类似特征的程序,那么它将无法解密消息。这样的额外零字节也可能影响与数据交换协议的兼容性(如果执行了 MQL 程序与“外部世界”的某种集成)。
接下来,我们以十六进制格式记录生成的密钥的原始表示:这是通过在“简化模式下的文件读写”部分中使用的 ByteArrayPrint
函数完成的。
cpp
if(ArraySize(key))
{
Print("Key (bytes):");
ByteArrayPrint(key);
}
else
{
Print("Key is not provided");
}
根据 Text
或 File
是否可用,我们用文本字符或文件内容填充 data
数组。
cpp
if(StringLen(Text))
{
PRTF(Text);
PRTF(StringToCharArray(Text, data, 0, -1, CP_UTF8));
ArrayResize(data, ArraySize(data) - 1);
}
else if(StringLen(File))
{
PRTF(File);
if(PRTF(FileLoad(File, data)) <= 0)
{
return; // 出错
}
}
最后,我们遍历所有方法,或者使用特定方法进行一次转换。
cpp
const int n = (Method == _CRYPT_ALL) ?
EnumToArray(method, methods, 0, UCHAR_MAX) : 1;
ResetLastError();
for(int i = 0; i < n; ++i)
{
method = (ENUM_CRYPT_METHOD)((Method == _CRYPT_ALL) ? methods[i] : Method);
Print("- ", i, " ", EnumToString(method), ", key required: ",
KEY_REQUIRED(method));
if(method == CRYPT_ARCH_ZIP)
{
if(DisableCRCinZIP)
{
ArrayCopy(zip, opt); // 用于 ArraySwap 的带有附加选项的动态数组
}
ArraySwap(key, zip); // 将密钥更改为空或选项
}
if(PRTF(CryptEncode(method, data, key, result)))
{
if(StringLen(Text))
{
// 使用拉丁(西方)代码页来统一所有用户的显示
Print(CharArrayToString(result, 0, WHOLE_ARRAY, 1252));
ByteArrayPrint(result);
if(method != CRYPT_BASE64)
{
const uchar dummy[] = {};
uchar readable[];
if(PRTF(CryptEncode(CRYPT_BASE64, result, dummy, readable)))
{
PrintFormat("Try to decode this with CryptDecode.mq5 (%s):",
EnumToString(method));
// 为了通过字符串输入接收编码数据以进行解码
// 对二进制结果应用 Base64 编码
Print("base64:'" + CharArrayToString(readable, 0, WHOLE_ARRAY, 1252) + "'");
}
}
}
else
{
string parts[];
const string filename = File + "." +
parts[StringSplit(EnumToString(method), '_', parts) - 1];
if(PRTF(FileSave(filename, result)))
{
Print("File saved: ", filename);
if(IS_HASH(method))
{
ByteArrayPrint(result, 1000, "");
}
}
}
}
}
当我们转换文本时,我们记录结果,但由于除了 CRYPT_BASE64
方法之外,结果几乎总是二进制数据,所以它们的显示将是完全乱码(说实话,二进制数据不应该被记录,但我们这样做是为了清楚起见)。在不同语言的计算机上,不可打印的符号和代码大于 128 的符号的显示方式会有所不同。因此,为了统一为所有读者显示示例,在 CharArrayToString
中形成一行时,我们使用显式的代码页(1252,西欧语言)。诚然,出版书籍时使用的字体很可能会影响某些字符的显示方式(字体中的字形集合可能有限)。
重要的是要注意,我们仅在显示方法中控制代码页的选择,并且结果数组中的字节不会因此而改变(当然,以这种方式获得的字符串不应该再发送到其他任何地方;它仅用于可视化,以便使用结果本身的字节进行数据交换)。
然而,我们仍然希望为用户提供一些保存加密结果的机会,以便以后对其进行解码。最简单的方法是使用 CRYPT_BASE64
方法重新转换二进制数据。
在文件编码的情况下,我们只需将结果保存在一个新文件中,文件名是在原始文件名的基础上添加方法名称中最后一个单词的扩展名。例如,对文件 Example.txt
应用 CRYPT_HASH_MD5
方法,我们将得到输出文件 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 | | | |
| §ßö*©ºø<br> |)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` 字段中删除文本。结果,程序将多次处理文件 `MQL5Book/clock10.htm`,并创建几个具有不同扩展名的派生文件。
File=MQL5Book/clock10.htm / ok FileLoad(File,data)=988 / ok
- 0 CRYPT_BASE64, key required: false CryptEncode(method,data,key,result)=1320 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.BASE64
- 1 CRYPT_AES128, key required: true CryptEncode(method,data,key,result)=992 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.AES128
- 2 CRYPT_AES256, key required: true CryptEncode(method,data,key,result)=992 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.AES256
- 3 CRYPT_DES, key required: true CryptEncode(method,data,key,result)=992 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.DES
- 4 CRYPT_HASH_SHA1, key required: false CryptEncode(method,data,key,result)=20 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.SHA1 [00] 486ADFDD071CD23AB28E820B164D813A310B213F
- 5 CRYPT_HASH_SHA256, key required: false CryptEncode(method,data,key,result)=32 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.SHA256 [00] 8990BBAC9C23B1F987952564EBCEF2078232D8C9D6F2CCC2A50784E8CDE044D0
- 6 CRYPT_HASH_MD5, key required: false CryptEncode(method,data,key,result)=16 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.MD5 [00] 0CC4FBC899554BE0C0DBF5C18748C773
- 7 CRYPT_ARCH_ZIP, key required: false CryptEncode(method,data,key,result)=687 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.htm.ZIP
你可以从文件管理器中查看所有文件的内容,并确保它们与原始内容没有任何共同之处。许多文件管理器都有计算哈希值的命令或插件,以便可以将它们与打印到日志中的 MD5、SHA1 和 SHA256 十六进制值进行比较。
如果我们尝试在没有提供正确长度密钥的情况下对文本或文件进行编码,我们将得到 `INVALID_ARRAY(4006)` 错误。例如,对于默认的文本消息,我们在方法参数中选择 `AES256`(需要 32 字节的密钥)。使用 `GenerateKey` 参数,我们指定生成一个长度为 16 字节的密钥(或者你可以部分或完全删除 `CustomKey` 字段中的文本,保持 `GenerateKey` 为默认值)。
Key (bytes): [00] 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | Text=Let's encrypt this message / ok StringToCharArray(Text,data,0,-1,CP_UTF8)=26 / ok
- 0 CRYPT_AES256, key required: true CryptEncode(method,data,key,result)=0 / INVALID_ARRAY(4006)
你也可以使用 `CRYPT_ARCH_ZIP` 方法或使用常规的归档器来压缩同一个文件(就像我们对 `clock10.htm` 所做的那样)。如果你随后使用二进制查看器实用程序(通常内置于文件管理器中)查看,那么两个结果都将显示一个共同的压缩块,并且差异仅在于围绕它的元数据框架。
### 使用 `CRYPT_ARCH_ZIP` 方法压缩的文件(左)与标准 ZIP 归档的比较
使用 `CRYPT_ARCH_ZIP` 方法压缩的文件(左)与标准 ZIP 归档的比较
这表明归档的中间和主要部分是一个字节序列(以深色突出显示),与 `CryptEncode` 函数生成的字节序列相同。
最后,我们将展示如何生成图形文件 `clock10.png` 的 Base64 文本表示。为此,清空 `Text` 字段,并在 `File` 参数中写入 `MQL5Book/clock10.png`。在下拉列表 `Method` 中选择 `Base64`。
File=MQL5Book/clock10.png / ok FileLoad(File,data)=457 / ok
- 0 CRYPT_BASE64, key required: false CryptEncode(method,data,key,result)=612 / ok FileSave(filename,result)=true / ok File saved: MQL5Book/clock10.png.BASE64
结果创建了 `clock10.png.BASE64` 文件。在该文件内部,我们将看到插入在网页代码 `img` 标签中的那一行内容。
顺便说一下,“deflate” 压缩方法是 PNG 图形格式的基础,所以我们可以使用 `CryptEncode` 将资源位图保存为 PNG 文件。本书附带了头文件 `PNG.mqh`,它对描述图像所需的内部结构提供了最小限度的支持:建议你自行对其源代码进行实验。使用 `PNG.mqh`,我们编写了一个简单的脚本 `CryptPNG.mq5`,它将终端附带的 “euro.bmp” 文件中的资源转换为 “my.png” 文件。目前尚未实现加载 PNG 文件的功能。
```cpp
#resource "\\Images\\euro.bmp"
#include <MQL5Book/PNG.mqh>
void OnStart()
{
uchar null[]; // 用于 CRYPT_ARCH_ZIP 的空密钥
uchar result[]; // 接收数组
uint data[]; // 原始像素
uchar bytes[]; // 原始字节
int width, height;
PRTF(ResourceReadImage("::Images\\euro.bmp", data, width, height));
ArrayResize(bytes, ArraySize(data) * 3 + width); // *3 用于 PNG_CTYPE_TRUECOLOR (RGB)
ArrayInitialize(bytes, 0);
int j = 0;
for(int i = 0; i < ArraySize(data); ++i)
{
if(i % width == 0) bytes[j++] = 0; // 每行都以一个过滤模式字节开头
const uint c = data[i];
// bytes[j++] = (uchar)((c >> 24) & 0xFF); // alpha, 用于 PNG_CTYPE_TRUECOLORALPHA (ARGB)
bytes[j++] = (uchar)((c >> 16) & 0xFF);
bytes[j++] = (uchar)((c >> 8) & 0xFF);
bytes[j++] = (uchar)(c & 0xFF);
}
PRTF(CryptEncode(CRYPT_ARCH_ZIP, bytes, null, result));
int h = PRTF(FileOpen("my.png", FILE_BIN | FILE_WRITE));
PNG::Image image(width, height, result); // 默认 PNG_CTYPE_TRUECOLOR (RGB)
image.write(h);
FileClose(h);
}