English Русский 中文 Español Deutsch 日本語 Português Français Italiano Türkçe
MQL5 코드 보호하기: 보안 암호, 키 생성기, 시간 제한, 원격 라이선스 및 고급 EA 라이선스 키 암호 테크닉

MQL5 코드 보호하기: 보안 암호, 키 생성기, 시간 제한, 원격 라이선스 및 고급 EA 라이선스 키 암호 테크닉

MetaTrader 5 | 5 8월 2021, 09:44
267 0
investeo
investeo

개요

개발자라면 자신이 개발한 프로그램이 안전하게 보호되길 바라죠. MQL5 소프트웨어를 보호할 수 있는 몇 가지 방법을 소개하겠습니다. 본문은 엑스퍼트 어드바이저를 기준으로 작성되었습니다만 스크립트와 인디케이터에도 동일하게 적용됩니다. 간단한 암호 설정부터 시작해 키 생성기 이용 방법, 브로커 계정 라이선스 부여 방법 및 시간 제한 설정법 순서로 다루어 보도록 하겠습니다. 마지막으로는 원격 라이선스 서버에 대해 알아볼게요. MQL5-RPC 프레임워크를 주제로 한 이전 글에서 MetaTrader 5 에서 XML-RPC 서버로 원격 프로시저 호출을 하는 방법을 설명했습니다.

동일한 방법을 이용해서 원격 라이선스의 예를 들도록 할게요. 베이스64 인코딩을 이용한 RPC 호출 향상 방법 및 MQL5 엑스퍼트 어드바이저와 인디케이터의 초강력 보안을 위한 PGP 사용법에 대해서도 설명하겠습니다. MetaQuotes는 MQL5.com의 마켓을 통해 코드에 라이선스를 부여할 수 있는 옵션을 제공하고 있기도 합니다. 이 또한 아주 좋은 방법이죠. 제가 소개할 방법들과 비교해 어느 것이 더 좋다고 말할 수는 없습니다. 오히려 두 가지를 함께 이용하면 무단 복제에 보다 강력한 대응책을 만들 수 있겠죠.


1. 암호 보호

간단한 것부터 살펴보겠습니다. 가장 널리 쓰이는 소프트웨어 보안 솔루션은 보안 암호 또는 라이선스 키입니다. 프로그램 설치 시 소프트웨어의 비밀번호(예: MS 제품 키)를 입력하라는 대화상자가 나타나죠. 올바른 비밀번호를 입력해야만 해당 특정 사본을 이용할 수 있게 됩니다. 우리의 경우 입력 변수 또는 텍스트 상자를 이용하면 되는데요. 다음은 메소드 스텁의 예입니다.

아래의 코드는 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]); 
      }
  }
//+------------------------------------------------------------------+

간단한 방법이지만 누군가 시리얼 키를 해킹해 온라인에 올려 버리면 곤란해지죠. 새로운 엑스퍼트 어드바이저가 출시되고 해킹된 키가 차단되기 전까지 개발자가 할 수 있는 게 아무 것도 없습니다.


2. 키 생성기

키 생성기는 미리 설정된 규칙에 따라 암호를 생성합니다. 스텁을 이용해 설명할게요. 아래 예시의 경우 생성된 암호는 두 개의 하이픈으로 연결된 숫자여야 합니다. 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는 해당 하드웨어를 기반으로 연산되는 추가 키 생성기를 실행해야 합니다.

이렇게 생성된 암호는 특정 하드웨어에서만 사용할 수 있게 되죠. 여러 대의 컴퓨터를 사용하는 경우 혹은 VPS를 이용해 EA를 실행하는 경우 좀 귀찮겠지만 암호를 두세 개 정도만 더 입력하면 사용할 수 있습니다. 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. 시간 제한 설정

일정한 기간 동안만 라이선스가 유효한 경우 사용 시간을 제한할 수 있습니다. 예를 들어 소프트웨어 체험판이 여기에 해당하죠. 당연히 엑스퍼트 어드바이저와 인디케이터 모두에 적용할 수 있습니다.

기준 서버 시간을 설정한 후 사용자에게 일정 시간 동안 엑스퍼트 어드바이저를 이용할 수 있게 해주는 것입니다. 정해진 시간이 지나면 라이선스 허락자가 기능의 일부 또는 전부를 제한할 수 있습니다.

//+------------------------------------------------------------------+
//|                                         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 에 미리 설정된 두 라이선스를 대상으로 파이썬으로 구현한 간단한 XML-RPC 서버인데요. MetaQuotes 데모 서버(access.metatrader5.com:443)를 이용하는 계정 번호 1024221과 1024223에게 'RemoteProtectedEA' 엑스퍼트 어드바이저에 대한 라이선스가 부여되었습니다. 실제 산업용 솔루션은 아마 Postgresql과 같은 라이선스 데이터베이스를 이용해야 하겠지만 이 글에서는 위의 예제만으로도 충분합니다.

파이썬 설치 방법이 궁금하시면 'MQL5-RPC. MQL5에서 원격 프로시저 호출하기: 웹서비스 액세스 및 XML-RPC ATC 분석기로 재미있게 돈 벌기'를 읽어 보세요.

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
     }
  }
//+------------------------------------------------------------------+

두 가지 스크립트를 모두 실행하면 해당 계정 번호에 원격 라이선스를 부여할 수 있습니다. 원격 라이선스를 이용하면 시간 제한 및 암호를 이용한 EA도 체험 기간 종료 후 사용을 중지시킬 수 있습니다. 예를 들어 10일 동안 체험 가능한 EA의 경우, 사용자가 추가 사용을 원치 않는 경우 라이선스를 종료하거나 추가 사용을 원하는 경우 라이선스 기간을 연장해 줄 수 있습니다.


6. 라이선스 암호화

위에서 라이선스 서버와 클라이언트 터미널 간의 원격 프로시저 호출을 이용한 정보 교환 방법을 설명했습니다. 하지만 패킷 분석기를 이용하면 중간에서 해킹을 당할 수도 있는데요. 그렇게 되면 서버와 터미널이 교환한 모든 TCP 패킷이 공개됩니다. 베이스64 인코딩을 이용한 계정 정보 전송으로 이런 문제점을 극복하고 암호화된 메세지를 수신할 수 있는지 보겠습니다.

숙련된 개발자라면 PGP를 사용하거나 코드를 DLL에 저장할 수도 있겠네요. 두 방법을 함께 사용해도 좋고요. 사실 수신되는 메세지는 또 다른 RPC 메세지인 거 잖아요(러시아 마트료시카 인형처럼요)? MQL5 데이터로 변환되기만 하는 거죠.

우선 베이스64 인코딩을 추가하고 MQL5-RPC 디코딩을 지원합니다. 이미 Renat 씨가 MetaTrader4를 이용해 만든 파일이 https://www.mql5.com/ko/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++;
     }
//----
  }
//+------------------------------------------------------------------+

베이스64 인코딩에 대한 자세한 설명은 위키피디아를 참조하세요.

다음은 간단한 MQL5 베이스64 코딩 및 디코딩 스크립트입니다.

//+------------------------------------------------------------------+
//|                                                   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줄의 코드만 작성하면 파이썬으로 인코딩 타당성을 확인할 수 있습니다.

import base64

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

<test>Abrakadabra</test>

이제 XMLRPC 결과를 베이스64로 암호화해야 하는데요.

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 = "<?xml version='1.0'?><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  <?xml version='1.0'?><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: <?xml version='1.0'?><methodResponse><params><param><value><boolean>1</boolean></value></param></params></methodResponse>
QL  0  RemoteProtectedEABase64 (EURUSD,H1) 19:47:57  License valid.

보시다시피 XML-RPC 페이로드에 베이스64로 인코딩되었지만 사실은 또 다른 XML-RPC 메세지인 문자열이 포함되어 있죠. 해당 메세지는 XMl 문자열로 복호화된 후 다시 MQL5 데이터로 변환됩니다.


7. 고급 디컴파일 방지 가이드라인

MQL5 코드가 디컴파일되어 버리면 아무리 강력한 보안 솔루션도 크랙될 위험이 있습니다. 구글에서 찾아보니 MQL5 디컴파일러를 판매하는 사이트가 있던데 제가 보기에는 다른 개발자의 코드를 훔치려는 사람들을 이용해서 돈을 벌려고 하는 일종의 사기 같네요. 하지만 제가 실제로 이용해 본 것은 아니니 제 말이 틀릴 수도 있겠습니다. 실제로 디컴파일러가 존재한다고 해도 암호화된 EA 또는 인디케이터 인풋 변수를 전송하거나 객체 인덱스를 복사해 보안을 강화할 수 있죠.

아무리 해커라도 보안된 EA나 인디케이터의 인풋 변수를 알아내기는 힘듭니다. 계좌 ID가 일치하는 경우에만 변수를 전송하고 일치하지 않는 경우에는 암호화되지 않은 가짜 변수를 전송하는 방법도 있죠. PGP(Pretty Good Privacy) 프로그램을 사용하면 됩니다. 그러면 코드가 디컴파일되어도 데이터는 PGP로 암호화되어 계좌 ID와 PGP 키가 일치하는 경우에만 EA 변수가 복호화되거든요.


결론

MQL5 프로그램을 보호할 수 있는 몇 가지 방법을 살펴보았습니다. 베이스64 인코딩과 MQL5-RPC 호출을 이용한 원격 라이선스에 대해서도 설명했습니다. 이 글이 또 다른 MQL5 코드 보안 방법 개발에 도움이 되었으면 좋겠네요. 모든 소스 코드는 본문 하단에 첨부되어 있습니다.


MetaQuotes 소프트웨어 사를 통해 영어가 번역됨
원본 기고글: https://www.mql5.com/en/articles/359

EX5 라이브러리로 프로젝트 홍보하기 EX5 라이브러리로 프로젝트 홍보하기
클래스 및 함수 구현 세부 사항을 .ex5 파일에 은닉함으로써 다른 개발자들과 노하우를 공유하고 공동 프로젝트 작업을 하며 온라인에서 프로젝트를 홍보할 수도 있습니다. MetaQuotes에서 EX5 라이브러리 클래스의 직접 상속을 가능하게 하기 위해 열심히 개발 중이긴 하지만 우리가 한번 먼저 구현해 보도록 하겠습니다.
트레이드미네이터 3: 라이즈 오브 더 트레이딩 머신 트레이드미네이터 3: 라이즈 오브 더 트레이딩 머신
지난 글 '닥터 트레이드러브...'에서는 미리 선택된 매매 시스템의 매개 변수를 독자적으로 최적화할 수 있는 엑스퍼트 어드바이저를 만들었습니다. 게다가 한 가지 매매 시스템의 매개 변수를 최적화할뿐만 아니라 여러 매매 시스템 가운데 가장 좋은 시스템을 선택해 주는 엑스퍼트 어드바이저까지 만들기로 했죠. 어떻게 되나 봅시다.
AutoElliottWaveMaker-MetaTrader 5  엘리엇 파동 반자동 분석 도구 AutoElliottWaveMaker-MetaTrader 5 엘리엇 파동 반자동 분석 도구
이번 글에서는 MetaTrader 5 의 첫 번째 엘리엇 파동 반자동 분석 기구인 AutoElliottWaveMaker에 대해 알아보겠습니다. 해당 도구는 MQL5만으로 작성되어 있으며 외부 라이브러리를 포함하지 않습니다. 이는 MQL5 언어만으로도 충분히 고급 프로그래밍이 가능하다는 반증이기도 하죠.
엑스퍼트 어드바이저 비주얼 마법사로 엑스퍼트 어드바이저 만들기 엑스퍼트 어드바이저 비주얼 마법사로 엑스퍼트 어드바이저 만들기
MetaTrader 5 의 엑스퍼트 어드바이저 비주얼 마법사는 매우 직관적인 그래픽 환경과 다양한 매매 블록을 제공하여 단 몇 분만에 엑스퍼트 어드바이저를 만들 수 있도록 도와줍니다. 클릭, 드래그 앤드 드롭만 할 줄 알면 종이에 그리는 것처럼 외환 거래 전략을 시각화할 수 있습니다. 이렇게 만들어진 매매 다이어그램은 몰라니스(Molanis) MQL5 코드 생성기로 자동 분석되며 즉시 사용 가능한 엑스퍼트 어드바이저로 완성됩니다. 인터랙티브 그래픽 환경 덕분에 MQL5 코드를 쓰지 않고 간단하게 디자인할 수 있죠.