English Русский 中文 Español Deutsch Português 한국어 Français Italiano Türkçe
MQL5 コードの保護:パスワード保護、キージェネレータ、時間制限、リモートライセンス、高度 EA ライセンスキー暗号化技術

MQL5 コードの保護:パスワード保護、キージェネレータ、時間制限、リモートライセンス、高度 EA ライセンスキー暗号化技術

MetaTrader 5 | 28 10月 2015, 17:18
1 808 0
investeo
investeo

はじめに

開発者のほとんどは自分の書いたコードの保護を必要としています。本稿は MQL5 ソフトウェアを保護するいくつかの異なる方法を紹介します。本稿の例はすべて Expert Advisors を対称としますが、スクリプトおよびインディケータにも同じルールが適用可能です。本稿はシンプルなパスワード保護から始め、キージェネレータ、ブローカーアカウントに与えられるライセンス、時間制限保護へと続きます。そしてリモートライセンスサーバーの概念を紹介します。私のMQL5-RPC フレームワークに関する前稿では MetaTrader 5からあらゆる XML-RPC サーバーへの「リモートプロシージャ呼び出し」について述べています。

このソリュ―ションをリモートライセンスの例を提供するために利用します。また base64エンコードを使用してこのソリュ―ションを強化する方法を述べ、MQL5 Expert Advisorsとインディケータに対する超安全な保護をするための PGP サポートへのアドバイスを行います。MetaQuotes Software Corp.が isMQL5.comの マーケットセクションから直接コードディレクトリのライセンスについてのオプションを提供していることは知っています。これはほんとうにすべての開発者にとって良いことで、それにより本稿で紹介する考えは説得力を持つでしょう。共に使用されているソリュ―ションは保護を強化しソフトウェアの盗用に対してさらに安全なものにすることができます。


. パスワード保護

簡単なものから始めます。コンピュータソフトウェアの保護にもっとも利用されているソリュ―ションはパスワードあるいはライセンスキープロテクションです。インストールした後の最初の起動時、ユーザーはソフトウェアのコピーにつながるパスワードを入力するためのダイアログボックスで問い合わせを受けます(Microsoft WindowsやMicrosoft Officeのシリアルキーのように)。そしてパスワードが一致するとユーザーはソフトウェアの単一登録コピーの使用が許可されます。それで入力変数やコード入力の直接のテキストボックスを使用することができます。スタブコードのサンプルは以下に示しています。

以下のコードはパスワード挿入に使用されることになるCChartObjectEdit フィールドの初期化を行います。ユーザーが入力したパスワードに一致した許可されたパスワードの定義済み配列があります。パスワードは CHARTEVENT_OBJECT_ENDEDIT イベントの受け取り後、 OnChartEvent() メソッドでチェックされます。

//+------------------------------------------------------------------+
//|                                          PasswordProtectedEA.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"

#include <ChartObjects/ChartObjectsTxtControls.mqh>

CChartObjectEdit password_edit;

const string allowed_passwords[] = { "863H-6738725-JG76364",
                             "145G-8927523-JG76364",
                             "263H-7663233-JG76364" };
                             
int    password_status = -1;
string password_message[] = { "WRONG PASSWORD. Trading not allowed.",
                         "EA PASSWORD verified." };

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   password_edit.Create(0, "password_edit", 0, 10, 10, 260, 25);
   password_edit.BackColor(White);
   password_edit.BorderColor(Black);
   password_edit.SetInteger(OBJPROP_SELECTED, 0, true);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   password_edit.Delete();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  if (password_status>0) 
  {
    // password correct
  } 
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "password_edit" )
      {
         password_status = -1;
         
         for (int i=0; i<ArraySize(allowed_passwords); i++)
            if (password_edit.GetString(OBJPROP_TEXT) == allowed_passwords[i]) 
            {
               password_status = i;
               break;
            }
            
         if (password_status == -1) 
            password_edit.SetString(OBJPROP_TEXT, 0, password_message[0]);
         else 
            password_edit.SetString(OBJPROP_TEXT, 0, password_message[1]); 
      }
  }
//+------------------------------------------------------------------+

このメソッドはシンプルですが、ハッキングされたシリアルナンバーを使ってウェブサイト上でパスワードを発行する人にとっては 脆弱です。EA プログラマーは新しい Expert Advisor が発売され盗まれたパスワードがブラックリストに挙がるまで何もできません。


2. キージェネレータ

キージェネレータは定義済みルールに基づくパスワードセットの使用を許可するメカニズムです。以下にキージェネレータのスタブによって概要を提示します。下に示される例ではキーは2つのハイフンで区切られた3つの数値で構成されます。パスワード用フォーマットは XXXXX-XXXXX-XXXXXということです。

最初の数字は3の倍数、二番目の数字は4の倍数、そして三番目の数字は5の倍数とします。よって許可されるパスワードは 3-4-5、 18000-20000-20000 あるいはより複雑な 3708-102792-2844770でなどとなります。

//+------------------------------------------------------------------+
//|                                      KeyGeneratorProtectedEA.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"

#include <ChartObjects/ChartObjectsTxtControls.mqh>
#include <Strings/String.mqh>

CChartObjectEdit password_edit;
CString user_pass;

const double divisor_sequence[] = { 3.0, 4.0, 5.0 };
                             
int    password_status = -1;
string password_message[] = { "WRONG PASSWORD. Trading not allowed.",
                         "EA PASSWORD verified." };

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   password_edit.Create(0, "password_edit", 0, 10, 10, 260, 25);
   password_edit.BackColor(White);
   password_edit.BorderColor(Black);
   password_edit.SetInteger(OBJPROP_SELECTED, 0, true);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   password_edit.Delete();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  if (password_status==3) 
  {
    // password correct
  } 
  }
  
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "password_edit" )
      {
         password_status = 0;
         
         user_pass.Assign(password_edit.GetString(OBJPROP_TEXT));

         int hyphen_1 = user_pass.Find(0, "-");
         int hyphen_2 = user_pass.FindRev("-");
         
         if (hyphen_1 == -1 || hyphen_2 == -1 || hyphen_1 == hyphen_2) {
            password_edit.SetString(OBJPROP_TEXT, 0, password_message[0]);
            return;
         } ;     
         
         long pass_1 = StringToInteger(user_pass.Mid(0, hyphen_1));
         long pass_2 = StringToInteger(user_pass.Mid(hyphen_1 + 1, hyphen_2));
         long pass_3 = StringToInteger(user_pass.Mid(hyphen_2 + 1, StringLen(user_pass.Str())));
         
         // PrintFormat("%d : %d : %d", pass_1, pass_2, pass_3);
         
         if (MathIsValidNumber(pass_1) && MathMod((double)pass_1, divisor_sequence[0]) == 0.0) password_status++;
         if (MathIsValidNumber(pass_2) && MathMod((double)pass_2, divisor_sequence[1]) == 0.0) password_status++;
         if (MathIsValidNumber(pass_3) && MathMod((double)pass_3, divisor_sequence[2]) == 0.0) password_status++;
            
         if (password_status != 3) 
            password_edit.SetString(OBJPROP_TEXT, 0, password_message[0]);
         else
            password_edit.SetString(OBJPROP_TEXT, 0, password_message[1]); 
      }
  }
//+------------------------------------------------------------------+

もちろん数字の桁数は与えられた値に設定することができ、計算をより複雑化することが可能です。計算に HDD シリアルナンバーや CPU ID を追加することで既定のハードウェアにのみ有効な変数を加えることも可能です。その場合 EA を起動する人はハードウェアベースで計算される別のジェネレータも起動する必要があります。

アウトプットは キージェネレータ のインプットとなり、作成されたパスワードは既定のハードウェアにのみ有効となります。これはハードウェアを変更する人や、 EA の起動にVPSを使用する人にとっては制約となりますが、それは2~3個の有効なパスワードを与えることで解決されます。これは MQL5 ウェブサイトのマーケットでも取り上げています。


3. シングルアカウントライセンス

任意のブローカーの端末アカウントナンバーはユニークなのでこれをひとつのまたはセットとしてのアカウントナンバーにおけるEA使用許可に利用することができます。その場合、メソッド AccountInfoString(ACCOUNT_COMPANY) および AccountInfoInteger(ACCOUNT_LOGIN) を使用しアカウントデータを取得し、それをコンパイル済みの許可値と比較するだけでよいのです。

//+------------------------------------------------------------------+
//|                                           AccountProtectedEA.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"


const string allowed_broker = "MetaQuotes Software Corp.";
const long allowed_accounts[] = { 979890, 436290, 646490, 225690, 279260 };
                             
int password_status = -1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   string broker = AccountInfoString(ACCOUNT_COMPANY);
   long account = AccountInfoInteger(ACCOUNT_LOGIN);
   
   printf("The name of the broker = %s", broker);
   printf("Account number =  %d", account);
   
   if (broker == allowed_broker) 
      for (int i=0; i<ArraySize(allowed_accounts); i++)
       if (account == allowed_accounts[i]) { 
         password_status = 1;
         Print("EA account verified");
         break;
       }
   if (password_status == -1) Print("EA is not allowed to run on this account."); 
    
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---  
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  if (password_status == 1) 
  {
    // password correct
  } 
  }

これはシンプルですがひじょうに力強い保護法です。ただ欠点は新しいアカウントナンバーがアカウントデータベースに追加されるたびに EA をコンパイルしなおす必要があることです。


4. 時間制限保護

時間制限保護は一時的にライセンスが与えられる場合有用です。たとえばソフトウェアのトライアルバージョンを使用するとか、ライセンスが月次、年次で与えられるときなどです。これは明らかなので Expert Advisors やインディケータに適用することができます。

最初の考えはサーバー時間を確認することです。そしてそれを基に既定の期間内でユーザーにインディケータや Expert Advisorを使ってもらうのです。期限がきれたらライセンス所有者は部分的にまたは全体にライセンス使用者に対してライセンスを無効にすることができます。

//+------------------------------------------------------------------+
//|                                         TimeLimitProtectedEA.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"
                           
datetime allowed_until = D'2012.02.11 00:00'; 
                             
int password_status = -1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   printf("This EA is valid until %s", TimeToString(allowed_until, TIME_DATE|TIME_MINUTES));
   datetime now = TimeCurrent();
   
   if (now < allowed_until) 
         Print("EA time limit verified, EA init time : " + TimeToString(now, TIME_DATE|TIME_MINUTES));
   
    
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  if (TimeCurrent() < allowed_until) 
    {        
    }
   else Print("EA expired."); 
  }

唯一の欠点はソリューションが各ライセンスに対して個別にコンパイルを必要とすることです。


5. リモートライセンス

ユーザーごとにライセンスをどこで無効にするか、なたトライアル期間を延長するかの管理がトータルでできたらすばらしいと思いませんか?アカウント名で問合せを送信し、トライアルモードでスクリプトを実行するかそれを無効にするかということについての値を受け取る MQL5-RPC 呼び出しを利用するだけでそれが可能なのです。

実装サンプルとして以下のコードを参照ください。

from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler

class RequestHandler( SimpleXMLRPCRequestHandler ):
    rpc_path = ( '/RPC2', )
    

class RemoteLicenseExample(SimpleXMLRPCServer):

    def __init__(self):
        SimpleXMLRPCServer.__init__( self, ("192.168.2.103", 9099), requestHandler=RequestHandler, logRequests = False)
        
        self.register_introspection_functions()
        self.register_function( self.xmlrpc_isValid, "isValid" )        
        
        self.licenses = {} 
        
    def addLicense(self, ea_name, broker_name, account_number):
        if ea_name in self.licenses:
            self.licenses[ea_name].append({ 'broker_name': broker_name, 'account_number' : account_number })
        else:
            self.licenses[ea_name] = [ { 'broker_name': broker_name, 'account_number' : account_number } ]
             
    def listLicenses(self):
        print self.licenses
        
    def xmlrpc_isValid(self, ea_name, broker_name, account_number):
        isValidLicense = False
        
        ea_name = str(ea_name)
        broker_name = str(broker_name)
        
        print "Request for license", ea_name, broker_name, account_number
        
        try:
            account_number = int(account_number)
        except ValueError as error:
            return isValidLicense
    
        if ea_name in self.licenses:
            for license in self.licenses[ea_name]:
                if license['broker_name'] == broker_name and license['account_number'] == account_number:
                    isValidLicense = True
                    break
                
        print "License valid:", isValidLicense
        
        return isValidLicense
    
if __name__ == '__main__':
    server = RemoteLicenseExample()
    server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024221)
    server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024223)
    
    server.listLicenses()
    server.serve_forever()  

これは MetaTrader 5に対する2件の定義済みライセンスを持つPythonでのXML-RPCサーバー実装例です。ライセンスはアカウントナンバー1024221 および 1024223でデフォルトのMetaQuotesデモサーバー(access.metatrader5.com:443)上で実行する"RemoteProtectedEA"エキスパートアドバイザーに対する設定です。産業的ソリューションとしては Postgresql または任意のデータベースでライセンスデータベースを利用することですが、上の例はリモートライセンスをうまく処理しているので本稿では十分すぎるほどです。

Python のインストール方法の簡単な説明をお望みの場合は"MQL5-RPCRemote Procedure Calls from MQL5: Web Service Access and XML-RPC ATC Analyzer for Fun and Profit"をご一読ください。

リモートライセンスを利用している EA には、ライセンスが有効かどうかに応じてtrue または falseのブール値を返す isValid() メソッドに対するリモートMQL5-RPC呼び出しを手配する必要があるだけです。以下はアカウント保護に基づく EA 例を示しています。

//+------------------------------------------------------------------+
//|                                            RemoteProtectedEA.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"

#include <MQL5-RPC.mqh>
#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayInt.mqh>
#include <Arrays\ArrayString.mqh>
#include <Arrays\ArrayBool.mqh>

bool license_status=false;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
/* License proxy server */
   CXMLRPCServerProxy s("192.168.2.103:9099");
   if(s.isConnected()==true)
     {

      CXMLRPCResult *result;

/* Get account data */
      string broker= AccountInfoString(ACCOUNT_COMPANY);
      long account = AccountInfoInteger(ACCOUNT_LOGIN);

      printf("The name of the broker = %s",broker);
      printf("Account number =  %d",account);

/* Get remote license status */
      CArrayObj* params= new CArrayObj;
      CArrayString* ea = new CArrayString;
      CArrayString* br = new CArrayString;
      CArrayInt *ac=new CArrayInt;

      ea.Add("RemoteProtectedEA");
      br.Add(broker);
      ac.Add((int)account);

      params.Add(ea); params.Add(br); params.Add(ac);

      CXMLRPCQuery query("isValid",params);

      result=s.execute(query);

      CArrayObj *resultArray=result.getResults();
      if(resultArray!=NULL && resultArray.At(0).Type()==TYPE_BOOL)
        {
         CArrayBool *stats=resultArray.At(0);

         license_status=stats.At(0);
        }
      else license_status=false;

      if(license_status==true) printf("License valid.");
      else printf("License invalid.");

      delete params;
      delete result;
     }
   else Print("License server not connected.");
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(license_status==true)
     {
      // license valid
     }
  }
//+------------------------------------------------------------------+

両スクリプトを実行する場合はご自身のアカウントナンバーにリモートライセンスを追加します。リモートライセンスは試用期間終了後遠隔で無効化ができる時間制限ライセンス、パスワードライセンスに対しても使用可能です。たとえば、だれかに10日間EAの試用を提供するとします。その人がソフトウェアが気に入らなければライセンスを無効にすることができ、また気に入った場合は既定の期間ライセンスを有効にすることができるのです。


6. 保護ライセンス暗号化

前の段落で紹介した考えは「リモードプロシージャ呼び出し」を利用してライセンスサーバーとクライアント端末間の情報交換をしました。これでは EAの登録コピーのスニファパッケージを使えばハッキングされる可能性があります。スニファアプリケーションを使ってハッカーは2台の端末間で送信されるすべてのTCP パケットを入手することができるのです。 ここではアカウントデータを送信し暗号家されたメッセージを受信するための base64 エンコードを用いてこの問題を克服します。

技能のある人なら PGP を使用してのちに保護するためにすべてのコードを DLL に入れることも可能です。ここで思いついたのは、メッセージは実のところ別の RPC メッセージ( ロシアの人形マトリョーシカのような)でそれはあとで MQL5 データに変換されるということです。

最初のステップは MQL5-RPCに対するサポートのデコードを追加することです。さいわいこれはすでにRenat著 MetaTrader 4 at https://www.mql5.com/ja/code/8098 で行われているので、私はそれを MQL5に変換するだけです。

//+------------------------------------------------------------------+
//|                                                       Base64.mq4 |
//|                      Copyright © 2006, MetaQuotes Software Corp. |
//|                                  MT5 version © 2012, Investeo.pl |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2006, MetaQuotes Software Corp."
#property link      "https://www.metaquotes.net"
 
static uchar ExtBase64Encode[64]={ 'A','B','C','D','E','F','G','H','I','J','K','L','M',
                                 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
                                 'a','b','c','d','e','f','g','h','i','j','k','l','m',
                                 'n','o','p','q','r','s','t','u','v','w','x','y','z',
                                 '0','1','2','3','4','5','6','7','8','9','+','/'      };
                                 
static uchar ExtBase64Decode[256]={
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  62,  -1,  -1,  -1,  63,
                    52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  -1,  -1,  -1,  -2,  -1,  -1,
                    -1,   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,
                    15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  -1,  -1,  -1,  -1,  -1,
                    -1,  26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,  39,  40,
                    41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,
                    -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1,  -1 };
                               

void Base64Encode(string in,string &out)
  {
   int i=0,pad=0,len=StringLen(in);

   while(i<len)
     {
      
      int b3,b2,b1=StringGetCharacter(in,i);
      i++;
      if(i>=len) { b2=0; b3=0; pad=2; }
      else
        {
         b2=StringGetCharacter(in,i);
         i++;
         if(i>=len) { b3=0; pad=1; }
         else       { b3=StringGetCharacter(in,i); i++; }
        }
      //----
      int c1=(b1 >> 2);
      int c2=(((b1 & 0x3) << 4) | (b2 >> 4));
      int c3=(((b2 & 0xf) << 2) | (b3 >> 6));
      int c4=(b3 & 0x3f);
 
      out=out+CharToString(ExtBase64Encode[c1]);
      out=out+CharToString(ExtBase64Encode[c2]);
      switch(pad)
        {
         case 0:
           out=out+CharToString(ExtBase64Encode[c3]);
           out=out+CharToString(ExtBase64Encode[c4]);
           break;
         case 1:
           out=out+CharToString(ExtBase64Encode[c3]);
           out=out+"=";
           break;
         case 2:
           out=out+"==";
           break;
        }
     }
//----
  }

void Base64Decode(string in,string &out)
  {
   int i=0,len=StringLen(in);
   int shift=0,accum=0;

   while(i<len)
     {
      int value=ExtBase64Decode[StringGetCharacter(in,i)];
      if(value<0 || value>63) break;
      
      accum<<=6;
      shift+=6;
      accum|=value;
      if(shift>=8)
        {
         shift-=8;
         value=accum >> shift;
         out=out+CharToString((uchar)(value & 0xFF));
        } 
      i++;
     }
//----
  }
//+------------------------------------------------------------------+

base64 エンコードの詳細は ウィキペディアの記事を参照ください。

MQL5 base64 のコーディングとデコードスクリプトのサンプルテストは以下です。

//+------------------------------------------------------------------+
//|                                                   Base64Test.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"

#include <Base64.mqh>

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   string to_encode = "<test>Abrakadabra</test>";
   
   string encoded;
   string decoded;
   
   Base64Encode(to_encode, encoded);
   
   Print(encoded);
   
   Base64Decode(encoded, decoded);
   
   Print(decoded);
   
  }
//+------------------------------------------------------------------+

このスクリプトから以下の結果を取得します。

DK      0       Base64Test (EURUSD,H1)  16:21:13        Original string: <test>Abrakadabra</test>
PO      0       Base64Test (EURUSD,H1)  16:21:13        Base64 encoded string: PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+
FM      0       Base64Test (EURUSD,H1)  16:21:13        Base64 decoded string: <test>Abrakadabra</test>

エンコードの有効性はコード4行で Python でのチェックを簡単に行うことができます。

import base64

encoded = 'PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+'
decoded = base64.b64decode(encoded)
print decoded

<test>Abrakadabra</test>

二番目のステップ XMLRPC結果を base64 (別名マトリョーシカ技術)に暗号化することです。

import base64
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler

class RequestHandler( SimpleXMLRPCRequestHandler ):
    rpc_path = ( '/RPC2', )
    

class RemoteLicenseExampleBase64(SimpleXMLRPCServer):

    def __init__(self):
        SimpleXMLRPCServer.__init__( self, ("192.168.2.103", 9099), requestHandler=RequestHandler, logRequests = False)
        
        self.register_introspection_functions()
        self.register_function( self.xmlrpc_isValid, "isValid" )        
        
        self.licenses = {} 
        
    def addLicense(self, ea_name, broker_name, account_number):
        if ea_name in self.licenses:
            self.licenses[ea_name].append({ 'broker_name': broker_name, 'account_number' : account_number })
        else:
            self.licenses[ea_name] = [ { 'broker_name': broker_name, 'account_number' : account_number } ]
             
    def listLicenses(self):
        print self.licenses
        
    def xmlrpc_isValid(self, ea_name, broker_name, account_number):
        isValidLicense = False
        
        ea_name = str(ea_name)
        broker_name = str(broker_name)
        
        print "Request for license", ea_name, broker_name, account_number
        
        try:
            account_number = int(account_number)
        except ValueError as error:
            return isValidLicense
    
        if ea_name in self.licenses:
            for license in self.licenses[ea_name]:
                if license['broker_name'] == broker_name and license['account_number'] == account_number:
                    isValidLicense = True
                    break
                
        print "License valid:", isValidLicense
        
        # additional xml encoded with base64
        xml_response = "<methodResponse><params><param><value><boolean>%d</boolean></value></param></params></methodResponse>"
        
        retval = xml_response % int(isValidLicense)
        
        return base64.b64encode(retval)
    
if __name__ == '__main__':
    server = RemoteLicenseExampleBase64()
    server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024221)
    server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024223)
    
    server.listLicenses()
    server.serve_forever()        

ライセンスが暗号化されたら MQL5-RPC メソッドを使用して復号されたメッセージを MQL5 データに変換し戻すことができます。

//+------------------------------------------------------------------+
//|                                      RemoteProtectedEABase64.mq5 |
//|                                      Copyright 2012, Investeo.pl |
//|                                           http://www.investeo.pl |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012, Investeo.pl"
#property link      "http://www.investeo.pl"
#property version   "1.00"

#include <MQL5-RPC.mqh>
#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayInt.mqh>
#include <Arrays\ArrayString.mqh>
#include <Arrays\ArrayBool.mqh>
#include <Base64.mqh>

bool license_status=false;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
/* License proxy server */
   CXMLRPCServerProxy s("192.168.2.103:9099");

   if(s.isConnected()==true)
     {
      CXMLRPCResult *result;

/* Get account data */
      string broker= AccountInfoString(ACCOUNT_COMPANY);
      long account = AccountInfoInteger(ACCOUNT_LOGIN);

      printf("The name of the broker = %s",broker);
      printf("Account number =  %d",account);

/* Get remote license status */
      CArrayObj* params= new CArrayObj;
      CArrayString* ea = new CArrayString;
      CArrayString* br = new CArrayString;
      CArrayInt *ac=new CArrayInt;

      ea.Add("RemoteProtectedEA");
      br.Add(broker);
      ac.Add((int)account);

      params.Add(ea); params.Add(br); params.Add(ac);

      CXMLRPCQuery query("isValid",params);

      result=s.execute(query);

      CArrayObj *resultArray=result.getResults();
      if(resultArray!=NULL && resultArray.At(0).Type()==TYPE_STRING)
        {
         CArrayString *stats=resultArray.At(0);

         string license_encoded=stats.At(0);

         printf("encoded license: %s",license_encoded);

         string license_decoded;

         Base64Decode(license_encoded,license_decoded);

         printf("decoded license: %s",license_decoded);

         CXMLRPCResult license(license_decoded);
         resultArray=license.getResults();

         CArrayBool *bstats=resultArray.At(0);

         license_status=bstats.At(0);
        }
      else license_status=false;

      if(license_status==true) printf("License valid.");
      else printf("License invalid.");

      delete params;
      delete result;
     }
   else Print("License server not connected.");

//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(license_status==true)
     {
      // license valid
     }
  }
//+------------------------------------------------------------------+

RemoteLicenseExampleBase64 サーバーは実行されているというスクリプトの実行結果は以下です。

KI  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  The name of the broker = MetaQuotes Software Corp.
GP  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  Account number =  1024223
EM  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  <methodResponse><params><param><value><string>PD94bWwgdmVyc2lvbj0nMS4wJz8+PG1ldGhvZFJlc3BvbnNlPjxwYXJhbXM+PHBhcmFtPjx2YWx1ZT48Ym9vbGVhbj4xPC9ib29sZWFuPjwvdmFsdWU+PC9wYXJhbT48L3BhcmFtcz48L21ldGhvZFJlc3BvbnNlPg==</string></value></param></params></methodResponse>
DG  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  encoded license: PD94bWwgdmVyc2lvbj0nMS4wJz8+PG1ldGhvZFJlc3BvbnNlPjxwYXJhbXM+PHBhcmFtPjx2YWx1ZT48Ym9vbGVhbj4xPC9ib29sZWFuPjwvdmFsdWU+PC9wYXJhbT48L3BhcmFtcz48L21ldGhvZFJlc3BvbnNlPg==
FL  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  decoded license: <methodResponse><params><param><value><boolean>1</boolean></value></param></params></methodResponse>
QL  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  License valid.

ご覧のようにXML-RPC ペイロードには実際にbase64によってエンコードされたXML-RPCメッセージのストリングが含まれています。この base64 エンコードメッセージは XML ストリングにデコードされ、後に MQL5 データにデコードされます。


7. 高度アンチ逆コンパイルガイドライン

MQL5 コードが逆コンパイルされるとすぐにスキルのあるリバースエンジニアの手によるもっとも安全な保護も脆弱となりクラックを許してしまいます。グーグルでいくらか調べたあと、MQL5 の逆コンパイラを見つけましたが、これはだれかのコードを盗みたいと思っている無知な人々からお金をまきあげるためのフェイクなのではないかと疑っています。とはいえ、私は試していないので、私の考えはまちがっているかもしれません。たとえそのような解決法があったとしても暗号化された EA/インディケータ入力パラメータを送信したりオブジェクトインデックスを渡すことで保護をより強くすることができます。

ハッカーにとって保護された EA に対する正確な入力パラメータを取得することや、同様に保護されたインディケータを役に立たなくする正しいインプット値を見つけるのは困難です。アカウントIDが一致していれば正しいパラメータを送信することや、アカウントIDが有効でない場合に暗号化されていないフェイクのパラメータを送信することも可能です。そのようなソリューションのためにPGP (Pretty Good privacy)を使いたいと思うかもしれません。それによってコードが逆コンパイルされているとしてもデータはプライベートな PGP キーで暗号化されて送信され、EA パラメータはアカウントIDとPGP キーが一致したときだけ復号されます。


おわりに

本稿では MQL5 コードを保護する法をいくつか紹介しました。またMQL5-RPC 呼び出しによるリモートライセンスコンセプトも取り上げ、base64 エンコードサポートについても加えてお話しました。本稿が MQL5 保護についてのより一層の発想の基となることを願っています。ソースコードはすべて本稿に添付があります。


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

トレーダミネーター 3:売買ロボットの台頭 トレーダミネーター 3:売買ロボットの台頭
記事 "Dr. Tradelove..." で Expert Advisorを作成しました。それは選択済みのトレーディングシステムのパラメータを自立的に最適化するものです。それ以上に EAにある一つのトレーディングシステムのパラメータだけを最適化するのではなく、複数あるトレーディングシステムから最良のものを選ぶExpert Advisorを作成しようと決めました。それがどうなったか見ていきます。
最後の改革 最後の改革
トレーディングターミナルを見てください。価格の提示はどのような意味に見えますか?バー、ろうそく足、罫線私たちは価格からしか利益を得ない一方、時間と価格の両方を追求しています。市場を分析する際に、価格のみに注意を向けるだけで良いのでしょうか?この記事は、(「3目並べ」)ポイント・フィギュアチャート作成のためのスクリプトとアルゴリズムを提唱します。記されている推奨にて、記載されている実用的な使用方法の様々な価格パターンを考察していきます。
Expert Advisor ビジュアルウィザードを用いたExpert Advisorsの作成 Expert Advisor ビジュアルウィザードを用いたExpert Advisorsの作成
MetaTrader 5 用Expert Advisor ビジュアルウィザードは、数分でExpert Advisを設計することができる理解しやすい定義済みトレーディンブロックセットを伴う高い直観的グラフィカル環境を提供します。Expert Advisor ビジュアルウィザードのクリック、ドラッグ、ドロップ方法により鉛筆と紙で行うように外為トレーディング戦略のビジュアル表現とシグナルを作成することができます。こういったトレーディングダイアグラムは、それらをすぐに使える Expert Advisorsに変換するモラニス社製 MQL5によって自動的に分析されます。連携したグラフィカル環境は設計プロセスを簡素化し、 MQL5 コードを書く必要はなくなります。
AutoElliottWaveMaker -  Elliott Wavesの半自動分析のためのMetaTrader 5ツール AutoElliottWaveMaker - Elliott Wavesの半自動分析のためのMetaTrader 5ツール
本稿は AutoElliottWaveMakerのレビューを行います。 - 手動と自動の組合せの波形ラベリングを表す MetaTrader 5 におけるElliott Wave分析に対する初めての開発です wave labeling. 波形分析ツールは包括的に MQL5 で書かれており、外部 dll ライブラリはインクルードしていません。これは MQL5で洗練されたおもしろいプログラムが開発できる(するべきである)というもうひとつの証明です。