Использование криптографии совместно с внешними приложениями

Andrei Novichkov | 7 июля, 2020

Введение

Использование криптографии нечасто встретишь в программах на MQL. В обычной повседневной практике трейдера места для неё мало, либо не находится вовсе. Можно себе представить параноидальный копировщик сигналов, который очень не хочет, чтобы пересылаемые им данные подслушал кто-то еще, да, пожалуй, и всё. Там, где данные не покидают терминала, очень трудно себе представить необходимость в их шифровке / дешифровке. Скорее, их применение в этом случае, скажет о недостаточной компетенции разработчика, создающего лишнюю нагрузку на терминал.

Может быть необходимости в криптографии нет вовсе? Конечно же, такая необходимость имеется. Возьмем, например, область лицензирования. Легко представить себе небольшую компанию, отдельного разработчика, продукция которых пользуется успехом. Для них вопросы лицензирования будут весьма актуальны, а следовательно, и необходимость в шифровке / дешифровке  лицензий.

В лицензии можно будет указать данные пользователя и изменяемый список инструментов, которые у пользователя имеются. Индикатор или советник начинают работу, проверяют наличие лицензии, срок её окончания по данному инструменту. Запрашивают сервер, при необходимости обновляют лицензию или получают новую. Скорее всего, это не самый эффективный и безопасный путь, но в этой статье мы используем его для иллюстрации. Очевидно, что в данном случае чтением / записью лицензии будут заниматься разные программные средства — терминал, удаленный сервер, контролирующие модули и модули логирования. Так же очевидно, что они могут быть написаны разными людьми, в разное время и на разных языках.

Целью настоящей статьи является изучение режимов шифровки / дешифровки, при которых объект, зашифрованный программой, написанной на C# или C++, мог быть гарантированно расшифрован терминалом MetaTrader и наоборот.

Статья рассчитана как на разработчиков средней квалификации, так и на новичков.

Постановка задачи

Об этом уже бегло сказано во введении. Мы попытаемся сымитировать решение реальной проблемы с созданием, шифровкой и дешифровкой лицензии на несколько инструментов — индикаторов и советников. При этом мы будем считать неважным факт, какой именно программой выполнена шифровка, а какой дешифровка. Например, легко представить ситуацию, когда первичное создание лицензии происходит на компьютере разработчика, корректировка в отделе продаж, а расшифровка на компьютере трейдера. Совершенно очевидно, что при этом не должно происходить никаких ошибок из-за плохо сконфигурированных алгоритмов.

Попутно с решением основной задачи мы коснемся и сложной проблемы лицензирования, рассматривая её как второстепенную. Ни в коем случае не претендуя на создание эффективного и рабочего объекта — лицензии, мы предложим один из возможных вариантов, который можно и должно править, развивать или рассматривать как вариант, который применять не следует.

Исходные данные

За исходными данными для работы обратимся к документации к терминалу. Имеются две штатные функции, отвечающие за всю работу по шифровке / дешифровке:

int  CryptEncode(
   ENUM_CRYPT_METHOD   method,        // метод преобразования
   const uchar&        data[],        // массив-источник
   const uchar&        key[],         // ключ шифрования
   uchar&              result[]       // массив-приемник
   );

int  CryptDecode(
   ENUM_CRYPT_METHOD   method,        // метод преобразования
   const uchar&        data[],        // массив-источник
   const uchar&        key[],         // ключ шифрования
   uchar&              result[]       // массив-приемник
   );

Как именно будет выполняться шифровка / дешифровка, определяет аргумент method. В данном случае интересны три значения, которые может принимать аргумент method: CRYPT_AES128, CRYPT_AES256, CRYPT_DES. Все эти три значения метода представляют алгоритмы симметричного шифрования с различной длиной ключа.

Для работы в рамках настоящей статьи мы будем применять только один из них — CRYPT_AES128. Это симметричный алгоритм блочного шифрования с ключом 128 бит (16 байт). Работа с другими алгоритмами будет аналогична данной.

Алгоритм AES (не только выбранный, но и с другой длиной ключа) имеет несколько важных настроек, которые в вышеприведенных функциях никак не представлены. Это режим шифрования и дополнение (padding). Останавливаться на подробном рассмотрении каждого из этих терминов мы не будем, но их значение нам все таки понадобится. Итак, в терминале применяется решим шифрования Electronic Codebook (ECB) и Padding, равный нулю. За эти знания нам следует поблагодарить коллег из англоязычной ветки форума. Теперь нашу задачу решить будет значительно легче.

Разработка объекта работы

Так как здесь рассматриваются вопросы шифровки / дешифровки в применении к лицензированию, то и объектом работы будет лицензия. Это должна быть некоторая конструкция, содержащая сведения об различных инструментах, находящихся в зоне ответственности  лицензии. Очевидно, что необходимы сведения о:

  1. Сроке окончания лицензии на данный инструмент.
  2. Название инструмента.

Создадим соответствующую структуру с сопутствующими простейшими методами:

#define PRODMAXLENGTH 255

struct ea_user 
  {
   ea_user() {expired = -1;}
   datetime expired;                //Срок окончания лицензии (-1 - бессрочно)
   int      namelength;             //Длина имени инструмента
   char    uname[PRODMAXLENGTH];    //Имя инструмента.
   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

Несколько сопутствующих пояснений:

Однако одной такой структуры недостаточно. Очевидно, что должны быть сведения о пользователе. Включить эти сведения в уже описанную структуру можно, но неэффективно, т.к. пользователь может обладать лицензией сразу на несколько инструментов. Рациональнее сделать отдельную структуру для пользователя и добавлять к ней необходимое количество лицензий на инструменты. Таким образом, на одного пользователя получается одна лицензия, содержащая разрешения и ограничения на все лицензируемые инструменты.

Сведения, которые могли бы содержаться в структуре, описывающей пользователя:

  1. Его уникальный идентификатор. Можно было бы сохранять и имя, но представляется нежелательным лишний раз пересылать персональные данные, пусть и в зашифрованном виде.
  2. Сведения об аккаунтах пользователя, на которых могут работать инструменты.
  3. Срок окончания лицензии пользователя. Это поле могло бы ограничивать время работы всех имеющихся инструментов, даже бессрочных, временем обслуживания пользователя, как такового.
  4. Количество лицензируемых инструментов в терминале пользователя:
#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;                       //Идентификатор пользователя
   datetime expired;               //Срок окончания обслуживания пользователя (-1 - бессрочно)
   int  log_count;                 //Количество аккаунтов пользователя
   long logins[COUNTACC];          //Аккаунты пользователя
   int  ea_count;                  //Количество лицензируемых инструментов
   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, что и требуется.

Теперь необходимо сериализовать данные структуры в байтовый массив для последующей шифровки. Это будет сделано следующим образом:

Для выполнения этих операций создаем класс:

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) и дополнения (padding). Используем уже имеющиеся у нас сведения об установках для этих режимов. Первая строка в блоке, касающаяся установки ключа должна быть понятной. Используется тот же ключ, что и при шифровке в терминале, но превращенный в байтовый массив:

            string skey = "qwertyuiopasdfgh";
            byte[] Key  = Encoding.ASCII.GetBytes(s: skey);

Особо отметим строку с установкой параметра "IV". Это так называемый "вектор инициализации" — случайное число, участвующее во всех режимах шифровки, кроме режима ECB. Поэтому мы просто создаем в этой точке массив нужной длины:

byte[] iv   = new byte[16];

Кроме того, обратим внимание, что в C# иная ситуация с длиной ключа, чем в MQL. Если длина ключа ( в данном случае строки "qwertyuiopasdfgh") будет больше 16, то будет выброшено исключение. Поэтому решение о том, чтобы жестко контролировать длину ключа в MQL  коде было принято верно.

Далее все довольно просто. Читаем бинарный файл -> дешифруем поток -> заполняем созданный экземпляр класса UserLic с помощью BinaryReader. Вполне возможно, что аналогичного результата можно было бы добиться, сделав соответствующие классы сериализуемыми. Желающим ничто не запрещает самостоятельно проверить данное утверждение.

Модифицируем какое либо поле, в данном случае изменим идентификатор пользователя. Зашифровываем данные аналогичным способом и  создаем новый файл "lic_C#.txt". Всю вышеописанную работу проделывают две статические функции EncryptToFile_Aes и DecryptFromFile_Aes проекта. Кроме того, для целей отладки, были добавлены еще две аналогичные функции, но работающие не с файлами а с байтовыми массивами: EncryptToArray_Aes и DecryptFromArray_Aes.

Проект CryptoC#.zip со всеми фалами прилагается к статье.

Разумеется, даже разработчик с небольшим опытом заметит недостатки проекта:

Эти, несомненно справедливые, замечания в данном случае не нашли своего отражения в коде по причине того, что нашей целью не является создание промышленного приложения. А дополнительная, довольно толстая, прослойка кода отвлечет внимание интересующихся от сути решаемой задачи.

Проект С++

Заключительный проект в настоящей статье будет создан на этом языке. Создадим консольное приложение в среде Visual Studio 2017. "Из коробки" никакой поддержки шифровки / дешифровки у нас не имеется, поэтому придется подключать известную библиотеку OpenSSL, для чего установочный пакет OpenSSL должен быть скачан и установлен на компьютер. В результате разработчику становятся доступны все библиотеки и "инклуды" OpenSSL, которые следует подключить к своему созданному проекту. Как подключать библиотеки к своему проекту желающие могут освежить в памяти, прочитав эту статью. К большому сожалению, документация по 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)

Релизацию методов здесь приводить не стоит, желающие смогут увидеть её в прилагаемом проекте. Однако, некоторые существенные моменты необходимо отметить:

Здесь мы станем выполнять ту же последовательность действий, что и предыдущем проекте:

Все исходные файлы проекта прилагаются в архиве CryptoС++.zip

Последняя проверка и результаты

Остался заключительный этап работы. Переносим только что зашифрованный файл lic_С++.txt в папку File MetaTrader-a и расшифровываем его ранее написанным скриптом decryptuser.mq5 . Получаем ожидаемый результат — файл успешно расшифрован, несмотря на изменение длины.

Что же получено в результате? Самое главное — выяснены параметры шифровки / дешифровки, позволяющие пересылать зашифрованные файлы из одной программы в другую. Очевидно, что в дальнейшем можно будет утверждать, что в том случае, если шифровка / дешифровка в каком либо случае не удалась, то дело в ошибках прикладных программ.

Хеши

Вероятно, подавляющее большинство знает, что криптография не ограничивается одной лишь шифровкой / дешифровкой. Рассмотрим такой криптографический примитив, как хеширование. Это процесс преобразования некоторого произвольного массива в массив фиксированной длины. Такой массив называют хешем, а функцию преобразования хеш-функцией. Два исходных массива, отличающихся друг от друга хотя бы одним битом, дадут в итоге абсолютно разные хеши, что и можно использовать для идентификации и сравнения.

Для более ясного понимания вот конкретный пример. Пользователь регистрируется на сайте, вводит свои идентификационные данные. Данные сохраняются где-то в очень секретной базе данных. Теперь этот же пользователь пытается войти на сайт, вводит на главной странице свой логин и пароль. Что делать сайту? Можно просто извлечь из базы данных пароль пользователя и сравнить со введенным. Но это небезопасно. Безопасно сравнить хеши паролей — того, что в базе данных и того, что только что введен. И даже в том случае, если хеш пароля, хранящегося на сайте, будет при этом похищен, то ничего страшного не произойдет — сам пароль останется в неприкосновенности. А сравнение хешей даст ответ на вопрос, правильный ли пароль ввел пользователь.

Хеширование — это движение в одну сторону. По имеющемуся хешу невозможно получить массив данных, для которого этот хеш получен. Вероятно, из вышесказанного ясно, насколько хеши важны для криптографии и почему мы в данной статье поговорим об их расчете в различных средах.

Нашей целью будет то же, что и раньше — выяснить, как сделать так, чтобы хеш для одинаковых исходных данных, был бы одинаков при расчетах в терминале и других, сторонних программах.

В 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# дает другой результат. Попытаемся использовать другую строку, состоящую из одного символа — "а". И опять результаты будут аналогичными — расчет в проекте на С# даст другой хеш.

Ответ кроется в вызове функции StringToCharArray, преобразующей строку в массив. Если мы после вызова StringToCharArray попытаемся посмотреть, что находится в получившемся массиве, то обнаружим, что массив увеличил длину. Например, после вызова функции со строкой "а" получившийся массив будет иметь два элемента. Второй элемент будет "0". А вызов метода Encoding.ASCII.GetBytes в проекте C# ни к чему подобному не приведет. Здесь "0" в массив включен не будет. 

Имея такой результат исследований, добавляем в проект на C# блок кода, дописывающий в байтовый массив "0" и уже после этого отправляем его на расчет хеша. Теперь результат положительный. Все три проекта рассчитывают одинаковый хеш для одинаковых исходных данных. Получившиеся хеши сохранены в файлах sha256.bin, sha256_c#.bin, sha256_С++.bin и находятся в прилагаемом архиве CryptoMQL.zip.

Стоит заметить, что приведенный пример касался текстовых данных. Очевидно, что когда речь идет об изначально двоичном массиве, то никакой вызов StringToCharArray и Encoding.ASCII.GetBytes не требуется. А следовательно и дополнительному нулю в массиве взяться будет неоткуда. Поэтому вполне возможно, что код следовало модифицировать, не прибавляя ноль к массиву в проекте на С#, но убирая ноль в конце массива в проекте на MQL.

Здесь мы остановимся и сделаем вывод о том, что нам удалось решить поставленную задачу — выяснить, при каких условиях хеш некоторого объекта будет одинаков при расчете в различных средах. Так же мы будем считать, что достигли цели, обозначенной в начале статьи. Нам удалось выяснить, какие режимы шифровки / дешифровки следует применять для совместимости результатов работы в различных средах.

Заключение

Хотя необходимость в шифровке / дешифровке не самая востребованная операция в алготрейдинге в терминале MetaTrader 5, когда такая необходимость возникнет, данная статья, вполне возможно, поможет сэкономить некоторое время на поиск информации.

Что не рассмотрели? Создание архивов, такая опция есть для функции CrypetEncode. Стандарт кодирования Base64, возможность такого кодирования также имеется. Однако, оба эти режима и не должны упоминаться здесь. Да, при создании архивов можно устанавливать пароль, но:

Еще один вариант, это кодирование при помощи Base64. Редко, но встречаются крайне ошибочные упоминания об этом стандарте в связи с криптографией. Применять этот стандарт для шифровки / дешифровке нельзя ни в коем случае! Желающие могут глубже ознакомиться с этим стандартом и узнать, для чего он применяется на практике.

Вероятно, стоило бы сказать несколько слов относительно объекта работы — лицензий. В статье выбран путь, который способствует более легкому восприятию решению задачи шифровки / дешифровки, как представляется автору. Поэтому для работы используются байтовые массивы. Их шифруют, записывают в файлы, расшифровывают и т.д. В реальной ситуации это было бы крайне неудобно и чревато ошибками. При упаковке исходных структур в массив и обратно, ошибка в один бит приведет к порче всей лицензии, а такая ситуация вполне возможна, с учетом различий в размерах отдельных типов, как это было показано выше. Поэтому для хранения лицензии стоило бы подумать о текстовом формате. И тут сразу вспоминаются xml и json. Надо полагать, стоило бы остановиться на последнем, учитывая, что для формата json имеются прекрасные парсеры для MQL,  C# и C++.


Программы, используемые в статье:

 # Имя
Тип
 Описание
1
CryptoMQL.zip Архив Архив со скриптами шифровки / дешифровки.
2 CryptoC#.zip Архив Проект C# по шифровке / дешифровке.
3 CryptoС++.zip Архив Проект C++ по шифровке / дешифровке.