利用外部应用程序进行加密
加密在 MQL 程序中很少使用。 在日常交易中,使用密码术的机会并不多。 一个例外就是偏执的信号跟单机希望保护发送的数据免于监听,仅此而已。 若数据不会离开终端,很难想象为什么需要加密/解密数据。 甚至,这可能代表开发人员能力低下,因其造成了终端的额外负载。
也许无需在交易中使用加密? 实际上,其实有。 例如,考虑许可。 可能会有一家小型公司,甚或一位广受欢迎产品的独立开发者。 这种情况与许可问题相关,因此需要许可证加密/解密。
可在许可证中指定用户数据,和产品的可编辑列表。 指标或智能交易系统开始操作,检查许可证的可用性,及给定产品的期限。 程序向服务器发送请求,更新许可证,如有必要,或可接收新的许可证。 这可能不是最有效和最安全的方式,但出于演示目的,我们将在本文中运用它。 显然,在这种情况下,许可证将通过不同的软件工具进行读取/写入 — 终端,远程服务器,控制模块和日志记录模块。 它们可以由不同的人,在不同的时间,以不同的语言编写。
本文的目的是研究加密/解密模式,在这种模式下,MetaTrader 终端可以解密由 C# 或 C++ 程序加密的对象,反之亦然。
本文适用于中等技能的程序员和初学者。
设定任务
概述中已经提到了这一点。 我们将尝试模拟一个实际问题的解决方案,要求为若干种产品(指标和智能交易系统)的许可证创建、加密和解密。 对于我们而言,用哪个程序来加密和解密许可证并不重要。 例如,可以先在开发者的计算机上创建许可证,然后在销售部门对其进行纠正,然后在交易者的计算机上解密。 该过程必须针对性能低廉的算法有很强的容错性。
除了解决主要任务外,我们还将研究许可的复杂问题。 这不是一切就绪立即可用的许可证,而只是可能的变体之一,应对其进一步进行编辑和开发。
源数据
我们参考终端文档来获取操作的源数据。 有两个标准函数负责加密/解密过程:
int CryptEncode( ENUM_CRYPT_METHOD method, // conversion method const uchar& data[], // source array const uchar& key[], // encryption key uchar& result[] // destination array ); int CryptDecode( ENUM_CRYPT_METHOD method, // conversion method const uchar& data[], // source array const uchar& key[], // encryption key uchar& result[] // destination array );
加密/解密的具体方法由 method 参数确定。 在这种情况下,我们对 method 参数可以具有的三个值很感兴趣: CRYPT_AES128,CRYPT_AES256, CRYPT_DES。 这三个值代表具有不同密钥长度的对称加密算法。
在本文中,我们仅用其中之一 CRYPT_AES128。 这是一种具有 128 位(16 字节)密钥的对称分组密码算法。 其他算法的用法类似。
AES 算法(不仅是选定的算法,还有其他具有密钥长度的算法)拥有一些上述函数中没有提供的一些重要设置。 其中包括加密模式和填充。 我们不会深入介绍这些术语。 因此,终端使用电子密码簿(ECB)加密模式,且 Padding (填充模板)等于零。 我要感谢交易伙伴,因为他们在 MQL5 论坛上给与了解释。 现在,我们的任务将更容易解决。
开发操作对象
鉴于我们研究如何将加密/解密应用于许可,故我们的操作对象是许可。 这应该是一些包含可适用在许可证的各种产品信息的结构。 这里需要以下数据:
- 该产品的许可期限。
- 产品名称。
我们用最简单的方法创建相应的结构:
#define PRODMAXLENGTH 255 struct ea_user { ea_user() {expired = -1;} datetime expired; //License expiration (-1 - unlimited) int namelength; //Product name length char uname[PRODMAXLENGTH]; //Product name void SetEAname(string name) { namelength = StringToCharArray(name, uname); } string GetEAname() { return CharArrayToString(uname, 0, namelength); } bool IsExpired() { if (expired == -1) return false; // NOT expired return expired <= TimeLocal(); } };//struct ea_user
此处是一些解释:
- 产品名称存储在固定长度的数组。
- 产品名称最大长度限制为 PRODMAXLENGTH。
- 可以很轻易地将此策略封包到字节数组中 — 这是我们在加密整个对象之前要做的。
然而,仅有此结构是不够的。 显然,许可证必须包含用户详细信息。 该信息能被包括在已讲述过的结构中,但用户可能拥有多个产品许可证,因此效率低下。 一个更合理的解决方案是为用户创建一个单独的结构,并往其中添加所需数量的产品许可证。 因此,用户将只拥有一个许可证,但其中包含所有许可产品的权限。
可包含在用户描述结构中的信息:
- 独有的用户 ID。 还可以保存名称,但每次都要发送个人数据(即使是加密形式)似乎并不可取。
- 用户帐户上可用产品的有关信息。
- 用户的许可证到期日期。 该字段可限定所有现有产品的用法,甚至是无限制产品,作为用户服务时间。
- 用户终端中许可产品的数量:
#define COUNTACC 5 struct user_lic { user_lic() { uid = -1; log_count = 0; ea_count = 0; expired = -1; ArrayFill(logins, 0, COUNTACC, 0); } long uid; //User ID datetime expired; //End of user service (-1 - unlimited) int log_count; //The number of the user's accounts long logins[COUNTACC]; //User's accounts int ea_count; //The number of licensed products bool AddLogin(long lg){ if (log_count >= COUNTACC) return false; logins[log_count++] = lg; return true; } long GetLogin(int num) { if (num >= log_count) return -1; return logins[num]; } bool IsExpired() { if (expired == -1) return false; // NOT expired return expired <= TimeLocal(); } };//struct user_lic
以下是一些澄清:
- 用户帐户存储在固定长度的数组中。 它们由帐号表示。 尽管,若有必要,可按服务器或经纪商名称,以及特定帐户的激活次数轻松补充信息。
到目前为止,此结构包含有关用户和相关产品的足够信息。 它们每个都是一种类型,可以通过 StructToCharArray 函数对其实例进行处理。
现在,我们需要将结构数据序列化为字节数组,以便进一步进行加密。 实现如下:
- 创建并初始化 user_lic 结构的实例。
- 将其序列化为字节数组。
- 创建并初始化 ea_user 结构的一个或多个实例。
- 将它们序列化为相同的字节数组,增加其大小,并调整 ea_count 字段。
创建一个类来执行以下操作:
class CLic { public: static int iSizeEauser; static int iSizeUserlic; CLic() {} ~CLic() {} int SetUser(const user_lic& header){ Reset(); if (!StructToCharArray(header, dest) ) return 0; return ArraySize(dest); }//int SetUser(user_lic& header) int AddEA(const ea_user& ea) { int c = ArraySize(dest); if (c == 0) return 0; uchar tmp[]; if (!StructToCharArray(ea, tmp) ) return 0; ArrayInsert(dest, tmp, c); return ArraySize(dest); }//int AddEA(ea_user& ea) bool GetUser(user_lic& header) const { if (ArraySize(dest) < iSizeUserlic) return false; return CharArrayToStruct(header, dest); }//bool GetUser(user_lic& header) //num - 0 based bool GetEA(int num, ea_user& ea) const { int index = iSizeUserlic + num * iSizeEauser; if (ArraySize(dest) < index + iSizeEauser) return false; return CharArrayToStruct(ea, dest, index); }//bool GetEA(int num, ea_user& ea) int Encode(ENUM_CRYPT_METHOD method, string key, uchar& buffer[]) const { if (ArraySize(dest) < iSizeUserlic) return 0; if(!IsKeyCorrect(method, key) ) return 0; uchar k[]; StringToCharArray(key, k); return CryptEncode(method, dest, k, buffer); } int Decode(ENUM_CRYPT_METHOD method, string key, uchar& buffer[]) { Reset(); if(!IsKeyCorrect(method, key) ) return 0; uchar k[]; StringToCharArray(key, k); return CryptDecode(method, buffer, k, dest); } protected: void Reset() {ArrayResize(dest, 0);} bool IsKeyCorrect(ENUM_CRYPT_METHOD method, string key) const { int len = StringLen(key); switch (method) { case CRYPT_AES128: if (len == 16) return true; break; case CRYPT_AES256: if (len == 32) return true; break; case CRYPT_DES: if (len == 7) return true; break; } #ifdef __DEBUG_USERMQH__ Print("Key length is incorrect: ",len); #endif return false; }//bool IsKeyCorrect(ENUM_CRYPT_METHOD method, string key) private: uchar dest[]; };//class CLic static int CLic::iSizeEauser = sizeof(ea_user); static int CLic::iSizeUserlic = sizeof(user_lic);
该类中添加了两个函数,可以进行加密和解密,还有一个受保护函数用于检查密钥长度。 从其代码中可以看到,例如, CRYPT_AES128 方法的密钥长度必须等于 16 个字节。 实际上,它不能少于 16 个字节。 大概,以后会将其常规化,令其对开发人员隐藏。 我们不会依赖于此,而是会严格设置所需的密钥长度。
最后,已生成的字节数组能够进行加密,并将其保存到二进制文件当中。 根据一般规则,此文件应存储在终端的 File 文件夹中。 若有必要,可对其进行读取和解密:
bool CreateLic(ENUM_CRYPT_METHOD method, string key, CLic& li, string licname) { uchar cd[]; if (li.Encode(method, key, cd) == 0) return false; int h = FileOpen(licname, FILE_WRITE | FILE_BIN); if (h == INVALID_HANDLE) { #ifdef __DEBUG_USERMQH__ Print("File create failed: ",licname); #endif return false; } FileWriteArray(h, cd); FileClose(h); #ifdef __DEBUG_USERMQH__ li.SaveArray(); #endif return true; }// bool CreateLic(ENUM_CRYPT_METHOD method, string key, const CLic& li, string licname) bool ReadLic(ENUM_CRYPT_METHOD method, string key, CLic& li, string licname) { int h = FileOpen(licname, FILE_READ | FILE_BIN); if (h == INVALID_HANDLE) { #ifdef __DEBUG_USERMQH__ Print("File open failed: ",licname); #endif return false; } uchar cd[]; FileReadArray(h,cd); if (ArraySize(cd) < CLic::iSizeUserlic) { #ifdef __DEBUG_USERMQH__ Print("File too small: ",licname); #endif return false; } li.Decode(method, key, cd); FileClose(h); return true; }// bool ReadLic(ENUM_CRYPT_METHOD method, string key, CLic& li, string licname)
两个函数都很清楚,不需要其他说明。 随附的 CryptoMQL.zip 文件包含两个脚本和一个函数库文件,它们实现了加密/解密,还有加密的许可证文件 lic.txt。
C# 项目
我们创建一个简单的 C# 项目,从而模拟另一个程序的解密和编辑过程。 使用 Visual Studio 2017 并为 .NET Framework 平台创建一个控制台应用程序。 检查 System.Security 和 System.Security.Cryptography 的空间连接。
代码中出现以下问题:MQL 和 C# 具有不同的时间格式。 本文已经解决了该问题。 作者已完成了一个壮举,我们可以在项目中借用他的 MtConverter 类。
创建两个类,分别为 EaUser 和 UserLic ,其字段类似于 ea_user 和 user_lic 结构。 目的是解密由终端创建的许可证(lic.txt 文件),解析接收的数据,修改对象,并重新加密,以便创建新文件。 如果您仔细设置加密/解密模式,此任务必将易于实现。 此处是相应代码的样子:
using (Aes aesAlg = Aes.Create()) { aesAlg.Key = Key; aesAlg.IV = IV; aesAlg.Mode = CipherMode.ECB; aesAlg.Padding = PaddingMode.Zeros; ..................................
请注意最后两行,其中设置了加密模式(ECB)和填充。 我们为这些模式采用有关的设置信息。 模块中与密钥安装有关的第一行应该予以澄清。 它采用与终端加密时相同的密钥,但这次将其转换为字节数组:
string skey = "qwertyuiopasdfgh"; byte[] Key = Encoding.ASCII.GetBytes(s: skey);
注意设置“ IV” 参数的那一行。 这就是所谓的“初始化向量”,即,除 ECB 模式外参与所有加密模式的随机数字。 因此,我们此时简单地创建所需长度的数组:
byte[] iv = new byte[16];
另外,请注意,C# 中的密钥情况与 MQL 中的情况不同。 如果密钥长度(在本例中为 “qwertyuiopasdfgh” 行)大于 16,则将引发异常。 这就是为什么严格控制 MQL 代码中的密钥长度是一个不错决定的原因。
其余的非常简单。 读取二进制文件 -> 解密流 -> 利用 BinaryReader 填充创建的 UserLic 类实例。 也许,若序列化相应的类,可能会获得相似的结果。 您可自行测试这种可能性。
我们来修改某些字段,在这种情况下,我们将更改用户 ID。 然后,以相同的方式加密数据,并创建一个新文件 “lic_C#.txt”。 以上操作由项目中的两个静态函数 EncryptToFile_Aes 和 DecryptFromFile_Aes 执行。 为了进行调试,我添加了两个类似的函数,它们不操纵文件,而是针对字节数组起作用: EncryptToArray_Aes 和 DecryptFromArray_Aes。
以下附件,含有 CryptoC#.zip 项目的所有必需文件。
任何人都可能注意到以下项目缺陷:
- 它没有必要检查所调用函数的参数。
- 没有异常处理。
- 单流操作模式。
我没有实现上述功能,因为本文目标并非创建功能齐全的应用程序。 如果我们实现了所有必需的部分,那么多余的代码部分会很庞大,并且会分散基本问题的注意力。
C++ 项目
下一个项目是以 C++ 创建的。 我们在 Visual Studio 2017 环境中创建控制台应用程序。 在箱体之外我们不支持任何加密/解密。 因此,我们必须通过下载并安装 OpenSSL 安装包来连接著名的 OpenSSL 函数库。 结果就是,我们可以使用所有 Open SSL 函数库和包含项,它们应该连接到所创建项目。 有关如何将函数库连接到项目的详细信息,请阅读这篇文章。 不幸的是,OpenSSL 文档远未完善,然而没有更好的使用方法。