English Deutsch 日本語
preview
Нативная реализация RSA-шифрования на MQL5

Нативная реализация RSA-шифрования на MQL5

MetaTrader 5Интеграция |
50 0
Vahid Irak
Vahid Irak

Представьте распространённую ситуацию: вашему советнику нужно отправить на сервер сигналы, ключи, данные для входа или другую важную информацию. Вы можете использовать HTTP или даже HTTPS и считать соединение достаточно безопасным. Но рано или поздно выясняется, что настоящая уязвимость находится вовсе не в соединении, а внутри самого кода.

В MQL5 бывают ситуации, когда у вас просто нет выбора и приходится встраивать чувствительные значения напрямую в EA. Параметры алгоритмов, приватные ключи, пароли — всё это может оказаться скомпилированным в файл. И хотя код скомпилирован и обфусцирован, достаточно квалифицированный человек всё ещё может извлечь эти значения. Это создаёт серьёзную проблему безопасности: транспорт может быть зашифрован, но сам советник остаётся потенциальной точкой утечки, потому что ему приходится хранить секреты внутри себя. При этом использование DLL для шифрования запрещено для продуктов Market. MQL5 не предоставляет RSA «из коробки», а лёгкие приёмы «шифрования» вроде XOR не дают реальной защиты. Многие разработчики упираются именно в это ограничение: им нужен способ защитить коммуникацию и ключи, но доступные инструменты либо ограничены, либо просто недостаточно надёжны.

Эта статья показывает, как обойти эти ограничения. В ней объясняется, как сделать так, чтобы ваш Expert Advisor (EA) больше не хранил внутри себя критически важные данные, а безопасно получал их с доверенного сервера в зашифрованном виде. Вы также узнаете, как реализовать корректное гибридное шифрование — RSA для обмена ключами и AES для быстрой передачи сообщений — полностью на MQL5, без DLL и внешних библиотек.


Введение в RSA

Название «RSA» образовано из первых букв фамилий Рона Ривеста, Ади Шамира и Леонарда Адлемана, которые представили алгоритм в 1977 году во время работы в MIT. Их работа стала первой практической реализацией криптосистемы с открытым ключом на основе однонаправленных математических функций, превратив теоретическую идею в пригодную для использования технологию. Хотя концепция криптографии с открытым ключом была предложена ранее Диффи и Хеллманом в 1976 году, именно конструкция RSA дала конкретный механизм для безопасного шифрования и цифровых подписей.

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

Сегодня RSA остаётся одним из самых широко используемых алгоритмов асимметричного шифрования и является важной частью таких стандартов, как SSL/TLS, PGP, SSH, а также многих систем защищённой связи. Несмотря на появление эллиптической криптографии (ECC) и других современных альтернатив, RSA продолжает цениться за простоту, надёжность и давно устоявшуюся модель безопасности.



Математика, лежащая в основе 

RSA — простой и элегантный алгоритм:

Работает он так: вы шифруете сообщение открытым ключом, а расшифровать его и восстановить исходный текст может только соответствующий закрытый ключ. Основа RSA опирается на формулу, показанную ниже. Если вам интересно, кто её открыл, заслуга принадлежит выдающемуся математику Леонарду Эйлеру. Теорема Эйлера (также известная как теорема Ферма–Эйлера или теорема Эйлера о функции Эйлера) является фундаментом системы RSA. Подробнее о ней можно прочитать в этой статье.

Основная формула

Вот что означают эти буквы:

  • m: ваше сообщение (просто записанное как число).
  • и n: вместе образуют открытый ключ.
  • d: это закрытый ключ.
  • n: называется модулем.

Рассмотрим основное уравнение подробнее. Даже при ограниченных знаниях математики можно заметить, что к сообщению (m) применяются определённые математические операции, а результат этих операций восстанавливает исходное сообщение. Это даёт идею разделить уравнение на две асимметричные части, используя распределительное свойство операций по модулю. Сообщение m можно зашифровать, возведя его в степень e по модулю n, в результате чего получается шифртекст, Z (зашифрованное сообщение).

Формула шифрования

А зашифрованное сообщение Z можно расшифровать с помощью закрытого ключа d

Формула расшифрования

Продемонстрируем эту концепцию на простом числовом примере с m = 2, e = 3, d = 3 и n = 15. Используя формулу шифрования, получаем:

Z = (m ^ emod n = 2^3 mod 15 = 8 

Теперь зашифрованное значение можно расшифровать с помощью закрытого ключа d = 3:

m = (Z^d) mod n = 8^3 mod 15 2

Мы даже можем написать небольшую программу на MQL5, реализующую приведённый выше числовой пример:

//+------------------------------------------------------------------+
//| Simple RSA Numerical Example (for educational purposes only)     |
//+------------------------------------------------------------------+

// Fast modular exponentiation: computes (base^exp) % mod
int ModPow(int base, int exp, int mod)
{
   long result = 1;
   long b = base % mod;

   while(exp > 0)
   {
      if(exp & 1)
         result = (result * b) % mod;

      b = (b * b) % mod;
      exp >>= 1;
   }
   return (int)result;
}

// RSA "encryption": c = m^e % n
int EncryptRSA(int m, int e, int n)
{
   return ModPow(m, e, n);
}

// RSA "decryption": m = c^d % n
int DecryptRSA(int c, int d, int n)
{
   return ModPow(c, d, n);
}

//+------------------------------------------------------------------+
//| Example based on small numbers (not secure!)                     |
//+------------------------------------------------------------------+
void OnStart()
{
   int m = 2;     // message
   int e = 3;     // public exponent
   int d = 3;     // private exponent
   int n = 15;    // modulus

   Print("Original message: ", m);

   int encrypted = EncryptRSA(m, e, n);
   Print("Encrypted: ", encrypted, "   // 2^3 % 15 = 8");

   int decrypted = DecryptRSA(encrypted, d, n);
   Print("Decrypted: ", decrypted, "   // 8^3 % 15 = 2");

   // Verifying the main RSA concept:
   // (m^e)^d % n = m
   int check = ModPow(ModPow(m, e, n), d, n);
   Print("Check (m^e)^d % n = ", check);
}

Советы и приёмы: 

Сначала нужно преобразовать текст в числа, прежде чем его можно будет зашифровать. Именно здесь используются ASCII или Unicode — каждая буква, пробел или символ превращается в число. Возьмём, например, «Hello world!». Его можно записать как одно огромное число: в десятичном виде → 22405534230753963835153736737 или в шестнадцатеричном → 0x48656c6c6f20776f726c6421. Однако модуль не может быть маленьким числом вроде 15; такое значение слишком мало для реальных сообщений. Модуль RSA должен быть больше числового представления сообщения, иначе математика шифрования не будет работать корректно. RSA-шифрование вычислительно затратно. Шифрование больших чисел с большими показателями степени, например 65537, требует множества умножений. Без оптимизации процессор может быть сильно загружен.

С точки зрения безопасности одного базового RSA-шифрования недостаточно. Оно уязвимо к атакам повторного воспроизведения и другим потенциальным эксплойтам. Чтобы устранить это, требуется дополнение. Например, схема дополнения PKCS#1 добавляет случайные байты перед фактическим сообщением, повышая безопасность за счёт предотвращения предсказуемых шаблонов шифртекста.
[0x00][0x02][random bytes][0x00][message]

При использовании дополнения повторная отправка одного и того же сообщения каждый раз даёт разные зашифрованные результаты. Это значительно усложняет злоумышленнику взлом шифрования. Без правильного дополнения и корректной работы с ключами RSA становится уязвимым к нескольким классам атак. На следующем шаге мы покажем, как на практике сгенерировать корректную пару ключей.

Генерация ключей с помощью OpenSSL:

Сначала убедитесь, что OpenSSL установлен в вашей операционной системе. RSA-ключи можно сгенерировать следующей командой OpenSSL (среда Windows):
//bash
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
Открытые ключи и публичные сертификаты содержат открытый показатель степени и модуль, а закрытые ключи — закрытый показатель степени. Следующая команда выводит компоненты как открытого, так и закрытого ключа:
//bash
openssl rsa -in private_key.pem -text -noout
openssl rsa -in public_key.pem -pubin -text -noout
После выполнения этих команд вы увидите вывод, похожий на изображение ниже.

сгенерированные закрытый и открытый ключи

Именно так браузеры обрабатывают защищённые соединения SSL или TLS. Итак, RSA обеспечивает безопасную связь даже по незащищённым каналам. Для промышленной эксплуатации важно применять правильное дополнение и выбирать достаточно большой ключ — 2048 бит или больше. Такие инструменты, как OpenSSL, упрощают генерацию ключей и управление ими. Для разработки можно найти и другие онлайн-инструменты для RSA-шифрования, расшифрования или даже генерации ключей, но никогда не доверяйте им в рабочей среде. Хотя Windows предоставляет функциональность RSA через CryptoAPI, вызов внешних DLL добавляет сложность и потенциальные риски безопасности, особенно для Expert Advisors (EA). Когда клиенты используют ваш EA и видят, что ему требуется доступ к DLL, они могут насторожиться или даже потерять доверие к вашему продукту.


Проектирование алгоритма (шаг за шагом)

Процесс шифрования состоит из нескольких этапов, и для удобства понимания объяснение кода разделено на три части: Часть 1 охватывает основные концепции и инициализацию требований класса. Часть 2 вводит необходимые арифметические функции для больших целых чисел. А Часть 3 представляет итоговую реализацию процесса RSA-шифрования.

      Часть 1

      Создание класса RSA:

          Основной класс MQL5_RSA инкапсулирует полный рабочий процесс шифрования и включает функции преобразования форматов данных, такие как Base64 и HexToByte. Он реализует арифметику больших чисел, схему дополнения PKCS#1 v1.5 для внесения случайности в сообщение и необязательный режим отладки для трассировки операций.
            Начнём с определения класса RSA, который хранит модуль, показатель степени и вспомогательные методы. Он также один раз инициализирует генератор случайных чисел, что важно, поскольку схема дополнения PKCS#1 v1.5 требует ненулевых случайных байтов.
            public class MQL5_RSA
            {
            private:
               uchar modulus[];  //will store the RSA modulus (n) as a big-endian byte array.
               int   exponent;   // holds the public exponent (usually 65537).
               bool  debugMode;  //toggles console logging.
            
            public:
               MQL5_RSA(bool debug=false)
               {
                  debugMode = debug;
                  MathSrand((int)TimeLocal());   // seed RNG once
               }
            };
            Инициализация:

              Загрузите модуль (открытый ключ) как шестнадцатеричную строку, преобразуйте её в байты, нормализуйте и сохраните открытый показатель степени (обычно 65537).

              void Init(string modulusHex, int e)
              {
                 ArrayResize(modulus, 0);  // clear previous values
              
                 if(debugMode)
                    PrintFormat("Init: modulus hex len=%d, e=%d", StringLen(modulusHex), e);
              
                 HexToBytes(modulusHex, modulus);  // convert hex → bytes
                 Normalize(modulus);               // remove leading zeros (important!)
                 exponent = e;
              
                 if(debugMode)
                    PrintFormat("Init completed: modulus bytes=%d", ArraySize(modulus));
              }
              

              Исправление и написание HexToBytes():

              Эта функция критически важна, потому что в исходном коде MetaQuotes была ошибка: он добавляет коды символов, а не hex-пары. Мы напишем корректную и требуемую версию:

              void HexToBytes(string hex, uchar &out[])
              {
                 string clean = "";
                 int L = StringLen(hex);
              
                 // keep only valid hex characters
                 for(int i = 0; i < L; i++)
                 {
                    ushort c = StringGetCharacter(hex, i);
                    if((c >= '0' && c <= '9') ||
                       (c >= 'A' && c <= 'F') ||
                       (c >= 'a' && c <= 'f'))
                       clean += StringSubstr(hex, i, 1);
                 }
              
                 // ensure even number of hex chars (pad if necessary)
                 if(StringLen(clean) % 2 == 1)
                    clean = "0" + clean;
              
                 int n = StringLen(clean) / 2;
                 ArrayResize(out, n);
              
                 for(int i = 0; i < n; i++)
                 {
                    ushort c1 = StringGetCharacter(clean, i*2);
                    ushort c2 = StringGetCharacter(clean, i*2+1);
              
                    int high = HexNibble(c1);   // convert 1 hex char -> 0..15
                    int low  = HexNibble(c2);
              
                    out[i] = (uchar)((high << 4) | low); // combine two nibbles → byte
                 }
              }
              

              Нормализация больших целых чисел:

              Числа RSA представлены массивами в формате big-endian. Это означает, что индекс 0 содержит самый значимый байт. Ведущие нулевые байты нам не нужны, поэтому мы их удаляем:

              void Normalize(uchar &a[])
              {
                 int size = ArraySize(a);
                 int leading = 0;
              
                 // count leading zero bytes
                 while(leading < size && a[leading] == 0)
                    leading++;
              
                 if(leading == 0)
                    return;  // already normalized
              
                 int newSize = size - leading;
                 if(newSize <= 0)
                 {
                    ArrayResize(a, 0);
                    return;
                 }
              
                 uchar temp[];
                 ArrayResize(temp, newSize);
              
                 // copy only the non-zero part
                 ArrayCopy(temp, a, 0, leading, newSize);
              
                 ArrayResize(a, newSize);
                 ArrayCopy(a, temp);
              }
              

              На этом этапе класс RSA предоставляет базовую функциональность ядра. Класс хранит флаг режима отладки, принимает и нормализует модуль, корректно преобразует шестнадцатеричные значения в байты, удаляет ведущие нули и сохраняет значение показателя степени. Эта основа необходима перед реализацией операций арифметики больших целых чисел, таких как вычитание, сравнение и умножение, а затем — модульная арифметика и схема дополнения PKCS#1. После добавления этих возможностей процесс RSA-шифрования можно выполнять.

              Часть 2

              Создание движка больших целых чисел: Внутреннее устройство RSA

              Далее мы строим систему арифметики больших целых чисел, необходимую для RSA. В MQL5 нет встроенного типа big-integer, поэтому RSA должен имитировать большие целые числа с помощью массивов байтов. В этом разделе разрабатывается математическая основа, поддерживающая схему дополнения PKCS#1 и модульное возведение в степень. Будут реализованы следующие функции:

              1. Compare();
              2. SubtractInPlace();
              3. LeftShiftBytes();
              4. Multiply();
              5. Mod();
              6. MulMod();

              В совокупности эти функции имитируют настоящие операции арифметики больших целых чисел.

              1. Compare() — определение, какое большое целое число больше:

              Перед выполнением вычитания, деления или операций по модулю необходимо уметь сравнивать два числа.
              a > b
                or
              a < b
                or
              a == b
              int Compare(const uchar &a_in[], const uchar &b_in[])
              {
                 // copy to avoid modifying original arrays
                 uchar a[];
                 ArrayCopy(a, a_in);
                 uchar b[];
                 ArrayCopy(b, b_in);
              
                 Normalize(a);
                 Normalize(b);
              
                 int na = ArraySize(a), nb = ArraySize(b);
              
                 // first compare lengths
                 if(na > nb) return 1;
                 if(na < nb) return -1;
              
                 // lengths equal → compare byte by byte
                 for(int i=0; i<na; i++)
                 {
                    if(a[i] > b[i]) return 1;
                    if(a[i] < b[i]) return -1;
                 }
              
                 return 0; // equal
              }
              

              2. SubtractInPlace() — вычитание в формате big-endian

              Эта функция выполняет a = a − b. Операция выполняется байт за байтом, начиная справа, поскольку последний байт представляет наименее значимую часть числа. Заём приходится обрабатывать вручную. Эта функция широко используется в Mod() на этапе редукции длинным делением, а также встречается в промежуточных шагах умножения.

              void SubtractInPlace(uchar &a[], const uchar &b[])
              {
                 // assume a >= b (Compare() must ensure this)
                 int na = ArraySize(a);
                 int nb = ArraySize(b);
                 int borrow = 0;
              
                 for(int i = 0; i < na; i++)
                 {
                    int ai = (int)a[na - 1 - i];               // rightmost byte of a
                    int bi = (i < nb) ? (int)b[nb - 1 - i] : 0; // rightmost byte of b
              
                    int diff = ai - bi - borrow;
              
                    if(diff < 0)
                    {
                       diff += 256;  // wrap around as unsigned byte
                       borrow = 1;
                    }
                    else
                    {
                       borrow = 0;
                    }
              
                    a[na - 1 - i] = (uchar)diff;
                 }
              
                 Normalize(a); // remove leading zeros
              }
              

              3. LeftShiftBytes() — умножение на 256ⁿ:

              Во время операций длинного деления часто требуется сдвигать число влево, что фактически умножает его на степени 256 (то есть выполняет сдвиг на целые байты).
              void LeftShiftBytes(const uchar &in[], int shiftBytes, uchar &out[])
              {
                 int n = ArraySize(in);
                 if(n == 0 || shiftBytes == 0)
                 {
                    ArrayCopy(out, in);
                    return;
                 }
                 ArrayResize(out, n + shiftBytes);
              
                 // copy original bytes
                 for(int i=0; i<n; i++)
                    out[i] = in[i];
                 // append zeros on the right (least significant side)
                 for(int i=n; i<n+shiftBytes; i++)
                    out[i] = 0;
              }
              При вычислении модуля делитель можно сдвигать до тех пор, пока он не выровняется с делимым.

              Это ядро алгоритма длинного деления.

              4. Multiply() — умножение «столбиком»:

              Стандартная функция умножения реализуется классическим методом «столбиком» следующим образом:
              void Multiply(const uchar &a[], const uchar &b[], uchar &result[])
              {
                 int na = ArraySize(a);
                 int nb = ArraySize(b);
              
                 if(na==0 || nb==0)
                 {
                    ArrayResize(result, 0);
                    return;
                 }
              
                 int nRes = na + nb;
                 int temp[];
                 ArrayResize(temp, nRes);
                 ArrayInitialize(temp, 0);  // accumulator array (int for safety)
              
                 // classic schoolbook multiplication
                 for(int i = na - 1; i >= 0; i--)
                 {
                    int carry = 0;
              
                    for(int j = nb - 1; j >= 0; j--)
                    {
                       int prod = (int)a[i] * (int)b[j] + temp[i + j + 1] + carry;
                       temp[i + j + 1] = prod % 256;
                       carry = prod / 256;
                    }
              
                    temp[i] += carry;
                 }
              
                 ArrayResize(result, nRes);
                 for(int i=0; i<nRes; i++)
                    result[i] = (uchar)temp[i];
              
                 Normalize(result);
              }
              

              Это умножение — самая медленная часть чистой реализации RSA на MQL5, но оно необходимо для функций MulMod() и ModExp(). Эти функции будут подробно рассмотрены далее.

              5. Mod(), модуль большого целого числа с использованием длинного деления:

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

              bool Mod(const uchar &a_in[], const uchar &m_in[], uchar &result[])
              {
                 if(ArraySize(m_in) == 0) return false;
              
                 uchar dividend[]; ArrayCopy(dividend, a_in); Normalize(dividend);
                 uchar modv[];     ArrayCopy(modv, m_in);   Normalize(modv);
              
                 // if dividend < modulus → done
                 if(Compare(dividend, modv) < 0)
                 {
                    ArrayCopy(result, dividend);
                    return true;
                 }
              
                 int m = ArraySize(dividend);
                 uchar rem[];
                 ArrayResize(rem, 0);
              
                 for(int i = 0; i < m; i++)
                 {
                    // shift remainder left by 1 byte
                    int rlen = ArraySize(rem);
                    ArrayResize(rem, rlen + 1);
                    rem[rlen] = dividend[i];
                    Normalize(rem);
              
                    // subtract modulus while remainder >= modulus
                    while(Compare(rem, modv) >= 0)
                    {
                       SubtractInPlace(rem, modv);
                    }
                 }
              
                 Normalize(rem);
                 ArrayCopy(result, rem);
                 return true;
              }
              

              6. MulMod() — умножение с последующим взятием по модулю:

              Это просто соединяет две операции:

              Multiply(a, b) → temp
              Mod(temp, m)   → out

              bool MulMod(const uchar &a[], const uchar &b[], const uchar &m[], uchar &out[])
              {
                 uchar product[];
                 Multiply(a, b, product);
                 return Mod(product, m, out);
              }

              Часть 3

              Теперь у нас есть все необходимые инструменты для арифметики RSA. Сначала к сообщению нужно применить схему дополнения PKCS#1 v1.5 перед шифрованием. Затем реализуется функция ModExp(), вычисляющая: result = base^exp mod n. Это ключевая операция RSA, формирующая шифртекст. Для удобного представления результата используется вспомогательная функция Base64Encode(), которая позволяет выводить шифртекст в текстовом виде.

              Модульное возведение в степень (ModExp) — сердце RSA:

                Функция ModExp() эффективно вычисляет result = base^exp mod n для больших показателей степени с помощью повторного возведения в квадрат и модульного умножения. Она опирается на MulMod() (умножить, затем сократить по модулю), благодаря чему все промежуточные значения остаются достаточно малыми для обработки.

                bool ModExp(const uchar &base[], int exp, const uchar &modn[], uchar &result[])
                {
                   if(debugMode)
                      PrintFormat("ModExp: exp=%d", exp);
                
                   // Work on a copy to avoid mutating caller arrays
                   uchar baseCopy[];
                   ArrayCopy(baseCopy, base);
                   Normalize(baseCopy);
                
                   // Reduce base modulo modn first: base = base % modn
                   if(Compare(baseCopy, modn) >= 0)
                   {
                      if(!Mod(baseCopy, modn, baseCopy))
                         return false; // mod failed
                   }
                
                   // Initialize result = 1 (big-int representation)
                   uchar res[];
                   ArrayResize(res, 1);
                   res[0] = 1;
                   Normalize(res);
                
                   // basePow holds current power of base (base^(2^i))
                   uchar basePow[];
                   ArrayCopy(basePow, baseCopy);
                
                   int e = exp; // local copy so we can shift it
                
                   // Square-and-multiply loop
                   while(e > 0)
                   {
                      // If current LSB is 1 → multiply into result
                      if((e & 1) == 1)
                      {
                         uchar tmp[];
                         if(!MulMod(res, basePow, modn, tmp))
                            return false; // MulMod failed
                         ArrayCopy(res, tmp); // res = (res * basePow) % modn
                      }
                
                      // Move to next bit
                      e >>= 1;
                
                      // If still bits left, square basePow: basePow = (basePow * basePow) % modn
                      if(e > 0)
                      {
                         uchar tmp2[];
                         if(!MulMod(basePow, basePow, modn, tmp2))
                            return false; // MulMod failed
                         ArrayCopy(basePow, tmp2);
                      }
                   }
                
                   Normalize(res);
                   ArrayCopy(result, res); // return result via out param
                   return true;
                }
                

                Схема дополнения PKCS#1 v1.5 и шифрование (EncryptPKCS1v15):

                Сначала нужно сформировать сообщение с дополнением EM = 0x00 || 0x02 || PS || 0x00 || M, где PS — ненулевые случайные байты, и убедиться, что длина сообщения меньше k - 11 (k = длина модуля в байтах), поскольку служебные байты учитываются в итоговой длине сообщения.

                bool EncryptPKCS1v15(uchar &plain[], uchar &cipher[])
                {
                   int k = ArraySize(modulus);      // modulus size (bytes)
                   int mlen = ArraySize(plain);     // message length (bytes)
                
                   if(debugMode)
                      PrintFormat("Encrypt: key bytes=%d, plain=%d, max=%d", k, mlen, k-11);
                
                   // PKCS#1 v1.5 requires at least 11 bytes of overhead
                   if(mlen > k - 11)
                   {
                      if(debugMode) Print("Error: data too large for key");
                      return false;
                   }
                
                   // Build the encoded message EM (k bytes)
                   uchar em[];
                   ArrayResize(em, k);
                   ArrayInitialize(em, 0);    // default everything to 0
                
                   em[0] = 0x00;              // leading zero by spec
                   em[1] = 0x02;              // block type 2 (encryption)
                
                   int psLen = k - mlen - 3;  // length of padding string PS
                
                   // Fill PS with non-zero random bytes
                   for(int i=0; i<psLen; i++)
                   {
                      uchar b = 0;
                      // loop until rand byte is non-zero (spec requirement)
                      do
                      {
                         b = (uchar)(MathRand() % 256);
                      }
                      while(b == 0);
                
                      em[2 + i] = b;
                   }
                
                   // Separator before message
                   em[2 + psLen] = 0x00;
                
                   // Copy message into EM at the right position
                   ArrayCopy(em, plain, 3 + psLen, 0, mlen);
                
                   if(debugMode) Print("EM constructed");
                
                   // Modular exponentiation: C = EM^e mod n
                   uchar cBig[];
                   if(!ModExp(em, exponent, modulus, cBig))
                   {
                      if(debugMode) Print("ModExp failed");
                      return false;
                   }
                
                   // Ensure ciphertext byte array has exact length k
                   int clen = ArraySize(cBig);
                   ArrayResize(cipher, k);
                   if(clen < k)
                   {
                      // left-pad with zeros
                      ArrayInitialize(cipher, 0);
                      ArrayCopy(cipher, cBig, k - clen, 0, clen);
                   }
                   else
                   {
                      ArrayCopy(cipher, cBig); // already k bytes or longer (shouldn't be longer normally)
                   }
                
                   if(debugMode) Print("Encryption finished");
                   return true;
                }
                

                Строка заполнения (PS) должна состоять из ненулевых случайных байтов. В этой реализации используется MathRand(), инициализированный в конструкторе. Хотя это лучше, чем неинициализированный генератор случайных чисел, он не является криптографически стойким. В промышленных системах следует использовать криптографически стойкий ГСЧ. Схема дополнения PKCS#1 v1.5 широко поддерживается, но имеет известные уязвимости, например атаки с оракулом дополнения. Для новых систем предпочтителен RSAES-OAEP. Функция дополнения возвращает false, если сообщение слишком длинное для выбранного ключа.

                Вспомогательная функция вывода Base64 (Base64Encode):

                Шифртексты — это бинарные данные. Для журналирования, передачи по HTTP или хранения в текстовом виде Base64 предоставляет удобное и стандартизованное представление.

                string Base64Encode(uchar &data[])
                {
                   string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
                   int len = ArraySize(data);
                   string out = "";
                
                   // process 3 bytes => 4 base64 chars
                   for(int i = 0; i < len; i += 3)
                   {
                      int b0 = data[i];
                      int b1 = (i + 1 < len) ? data[i + 1] : 0;
                      int b2 = (i + 2 < len) ? data[i + 2] : 0;
                      int val = (b0 << 16) | (b1 << 8) | b2;
                
                      // append 4 chars, using '=' padding for missing bytes
                      out += StringSubstr(chars, (val >> 18) & 0x3F, 1);
                      out += StringSubstr(chars, (val >> 12) & 0x3F, 1);
                      out += (i + 1 < len) ? StringSubstr(chars, (val >> 6) & 0x3F, 1) : "=";
                      out += (i + 2 < len) ? StringSubstr(chars, val & 0x3F, 1) : "=";
                   }
                
                   return out;
                }
                


                Собираем всё вместе 

                Чтобы сохранить читаемость, полный исходный код класса RSA здесь не приводится. Полная реализация предоставлена в виде библиотеки RSA.mqh доступной для скачивания внизу страницы. Библиотека содержит все компоненты, рассмотренные в статье, а также пример с комментариями, демонстрирующий использование класса. Её можно использовать для быстрой интеграции либо расширять и изменять после понимания внутреннего устройства. Если возникнут ошибки или потребуется уточнение по какой-либо части реализации, читателям предлагается оставить комментарий ниже.

                Примеры использования

                Базовая реализация RSA:

                Этот простой пример показывает, как может быть реализовано RSA-шифрование. Сначала создайте экземпляр класса RSA, затем подготовьте сообщение (данные, которые нужно зашифровать) как uchar[], где каждый элемент — один байт. Наконец, вызовите EncryptPKCS1v15(plain, cipher), чтобы выполнить шифрование. Результат представляет собой массив char и сохраняется в cipherData[]:

                void OnStart()
                {
                   MQL5_RSA rsa;
                   string modulusHex = "A1B2C3D4E5F6..."; // Your modulus in hex
                   int exponent = 65537; // exponent value
                
                   rsa.Init(modulusHex, exponent);
                
                   string plainText = "Hello RSA!"; //Data that you want to be encrypted.
                   uchar plainData[];
                   StringToCharArray(plainText, plainData, 0, StringLen(plainText));
                
                   uchar cipherData[];
                   if(rsa.EncryptPKCS1v15(plainData, cipherData))
                   {
                      string base64Cipher = rsa.Base64Encode(cipherData);
                      Print("Encrypted: ", base64Cipher);
                   }
                   else
                   {
                      Print("Encryption failed!");
                   }
                }

                Использование RSA + AES в реальном проекте:

                Гибридное шифрование AES-RSA

                В реальных криптографических системах — включая защищённые торговые платформы, веб-серверы и распределённые API — RSA редко используется для прямого шифрования больших данных. RSA-шифрование вычислительно затратно и ограничено размером ключа, поэтому оно не подходит для крупных полезных нагрузок. Чтобы получить одновременно высокую производительность и сильную безопасность, RSA обычно комбинируют с AES в гибридной модели шифрования, использующей преимущества обоих алгоритмов.

                Рабочий процесс гибридного шифрования обычно состоит из следующих шагов:

                1. Сгенерировать случайный сеансовый ключ AES (например, 128 или 256 бит).
                2. Зашифровать этот AES-ключ с помощью открытого ключа RSA. Позже расшифровать его сможет только владелец соответствующего закрытого ключа.
                3. Зашифровать фактическую полезную нагрузку данных с помощью AES — быстрого симметричного шифра, идеально подходящего для больших сообщений или файлов.
                4. Передать оба компонента вместе:
                  • Данные, зашифрованные AES (быстро и компактно).
                  • AES-ключ, зашифрованный RSA (маленький, но защищённый).
                Такой подход объединяет преимущества обоих алгоритмов. RSA обеспечивает безопасный обмен ключами между двумя сторонами без заранее общего секрета, а AES обеспечивает высокоскоростное шифрование фактических данных. В этой модели RSA выступает защитником ключа, а AES — защитником данных. Вместе они образуют основу современных протоколов защищённой связи, таких как SSL/TLS, HTTPS и VPN-туннели.

                В контексте MQL5 эта стратегия позволяет разработчикам защищать коммуникацию между Expert Advisors, индикаторами и внешними серверами даже при использовании обычных HTTP- или сокет-соединений. Реализованный в этой статье класс RSA можно использовать для шифрования AES-ключа, а встроенные функции MQL5 CryptEncode() и CryptDecode() — для AES-шифрования и расшифрования фактического сообщения.

                Это позволяет создать полностью самодостаточный слой безопасности внутри MetaTrader 5, фактически формируя облегчённую версию «HTTPS поверх HTTP». Его можно использовать для защиты чувствительных данных, таких как торговые команды, учётные данные аутентификации или конфигурационные сообщения, без внешних библиотек шифрования или DLL.

                Шаг 1— сервер генерирует открытый ключ RSA:

                Используйте Python, Java, OpenSSL или любую серверную среду, чтобы сгенерировать модуль (шестнадцатеричную строку), показатель степени (обычно 65537) и закрытый ключ (хранится на стороне вашего сервера).

                EA будет использовать только открытые значения. Не раскрывайте закрытый ключ на стороне клиента.

                modulusHex = "A1B2C3…";
                exponent   = 65537;

                Шаг 2— EA создаёт экземпляр RSA и загружает ключ:

                #include <RSA.mqh>  
                
                MQL5_RSA rsa;  
                rsa.Init(modulusHex, exponent);

                Шаг 3 — EA подготавливает сообщение запроса:

                Пример полезной нагрузки может быть запросом входа, содержащим: ID EA, номер счёта и timestamp. Сформируйте корректную JSON-строку для своего запроса. Для создания JSON-строки можно использовать любую стороннюю библиотеку. Фрагмент ниже показывает, как должна выглядеть ожидаемая JSON-строка:

                string json =
                   "{\"cmd\":\"login\","
                   "\"account\":" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + ","
                   "\"ts\":" + IntegerToString((int)TimeCurrent()) + ","
                   "}"; 
                uchar plain[];
                StringToCharArray(json, plain, 0, StringLen(json));

                Шаг 4 — EA шифрует сеансовый AES-ключ с помощью RSA:

                AES-ключ, используемый для остальной части сессии:

                uchar aesKey[];
                GenerateAESKey(aesKey);   // 16 random bytes (AES-128)
                uchar encryptedAes[];
                rsa.EncryptPKCS1v15(aesKey, encryptedAes);
                string encryptedAesBase64 = rsa.Base64Encode(encryptedAes);
                Шаг 5 — EA шифрует фактические данные с помощью AES:
                uchar encryptedPayload[];
                CryptEncode(CRYPT_AES128, plain, aesKey, encryptedPayload);
                string payloadBase64 = CryptBase64Encode(encryptedPayload);
                

                Шаг 6 — EA отправляет всё на сервер:

                string body =
                   "{\"key\":\"" + encryptedAesBase64 + "\","
                   + "\"data\":\"" + payloadBase64 + "\"}";
                
                string result;
                char headers[];
                int status = WebRequest("POST", url, headers, 5000, body, result);

                Шаг 7 — расшифрование на стороне сервера (концептуально):

                На сервере сначала выполните base64-декодирование RSA-зашифрованного AES-ключа, затем расшифруйте AES-ключ с помощью закрытого RSA-ключа. После восстановления AES-ключа его можно использовать для расшифрования полезной нагрузки данных. Поскольку AES-ключ случайно генерируется для каждой сессии, каждое сообщение остаётся уникальным и защищённым, даже если злоумышленник перехватит трафик. Этот механизм фактически устанавливает защищённую сессию, аналогичную упрощённому HTTPS-туннелю.

                Шаг 8 — клиентская сторона расшифровывает ответ сервера:

                uchar responseCipher[];
                CryptBase64Decode(result, responseCipher);
                uchar responsePlain[];
                CryptDecode(CRYPT_AES128, responseCipher, aesKey, responsePlain);
                
                string serverReply = CharArrayToString(responsePlain);
                Print("Server replied: ", serverReply);
                
                Следуя этим шагам, реальную криптографию можно реализовать напрямую в среде MetaTrader 5, получив надёжное и полностью переносимое решение для конечных пользователей.


                Заключение

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

                На этой основе разработчики теперь получают практический набор инструментов для создания защищённых систем обмена сообщениями, защиты конфигурационных файлов, поддержки безопасного обмена ключами и зашифрованной коммуникации непосредственно внутри MetaTrader 5. Это позволяет Expert Advisors и индикаторам безопасно взаимодействовать с удалёнными сервисами, аутентифицировать источники данных и обмениваться зашифрованными торговыми командами, не раскрывая чувствительную информацию в сети.

                Реализация также подчёркивает важную мысль: криптография не обязана опираться на внешние DLL или сторонние библиотеки, чтобы быть эффективной. Когда алгоритмы реализованы нативно в MQL5, код остаётся прозрачным, переносимым и полностью контролируемым разработчиком. Это обеспечивает как проверяемость, так и соответствие политикам безопасности MetaQuotes, что особенно важно при разработке коммерческих торговых инструментов, распространяемых через Market.

                Для разработчиков, заинтересованных в развитии этой работы, возможны несколько направлений. Библиотеку RSA можно объединить с цифровыми подписями для проверки подлинности и целостности сообщений или расширить современными схемами заполнения, такими как OAEP, для повышения устойчивости к криптоанализу. Аналогично, интеграция AES в аутентифицированных режимах, например GCM или CBC-HMAC, может обеспечить и шифрование, и обнаружение подмены за один шаг. Такие улучшения приблизят реализацию к современным стандартам вроде TLS и PGP — полностью внутри среды MQL5.

                С практической точки зрения эта статья призывает разработчиков воспринимать MQL5 не просто как язык торговых скриптов, а как полноценную программную среду, где сосуществуют безопасность, сетевое взаимодействие и вычисления. Сочетая математическую точность с аккуратным проектированием, разработчики MQL5 могут создавать защищённые самодостаточные системы, способные уверенно взаимодействовать с внешними API, брокерами или облачными сервисами.

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

                Авторы и программисты


                Ссылки и дополнительное чтение

                Для читателей, интересующихся математическими основами и современными применениями RSA- и AES-шифрования, следующие источники дают надёжные и подробные объяснения:

                • Wikipedia – PKCS #1: стандарт криптографии RSA
                  Объясняет официальный стандарт RSA, включая схемы заполнения, такие как PKCS#1 v1.5 и OAEP, используемые для шифрования и подписей.
                  Официальная документация MQL5 по CryptEncode(), CryptDecode() и другим встроенным функциям для AES и хеширования.
                • Paar, C. & Pelzl, J. (2024). Understanding Cryptography – учебник для студентов и практиков, глава 7: криптосистема RSA. Springer. ссылка
                • Mollin, R. A. (2023). RSA и криптография с открытым ключом. Routledge. ссылка
                • National Institute of Standards and Technology. FIPS 197: Advanced Encryption Standard (AES). 2001 (обновлено). ссылка
                • Tuo, Z. (2023). «Сравнительный анализ алгоритмов AES и RSA и их интегрированного применения». Theoretical and Natural Science, Vol 25, pp. 28-35. ссылка
                • «Гибридное шифрование RSA-AES: объединение преимуществ симметричных и асимметричных алгоритмов». IJRAR, 2023. ссылка
                • Ganesh, R., Khan, A. R. и др. (2025). «Панорамный обзор Advanced Encryption Standard (AES)». International Journal of Information Security. ссылка
                • Упрощённое объяснение того, как работает шифрование/расшифрование сообщений RSA


                Перевод с английского произведен MetaQuotes Ltd.
                Оригинальная статья: https://www.mql5.com/en/articles/20273

                Прикрепленные файлы |
                RSA.mqh (24.79 KB)
                Торговые инструменты MQL5 (Часть 23): Трёхмерные графики с управляемой камерой и поддержкой DirectX для анализа распределений Торговые инструменты MQL5 (Часть 23): Трёхмерные графики с управляемой камерой и поддержкой DirectX для анализа распределений
                В этой статье мы усовершенствовали инструмент построения графиков биномиального распределения в MQL5, интегрировав DirectX для 3D-визуализации, что позволило переключаться между 2D и 3D режимами с управляемым камерой поворотом, масштабированием и автоматическим подбором положения камеры для иммерсивного анализа. Мы визуализируем столбцы гистограммы в 3D, опорные плоскости и оси наряду с кривой функции массы вероятности, сохраняя при этом 2D-элементы, такие как панели статистики, легенда и настраиваемые темы, градиенты и метки.
                Осваиваем графики Kagi в MQL5 (Часть I): Создание движка графика Kagi Осваиваем графики Kagi в MQL5 (Часть I): Создание движка графика Kagi
                Узнайте, как создать полноценный движок графиков Kagi в MQL5: строить ценовые развороты, формировать динамические отрезки линий и обновлять структуру Kagi в реальном времени. В первой части показано, как отображать графики Kagi непосредственно в MetaTrader 5, давая трейдерам ясное представление о смене тренда и силе рынка и одновременно закладывая основу для автоматизированной торговой логики на базе Kagi во второй части.
                Моделирование рынка: Первые шаги на SQL в MQL5 (V) Моделирование рынка: Первые шаги на SQL в MQL5 (V)
                В предыдущей статье я показал, как следовало действовать для добавления механизма запросов. Это было нужно для того, чтобы внутри кода MQL5 вы могли полноценно использовать SQL и получать результаты при выполнении команды SQL SELECT FROM. Но осталось рассказать последнюю функцию, которую нам необходимо реализовать. Это функция DatabaseReadBind. И, поскольку для правильного понимания требуется чуть более развернутое объяснение, было решено сделать это не в той предыдущей статье, а в сегодняшней. Итак, поскольку тема будет довольно объемной, перейдём сразу к следующему разделу.
                От начального до среднего уровня: События в объектах (II) От начального до среднего уровня: События в объектах (II)
                В данной статье мы рассмотрим принцип работы трех последних типов событий, которые может генерировать объект. Разбираться в этом будет очень увлекательно, так как в итоге мы сделаем то, что многим может показаться безумием, но это вполне возможно и дает весьма удивительный результат.