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。
添加图片注释,不超过 140 字(可选)
以下附件,含有 CryptoC#.zip 项目的所有必需文件。
任何人都可能注意到以下项目缺陷:
-
它没有必要检查所调用函数的参数。
-
没有异常处理。
-
单流操作模式。
我没有实现上述功能,因为本文目标并非创建功能齐全的应用程序。 如果我们实现了所有必需的部分,那么多余的代码部分会很庞大,并且会分散基本问题的注意力。
C++ 项目
下一个项目是以 C++ 创建的。 我们在 Visual Studio 2017 环境中创建控制台应用程序。 在箱体之外我们不支持任何加密/解密。 因此,我们必须通过下载并安装 OpenSSL 安装包来连接著名的 OpenSSL 函数库。 结果就是,我们可以使用所有 Open SSL 函数库和包含项,它们应该连接到所创建项目。 有关如何将函数库连接到项目的详细信息,请阅读这篇文章。 不幸的是,OpenSSL 文档远未完善,然而没有更好的使用方法。
连接函数库后,继续编写代码。 首先要做的事情是再次讲述已知的两个结构:
constexpr size_t PRODMAXLENGTH = 255; #pragma pack(1) typedef struct EA_USER { EA_USER(); EA_USER(std::string name); EA_USER(EA_USER& eauser); std::time_t expired; long namelength; char eaname[PRODMAXLENGTH]; std::string GetName(); void SetName(std::string newName); std::string GetTimeExpired(); std::string ToString(); size_t ToArray(byte* pbyte); constexpr size_t GetSize() noexcept; friend std::ostream& operator<< (std::ostream& out, EA_USER& eauser); friend std::istream& operator>> (std::istream& in, EA_USER& eauser); }EAUSER, *PEAUSER; #pragma pack() constexpr size_t COUNTACC = 5; #pragma pack(1) typedef struct USER_LIC { using PEAUNIC = std::unique_ptr<EAUSER>; USER_LIC(); USER_LIC(USER_LIC&& ul); USER_LIC(const byte* pbyte); int64_t uid; std::time_t expired; long log_count; int64_t logins[COUNTACC]; long ea_count; std::vector<PEAUNIC> pEa; std::string GetTimeExpired(); std::string ToString(); size_t ToArray(byte* pbyte); void AddEA(EA_USER eau); bool AddAcc(long newAcc); size_t GetSize(); friend std::ostream& operator<< (std::ostream& out, USER_LIC& ul); friend std::istream& operator>> (std::istream& in, USER_LIC& ul); USER_LIC& operator = (const USER_LIC&) = delete; USER_LIC(const USER_LIC&) = delete; } USERLIC, *PUSERLIC; #pragma pack()
该代码比 C# 的要复杂一些。 某些字段类型有所不同。 例如,在此项目中,携带帐户数组的字段具有 int64_t 数组类型,而 MQL 包含文件具有 long 类型。 这与相应类型的大小有关。 如果您不控制此类功能,则可能导致难以捕获的错误。 有些部分则比较容易:此处我们不需要转换时间。
另外,在这个项目中,我们可能会遇到密钥长度不正确的问题。 若要解决此问题,请在项目中包括以下函数:
std::string AES_NormalizeKey(const void *const apBuffer, size_t aSize)
此函数会将 appBuffer 数组“修剪”为所需的 aSize 长度。 另外,我们编写以下辅助函数:
void handleErrors(void) { ERR_print_errors_fp(stderr); }
此函数将提供 OpenSSL 函数库中错误代码的说明。 以下两个函数实现了主要操作:
int aes_decrypt(const byte* key, const byte* iv, const byte* pCtext, byte* pRtext, int iTextsz) int aes_encrypt(const byte* key, const byte* iv, const byte* pPtext, byte* pCtext, int iTextsz)
附件中提供了方法的实现。 我只会提及一些基本要点:
-
这里不使用初始化向量。 我们创建所需大小的数组,并在调用点将其传递。
-
该函数库不提供任何有关填充的好处。 通过调用设置此模式: EVP_CIPHER_CTX_set_padding(ctx.get(), 0); 确保传递“零”,而不是这样:EVP_CIPHER_CTX_set_padding(ctx.get(), EVP_PADDING_ZERO); 也许看似与此相应。 更多问题是与添加的连接有关。 事实上,如果填充值为零(如同我们的项目中一样),则开发者必须注意确保加密对象的长度是 BLOCK_SIZE = AES_BLOCK_SIZE 的倍数,即 16 个字节。 这就是为什么在调用 aes_encrypt(......) 之前,必须要为加密数组提供相应的对齐方式。
按顺序执行与上一个项目相同的操作:
-
解密生成的文件,对其进行编辑,然后再次对其进行加密。 在这种情况下,将另一个用户帐户的有关信息添加到许可证之中。
-
现在,我们得到一个加密文件 lic_С++.txt 。 这次的文件大小不同。 这是时钟大小(16 字节),它是在对齐过程中加进来的。
该项目的所有源文件都位于下面随附的 CryptoС++.zip 存档中。
最终检查和结果
现在,我们进入最后的操作步骤。 将最近加密的 lic_С++.txt 文件移至 MetaTrader 数据目录的 File 文件夹当中,并用先前编写的脚本 deleteuser.mq5 对其解密。 我们得到了预期的结果:尽管长度有所变化,但文件已成功解密。
那么,我们会得到什么结果呢? 最重要的是,我们检测到加密/解密参数,该参数允许将加密文件从一个程序传输到另一个程序。 显然,我们可以稍后假定,如果加密/解密失败,则该问题可能是由应用程序中的错误引起的。
哈希值
如您一样的许多人可能都知道,加密不仅限于加密/解密。 我们来研究一个加密原语 - 哈希散列。 此过程意味着将某个任意数组转换为固定长度的数组。 这样的数组称为哈希值,转换函数称为 hash 函数。 两个初始数组至少在某一数位上彼此不同,将产生完全不同的哈希值,这些哈希值可用于识别和比较。
此为一个示例。 用户在站点上注册,并输入他的识别数据。 数据保存在一个非常机密的数据库当中。 现在,同一用户尝试在主页上输入登录名和密码,从而登录该站点。 网站应该怎么做? 它可以简单地从数据库中检索用户的密码,并将其与输入的密码进行比较。 但这并不安全。 安全的方式是取所存储密码的哈希值并和输入密码的哈希值进行比较。 即使存储的哈希值被盗,密码本身依然保持安全。 通过比较哈希值,我们可以确定输入的密码是否正确。
哈希是一个单向过程。 即使拿到哈希值,也不可能将其还原为数据数组。 因此,哈希对于密码学非常重要。 我们研究不同环境中的哈希计算。
我们的目的相同:找出在终端程序和其它第三方程序中进行计算时,如何确保相同初始数据的哈希值相同。
在 MQL 中,用我们之前所用的库函数 CryptEncode 计算哈希值。 为计算哈希值,应设置 method 函数的参数值。 我们采用 CRYPT_HASH_SHA256 值。 文档提供了其他值和其他哈希类型,因此您可以进一步阅读本主题。 采用现有密码行:“qwertyuiopasdfgh” 作为源数组。 计算其哈希值,并将哈希值写入文件。 所得代码非常简单;因此,我们简单地将其包含在附加的脚本文件 decryptuser.mq5 当中,而无需创建单独的类和函数:
string k = "qwertyuiopasdfgh"; uchar key[], result[], enc[]; StringToCharArray(k, enc); int sha = CryptEncode(CRYPT_HASH_SHA256,enc,key,result); string sha256; for(int i = 0; i < sha; i++) sha256 += StringFormat("%X ",result[i]); Print("SHA256 len: ",sha," Value: ",sha256); int h = FileOpen("sha256.bin", FILE_WRITE | FILE_BIN); if (h == INVALID_HANDLE) { Print("File create failed: sha256.bin"); }else { FileWriteArray(h, result); FileClose(h); }
此处未使用先前用于加密的 key 数组。 将生成的哈希值写入 result 数组,输出到窗口,并写入 sha256.bin 文件。 所得哈希值的长度固定为 32 个字节。 您可以更改源数组的大小,我们将其设置为一个字符长,但哈希值大小仍为 32 个字节。
在 C# 和 C++ 项目中添加所需的功能,来重现相同的计算。 所做的修改很小,非常简单。 我们采用来自密码字符串的相同源数组。 添加类似的代码行。 计算,然而... 您会得到令人沮丧的不同结果! 好吧,它们“并非完全不同”。 MQL 脚本和 C++ 项目计算的哈希值是相同的。 但 C# 项目给出了不同的结果。 我们尝试采用另一个只有一个字符 “a” 组成的字符串。 同样,C# 项目将计算并产生不同的哈希值。
该问题与调用 StringToCharArray 函数有关,该函数调用将字符串转换为数组。 如果在 StringToCharArray 调用之后查看结果数组,则会看到该数组的大小增加了一倍。 例如,在采用字符串 “a” 调用函数之后,结果数组将包含两个元素。 第二个元素为 “0”。 在 C# 中调用 Encoding.ASCII.GetBytes 则可以避免这种情况。 在这种情况下,数组中将不包含 “0”。
现在,我们可以向 C# 项目中添加一个代码块,该代码块将 “0” 附加到字节数组。 之后,我们可以采用此字节数组来计算哈希值。 现在我们获得了预期的结果。 针对相同的输入数据所有三个项目计算出相同的哈希值。 生成的哈希值在文件 sha256.bin, sha256_c#.bin, sha256_С++.bin 中可用,这些文件位于下面随附的 CryptoMQL.zip 存档之中。
请注意,上面的示例涉及文本数据。 显然,对于最初的二进制数组,不需要调用 StringToCharArray 和 Encoding.ASCII.GetBytes。 多余的 “0” 不会产生问题。 因此,另一个可能的选择是从 MQL 项目中删除 “0”,替代在 C# 中将其加入。
无论如何,我们已经解决了最初的问题 - 我们发现即使在不同环境里计算,在确定条件下对象哈希值也将是相同的。 我们还实现了本文开头列出的目标。 我们已确定应采用哪种加密/解密模式来确保不同环境中结果的兼容性。