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

uchar key[] = {1000};
CryptEncode(CRYPT_ARCH_ZIPdatakeyresult);

В принципе, подойдет любой ключ длиной не менее 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
{
   _CRYPT_ALL = 0xFF,                      // Try All in a Loop
   _CRYPT_DES = CRYPT_DES,                 // DES    (key required, 7 bytes)
   _CRYPT_AES128 = CRYPT_AES128,           // AES128 (key required, 16 bytes)
   _CRYPT_AES256 = CRYPT_AES256,           // AES256 (key required, 32 bytes)
   _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
};
   
input string Text = "Let's encrypt this message"// Text (empty to process File)
input string File = "MQL5Book/clock10.htm"// File (used only if Text is empty)
input ENUM_CRYPT_METHOD_EXT Method = _CRYPT_ALL;

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

Поскольку для шифрования требуется ключ, два других параметра позволяют ввести его напрямую в виде текста (хотя ключ не обязан быть текстовым и может содержать любые двоичные данные, но они не поддерживаются в input-ах) или сгенерировать нужной длины, в зависимости от метода шифрования.

enum DUMMY_KEY_LENGTH
{
   DUMMY_KEY_0 = 0,   // 0 bytes (no key)
   DUMMY_KEY_7 = 7,   // 7 bytes (sufficient for DES)
   DUMMY_KEY_16 = 16// 16 bytes (sufficient for AES128)
   DUMMY_KEY_32 = 32// 32 bytes (sufficient for AES256)
   DUMMY_KEY_CUSTOM,  // use CustomKey
};
   
input DUMMY_KEY_LENGTH GenerateKey = DUMMY_KEY_CUSTOM// GenerateKey (длина, или из CustomKey)
input string CustomKey = "My top secret key is very strong";

Наконец, для включения режима ZIP-совместимости предусмотрена опция DisableCRCinZIP — она влияет только на метод CRYPT_ARCH_ZIP.

input bool DisableCRCinZIP = false;

Для упрощения проверок, когда метод требует ключа шифрования, а когда рассчитывается хэш (необратимое одностороннее преобразование), определено 2 макроса.

#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 описаны требуемые переменные и массивы.

void OnStart()
{
   ENUM_CRYPT_METHOD method = 0;
   int methods[];           // сюда соберем все элементы ENUM_CRYPT_METHOD для цикла по ним
   uchar key[] = {};        // пусто по умолчанию: подходит для хэширования, zip, base64
   uchar zip[], opt[] = {1000}; // "опции" для zip
   uchar data[], result[];  // исходные данные и результат

Согласно настройке GenerateKey, получаем ключ из поля CustomKey или просто заполняем массив key монотонно возрастающими целыми значениями. В реальности ключ должен представлять собой секретный, нетривиальный, произвольно выбранный блок значений.

   if(GenerateKey == DUMMY_KEY_CUSTOM)
   {
      if(StringLen(CustomKey))
      {
         PRTF(CustomKey);
         StringToCharArray(CustomKeykey0, -1, CP_UTF8);
         ArrayResize(keyArraySize(key) - 1);
      }
   }
   else if(GenerateKey != DUMMY_KEY_0)
   {
      ArrayResize(keyGenerateKey);
      for(int i = 0i < GenerateKey; ++ikey[i] = (uchar)i;
   }

Обратите внимание здесь и далее на использование ArrayResize после StringToCharArray. Обязательно сокращайте массив на 1 элемент, поскольку случае функция StringToCharArray преобразует строку в массив байтов, включая терминальный 0, а это способно нарушить ожидаемое выполнение программы. В частности, в данном случае у нас появится лишний нулевой байт в секретном ключе, и если на принимающей стороне не используется программа с аналогичным артефактом, то там не смогут расшифровать посылку. Подобные лишние нули могут сказаться и на совместимости с протоколами обмена данными (если производится та или иная интеграция MQL-программы с "внешним миром").

Далее мы выводим в журнал "сырое" представление получившегося ключа в шестнадцатеричном формате: это делает функция ByteArrayPrint, уже использовавшаяся в разделе Запись и чтение файлов в упрощенном режиме, но, как и там, мы оставим её исходный код для самостоятельного изучения.

   if(ArraySize(key))
   {   
      Print("Key (bytes):");
      ByteArrayPrint(key);
   }
   else
   {
      Print("Key is not provided");
   }

В зависимости от наличия Text или File, заполняем массив data либо символами текста, либо содержимым файла.

   if(StringLen(Text))
   {
      PRTF(Text);
      PRTF(StringToCharArray(Textdata0, -1, CP_UTF8));
      ArrayResize(dataArraySize(data) - 1);
   }
   else if(StringLen(File))
   {
      PRTF(File);
      if(PRTF(FileLoad(Filedata)) <= 0)
      {
         return// ошибка
      }
   }

Наконец, запускаем цикл по всем методам или однократно выполняем преобразование конкретным методом.

   const int n = (Method == _CRYPT_ALL) ?
      EnumToArray(methodmethods0UCHAR_MAX) : 1;
   ResetLastError();
   for(int i = 0i < 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(zipopt); // массив с доп.опцией динамический для ArraySwap
         }
         ArraySwap(keyzip); // подменяем ключ на пустой или опцию
      }
      
      if(PRTF(CryptEncode(methoddatakeyresult)))
      {
         if(StringLen(Text))
         {
            // кодовая страница Latin (Western) для унификации отображения у всех пользователей
            Print(CharArrayToString(result0WHOLE_ARRAY1252));
            ByteArrayPrint(result);
            if(method != CRYPT_BASE64)
            {
               const uchar dummy[] = {};
               uchar readable[];
               if(PRTF(CryptEncode(CRYPT_BASE64resultdummyreadable)))
               {
                  PrintFormat("Try to decode this with CryptDecode.mq5 (%s):",
                     EnumToString(method));
                  // чтобы принять закодированные данные обратно для раскодировки
                  // через строковый input, применим Base64 поверх двоичного результата
                  Print("base64:'" + CharArrayToString(readable0WHOLE_ARRAY1252) + "'");
               }
            }
         }
         else
         {
            string parts[];
            const string filename = File + "." +
               parts[StringSplit(EnumToString(method), '_', parts) - 1];
            if(PRTF(FileSave(filenameresult)))
            {
               Print("File saved: "filename);
               if(IS_HASH(method))
               {
                  ByteArrayPrint(result1000"");
               }
            }
         }
      }
   }
}

Когда мы конвертируем текст, мы выводим результат в журнал, но поскольку это почти всегда двоичные данные, за исключением метода CRYPT_BASE64, их отображение будет представлять собой абракадабру (по-хорошему, двоичные данные не стоит выводить в журнал, но мы это делаем для наглядности). Непечатаемые символы и символы с кодами больше 128 по-разному выводятся на компьютерах с разными языками. Поэтому, в целях унификации отображения примеров у всех читателей, мы используем при формировании строки в CharArrayToString явно заданную кодовую страницу (1252, Западноеропейские языки). Правда, используемые при публикации книги шрифты, скорее всего, внесут свою лепту в то, как именно будут отображаться те или иные знаки (набор глифов в шрифтах может быть ограничен).

Важно отметить, что выбором кодовой страницы мы управляем лишь способом отображения, а байты в массиве 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. В результате программа обработает файл 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). Например, для текстового сообщения по умолчанию выберем в параметре Method 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)

Также вы можете сжать один и тот же файл (как мы делали с clock10.htm) и скриптом методом CRYPT_ARCH_ZIP, и штатным архиватором. Если потом заглянуть с помощью утилиты просмотра двоичных файлов (которая встроена обычно в файловый менеджер) то оба результата покажут общий упакованный блок, а различия будут лишь в обрамляющих его мета-данных.

Сравнение файла, сжатого методом CRYPT_ARCH_ZIP (слева), и стандартного ZIP-архива с ним (справа)

Сравнение файла, сжатого методом CRYPT_ARCH_ZIP (слева), и стандартного ZIP-архива с ним (справа)

Здесь показано, что середину и основную часть архива составляет последовательность байтов (выделенных темным), идентичных тем, что выдает функция CryptEncode, сжимавшая ту же веб-страницу.

Напоследок покажем, как было сгенерировано текстовое Base64-представление графического файла clock10.png. Для этого очистим поле 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-файлов не реализована.

#resource "\\Images\\euro.bmp"
   
#include <MQL5Book/PNG.mqh>
   
void OnStart()
{
   uchar null[];      // пустой ключ для CRYPT_ARCH_ZIP
   uchar result[];    // приемный массив
   uint data[];       // исходные пиксели
   uchar bytes[];     // исходные байты
   int widthheight;
   PRTF(ResourceReadImage("::Images\\euro.bmp"datawidthheight));
   
   ArrayResize(bytesArraySize(data) * 3 + width); // *3 для PNG_CTYPE_TRUECOLOR (RGB)
   ArrayInitialize(bytes0);
   int j = 0;
   for(int i = 0i < ArraySize(data); ++i)
   {
      if(i % width == 0bytes[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_ZIPbytesnullresult));
   
   int h = PRTF(FileOpen("my.png"FILE_BIN | FILE_WRITE));
   
   PNG::Image image(widthheightresult); // по умолчанию PNG_CTYPE_TRUECOLOR (RGB)
   image.write(h);
   
   FileClose(h);
}