English Deutsch
preview
MQL5における純粋なRSA暗号化の実装

MQL5における純粋なRSA暗号化の実装

MetaTrader 5統合 |
25 0
Vahid Irak
Vahid Irak

よくある状況を想像してみてください。エキスパートアドバイザー(EA)が、シグナル、キー、ログイン情報、またはその他の重要なデータをサーバーに送信する必要がある場合です。HTTPやHTTPSを使えば接続は概ね安全だと考えるかもしれません。しかし、いずれ気づくのは、実際の脆弱性は接続自体にはなく、コード内に存在するということです。

MQL5では、機密な値をEAに直接埋め込むしかない場面があります。アルゴリズムのパラメータ、秘密鍵、パスワードなど、すべてコンパイル済みファイルに含まれる可能性があります。コードがコンパイルされて難読化されていても、十分な技術を持つ人であればそれらの値を抽出することは可能です。これにより重大なセキュリティ問題が生じます。輸送経路は暗号化されていても、EA自体が内部に秘密情報を保持せざるを得ないため、潜在的な情報漏洩の原因となる可能性があるのです。同時に、市場向け製品ではDLLを用いた暗号化が禁止されており、MQL5には標準でRSAが提供されていません。軽量な「暗号化」トリックであるXORなども、実際の保護にはほとんど役に立ちません。多くの開発者はここで行き詰まります。すなわち、通信を安全にし、鍵を保護する手段が必要である一方で、利用可能なツールは制限されているか、そもそも強度が不十分なのです。

本記事では、これらの制限を解決する方法を紹介します。EAが重要なデータを内部に保持せず、信頼できるサーバーから、暗号化された形でデータを安全に取得する方法を説明します。また、RSAを用いたキー交換とAESによる高速通信というハイブリッド暗号を、DLLや外部ライブラリに頼らずMQL5だけで実装する方法も学べます。


RSA入門

「RSA」という名前は、1977年にMITで働いていたロナルド・リベスト、アディ・シャミア、そしてレナード・エイドルマンの頭文字に由来します。彼らの研究は、公開鍵暗号システムを実際に実装した初の試みであり、理論上のアイデアを実用的な技術に変えました。公開鍵暗号の概念自体は1976年にディフィーとヘルマンによって提案されていましたが、RSAは安全な暗号化と電子署名を実現する具体的な仕組みを提供しました。

RSAの核心は、大きな合成数の因数分解が計算上極めて困難であるという数学的根拠に基づいています。この性質により、RSAは現代暗号技術の基盤として利用され続けています。長年にわたり、Webトラフィックの保護、電子文書の認証、鍵交換、機密データ保護など、さまざまなプラットフォームやプロトコルで使われてきました。

現在でもRSAは最も広く採用されている非対称暗号アルゴリズムの一つであり、SSL/TLS、PGP、SSHなど多くの安全な通信システムの標準規格に組み込まれています。楕円曲線暗号(ECC)やその他の新しい手法が登場しても、RSAはそのシンプルさ、堅牢性、そして長年の信頼性から評価されています。



数学的な仕組み 

RSAは非常にシンプルで優雅なアルゴリズムです。

基本的な考え方は、メッセージを公開鍵で暗号化し、対応する秘密鍵だけが復号して元のメッセージを取り出せるというものです。RSAの核心は、以下に示す式に基づいています。誰がこの理論を発見したのか気になる場合、その基礎となる理論は、数学者レオンハルト・オイラーに由来します。オイラーの定理(フェルマー–オイラーの定理とも呼ばれる)はRSAの基盤となります。詳細については、こちらの記事をご覧ください。

主な式

これらの記号の意味は次の通りです。

  • m:メッセージ(数値として表現)
  • n:公開鍵を構成する値
  • d:秘密鍵
  • n:法(モジュラス)

では、主要な式をもう少し詳しく見てみましょう。数学の知識があまりなくても、メッセージmに特定の数学的操作をおこなうことで、結果として元のメッセージが復元できることが分かります。 この性質を利用して、式をモジュロ演算の分配法則に基づき2つの非対称な部分に分けることができます。具体的には、メッセージme乗し、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!」の場合、1つの大きな数値として書き出すことができます。10進数の場合は22405534230753963835153736737、 16進数の場合は0x48656c6c6f20776f726c6421です。ただし、モジュラスは15のような小さな数値では不十分です。現実のメッセージに対しては、モジュラスは必ずメッセージの数値表現より大きくなければなりません。さもないと、暗号化の計算が正しく機能しません。RSAの暗号化は計算量が多い処理です。大きな数値を大きな指数(例:65537)で暗号化する場合、多くの乗算が必要となります。最適化をおこなわなければ、CPUに大きな負荷がかかることがあります。

セキュリティに関して言えば、単純な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はCryptoAPIを通じてRSA機能を提供していますが、外部DLLを呼び出すと、特にEAの場合、複雑さが増し、潜在的なセキュリティリスクが高まります。 クライアントがあなたのEAを使用する際に、DLLアクセスが必要であることが分かると、躊躇したり、製品への信頼を失ったりする可能性があります。


アルゴリズム設計(ステップごと)

暗号化プロセスはいくつかの段階に分かれており、理解しやすくするために、コードの説明を3つの部分に分けています。 第1部ではコアコンセプトとクラスの初期化方法を解説します。 第2部では大きな整数の算術関数を紹介します。第3部でRSA暗号化プロセスの最終的な実装を示します。

      第1部

      RSAクラスの作成

          メインクラスであるMQL5_RSAは暗号化の全ワークフローをカプセル化しており、Base64やHexToByteなどのデータ形式変換機能を含みます。また、大きな数の算術、メッセージにランダム性を導入するPKCS#1 v1.5パディング、オプションのデバッグモードによる操作の追跡機能も実装されています。
            まず、モジュラス、指数、および補助メソッドを格納するRSAクラスを定義します。また、PKCS#1 v1.5パディングでは0でないランダムバイトが必要なため、ランダム生成器は一度だけシードを設定します。
            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
               }
            };
            初期化:

              モジュラス(公開鍵)を16進数文字列として読み込み、バイトに変換し、正規化して、公開指数(通常は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コードにはバグがあり、16進ペアではなく文字コードを追加していたからです。ここでは正しい実装を示します。

              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の数値はビッグエンディアン配列で管理されます。つまり、インデックス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クラスは基本的なコア機能を提供できる状態です。 デバッグ状態を保持でき、モジュラスを受け取り正規化でき、16進値を正しくバイトに変換でき、先頭のゼロを削除でき、指数値を保持できます。これらの基盤が整った後に、大きな整数の算術(引き算、比較、乗算)、続いて剰余算術やPKCS#1パディングを実装することが可能になります。この機能が整えば、RSA暗号化プロセスを安全に実行できます。

              第2部

              大きな整数エンジンの構築: RSAの基本

              次に、RSAに必要な大きな整数の算術システムを構築します。MQL5にはネイティブな大きな整数型が存在しないため、RSAではバイト配列を使って大きな整数をシミュレートする必要があります。このセクションでは、PKCS#1パディングや剰余指数演算をサポートする数学的な基盤を作ります。以下の関数を実装します。

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

              これらの関数はまとめて、大きな整数の算術演算をエミュレートします。

              1.Compare():大きい整数の比較

              引き算や割り算、剰余演算をおこなう前に、2つの数値を比較できる必要があります。
              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():ビッグエンディアンの引き算

              この関数は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);
              }
              

              この乗算は、純粋なMQL5によるRSA実装の中で最も処理が遅い部分ですが、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():乗算後の剰余演算

              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は0でないランダムなバイト列を表します。また、最終的なメッセージ長にパディング用のオーバーヘッドバイトが含まれるため、メッセージの長さは必ず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)は、0でないランダムなバイトで構成する必要があります。この実装では、コンストラクタでシードを設定したMathRand()を使用しています。これはシードなしの乱数生成器よりは改善されていますが、暗号学的に安全な乱数生成器ではありません。本番環境では、暗号学的に安全なRNGを使用するべきです。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[]として準備します。この配列の各要素は1バイトに対応します。最後に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で暗号化します。AESは高速で、大きなメッセージやファイルに適した対称鍵暗号です。
                4. 両方の要素を一緒に送信します。
                  • AESで暗号化されたデータ(高速でコンパクト)
                  • RSAで暗号化されたAESキー(小さいが安全)
                この方式により、両方のアルゴリズムの利点を組み合わせることができます。RSAは事前共有秘密なしで安全な鍵交換を可能にし、AESは実際のデータを高速に暗号化します。このモデルではRSAが鍵を保護し、AESがデータを保護します。これらを組み合わせることで、SSL/TLS、HTTPS、VPNトンネルなど、現代の安全な通信プロトコルの基盤が形成されます。

                MQL5の文脈では、この戦略により、標準のHTTPやソケット接続を使用する場合でも、EA、インジケーター、外部サーバー間の通信を安全に保護することができます。この記事で実装したRSAクラスはAESキーの暗号化に使用でき、組み込みのMQL5関数CryptEncode()とCryptDecode()が、実際のメッセージのAES暗号化および復号を担当します。

                これにより、MetaTrader 5内で完全に自己完結したセキュリティ層を構築でき、いわば「HTTP 上の軽量 HTTPS」を実現することができます。外部の暗号ライブラリやDLLに依存せずに、取引コマンド、認証情報、設定メッセージなどの機密データを保護することが可能です。

                ステップ1:サーバー側でRSA公開鍵を生成

                Python、Java、OpenSSL、または任意のバックエンド環境を使用して、モジュラス(16進文字列)、公開指数(通常は65537)、秘密鍵(サーバー側で保持)を生成します。

                EAは公開値のみを使用します。秘密鍵をクライアント側に公開しないでください

                modulusHex = "A1B2C3…";
                exponent   = 65537;

                ステップ2:EAがRSAインスタンスを作成し鍵を読み込む

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

                ステップ3:EAがリクエストメッセージを準備する

                例として、ログインリクエストにはEA ID、アカウント番号、タイムスタンプを含めることができます。 リクエスト用の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:サーバー側での復号化(概念)

                サーバーではまず、RSAで暗号化されたAESキーをBase64デコードし、秘密鍵を使って復号します。 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);
                
                クライアントは、サーバーから返された応答をBase64デコードし、AESキーで復号します。その結果を文字列として取得することで、サーバーの応答内容を読み取ることができます。


                結論

                本記事では、MQL5だけで完全に実装された、機能的なRSA暗号化ライブラリを紹介しました。これにより、複雑な数学的アルゴリズムであっても、外部依存なしでMetaTrader 5環境内に直接実装できることを示しました。この取り組みを通じて、MQL5は単なる取引自動化用スクリプト言語にとどまらず、剰余算術や公開鍵暗号のような高度な計算処理もおこなえることが明らかになりました。

                この基盤により、開発者はMetaTrader 5内で安全なメッセージ処理システムを構築したり、設定ファイルを保護したり、鍵交換や暗号化通信を直接サポートしたりする実用的なツールキットを手に入れることができます。これにより、EAやインジケーターはリモートサービスと安全に通信し、データソースを認証し、暗号化された取引コマンドをネットワーク上に漏らさずに交換できるようになります。

                さらに、この実装は重要な概念を強調しています。すなわち、暗号化は外部DLLやサードパーティライブラリに依存せずとも十分に機能するということです。アルゴリズムをMQL5ネイティブで実装することで、コードは透明でポータブルになり、開発者が完全に制御可能です。これにより、監査可能性やMetaQuotesのセキュリティ方針の遵守が保証され、特にマーケットを通じて配布される商用取引ツールの開発において重要です。

                開発者がこの取り組みを拡張する場合、いくつかの方向性があります。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などのパディング方式も含まれています。
                  CryptEncode()、CryptDecode()、およびAESとハッシュ化のためのその他の組み込み関数に関するMQL5の公式ドキュメント
                • Paar, C. & Pelzl, J.(2024).Understanding Cryptography – A Textbook for Students and Practitioners, Chapter 7:The RSA Cryptosystem.Springer [リンク]
                • Mollin, R. A.(2023).RSA and Public-Key Cryptography.Routledge [リンク]
                • National Institute of Standards and Technology.FIPS 197:Advanced Encryption Standard (AES)。2001年(更新版)[リンク]
                • Tuo, Z.(2023).「A Comparative Analysis of AES and RSA Algorithms and Their Integrated Application.」Theoretical and Natural Science、第25巻、28-35ページ[リンク]
                • 「RSA-AES Hybrid Encryption:Combining the Strengths of Symmetric & Asymmetric Algorithms.」IJRAR、2023年リンク
                • Ganesh, R., Khan, A. R., et al.(2025).「A Panoramic Survey of the Advanced Encryption Standard (AES)」International Journal of Information Security.[リンク]
                • Simplified explanation of how RSA message encryption/decryption works


                MetaQuotes Ltdにより英語から翻訳されました。
                元の記事: https://www.mql5.com/en/articles/20273

                添付されたファイル |
                RSA.mqh (24.79 KB)
                プロップファームチャレンジをクリアするための自動リスク管理 プロップファームチャレンジをクリアするための自動リスク管理
                本記事では、GOLD向けのプロップファーム用エキスパートアドバイザー(EA)の設計について解説します。このEAは、ブレイクアウトフィルター、マルチタイムフレーム分析、堅牢なリスク管理、そして厳格なドローダウン制御を特徴としています。ルール違反を回避し、ボラティリティの高い市場環境下でも安定した取引実行を維持することで、トレーダーがプロップファームのチャレンジをクリアするのを支援します。
                MQL5標準ライブラリエクスプローラー(第5回):マルチシグナルEA MQL5標準ライブラリエクスプローラー(第5回):マルチシグナルEA
                本セッションでは、MQL5標準ライブラリを使用して、複数のシグナルを組み合わせた高度なエキスパートアドバイザー(EA)を構築します。このアプローチにより、組み込みシグナルと独自ロジックをシームレスに統合し、柔軟かつ強力な取引アルゴリズムの構築方法を示します。詳細については、続きをご覧ください。
                エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
                この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
                MQL5での取引戦略の自動化(第44回):スイングハイ/ローのブレイクによる性格の変化(CHoCH)検出 MQL5での取引戦略の自動化(第44回):スイングハイ/ローのブレイクによる性格の変化(CHoCH)検出
                この記事では、MQL5で性格の変化(CHoCH)検出システムを開発します。本システムは、ユーザーが設定したバーの長さに基づいてスイングハイとスイングローを特定し、高値には「HH/LH」、安値には「LL/HL」とラベル付けをおこない、トレンド方向を判定します。そして、これらのスイングポイントをブレイクした際にエントリーをおこない、潜在的な反転を示すサインとして活用します。構造が変化した際のブレイクもエントリー対象とします。