. 보안 처리

여기까지 작업한 후 FTP 기능을 테스트해보면 아주 잘 동작한다. 네트워크상의 파일을 잘 읽어오고 편집 결과도 잘 저장되며 속도가 좀 느릴 뿐 로컬 파일을 편집하는 것과 큰 차이없이 작업을 할 수 있다. 그러나 기능적으로는 큰 문제가 없지만 아주 심각한 문제가 있다. 자신이 알고 있는 FTP 서버에 접속해서 파일 하나를 읽어본 후 MRU를 열어보자. MRU는 최근 연 파일의 목록을 유지하며 FTP 경로에는 파일을 다시 읽을 수 있는 모든 정보가 고스란히 들어있다.

그래서 요렇게 보인다. 접속정보가 훤히 다 드러나 있는 것이다. 또한 당근은 편집중인 파일의 경로를 타이틀바에 보여주는데, FTP 파일의 경로에 접속정보가 표시되기 때문에 누구에게나 접속정보가 공개되는 셈이다. 만약 사무실에서 FTP 서버에 접속하여 아주 중요한 파일을 편집하고 있다고 해보자. 사무실에 보험 아줌마나 구두 미화원이 돌아다니다가 우연히 내 모니터를 어깨 넘어로 살짝 보고는 음음. 그렇단 말이지?하고 돌아간 후 FTP 서버를 해킹한다면 큰일이 아닐 수 없다.

FTP ID와 비밀번호만 알고 있으면 완전히 자기 것이 되며 무슨 짓을 하든지 말릴 수가 없다. 그렇다고 모니터를 가리고 작업을 할 수도 없지 않은가? 구두 미화원보다 더 경계해야 할 대상은 바로 내부 동료인데 타이틀바나 MRU가 문제가 아니라 레지스트리를 뒤지면 기억되어 있는 모든 접속정보를 일목 요연하게 볼 수 있으니 이 얼마나 무서운 일인가? 이런 상황이면 잠시 자리를 비우고 고민 해소하러 가기도 힘들 것이다.

보안 기능은 모든 프로그램에 있어 아주 중요한 문제이다. 당근이 이 문제를 제대로 해결하지 못하면 텍스트 편집기로서의 기능이 아무리 훌륭하다 하더라도 외면받을 수밖에 없다. 일단 접속정보를 숨겨서 밖으로 드러나지 않도록 수정해보자. FTP 경로에서 접속정보를 빼고 파일 이름만 추출하는 함수를 작성하되 이 함수는 프로젝트의 모든 곳에서 사용되므로 Util.cpp에 작성한다.

 

void EncodePath(TCHAR *Path, TCHAR *EncPath)

{

     TCHAR Server[256];

     TCHAR User[32];

     TCHAR Pass[32];

     TCHAR *File;

     int Port;

 

     if (strnicmp(Path,"ftp",3) == 0) {

          ParseFtpInfo(Path,Server,User,Pass,File,Port);

          wsprintf(EncPath,"ftp://%s/%s",Server,File);

     } else {

          lstrcpy(EncPath,Path);

     }

}

 

ParseFtpInfo로 접속정보를 분석한 후 접속 서버와 파일의 경로만으로 다시 경로를 조립하여 ID와 비밀 번호는 경로에서 삭제하였다. 타이틀바에 표시할 경로는 이 함수가 만들어낸 간략화된 경로로 대체한다. 파일을 여는 함수와 저장(업로드) 함수에 경로를 표시하는 코드가 있는데 이 코드를 다음과 같이 수정한다.

 

BOOL OpenFileToChild(HWND hChild, TCHAR *Path)

{

    TCHAR EncPath[MAX_PATH];

     ....

     pSi->Ae.SetModified(FALSE);

     lstrcpy(pSi->NowFile,Path);

    EncodePath(Path,EncPath);

    SetWindowText(hChild,EncPath);

 

     return TRUE;

}

 

BOOL SaveAs(HWND hChild)

{

    TCHAR EncPath[MAX_PATH];

     ....

     Mru.AddMRU(OFN.lpstrFile);

     lstrcpy(pSi->NowFile,OFN.lpstrFile);

    EncodePath(pSi->NowFile,EncPath);

    SetWindowText(hChild,EncPath);

         

경로 문자열을 바로 타이틀바에 출력하지 않고 EncodePath 함수로 간략화시켜서 출력하였다. 이제 타이틀바에는 FTP 접속정보가 보이지 않을 것이다. 다운로드 대화상자의 파일명도 간략화하여 보여주도록 수정한다.

 

int DgFtpDown(TCHAR *Path, TCHAR *&Text)

{

     TCHAR EncPath[MAX_PATH];

     ....

     EncodePath(Path,EncPath);

     wsprintf(Mes,"위치 : %s",EncPath);

     SetDlgItemText(hDlgDown,IDC_STDOWN1,Mes);

 

BOOL DgFtpUp(TCHAR *Path, TCHAR *Text)

{

     TCHAR EncPath[MAX_PATH];

     ....

     EncodePath(Path,EncPath);

     wsprintf(Mes,"위치 : %s",EncPath);

     SetDlgItemText(hDlgDown,IDC_STDOWN1,Mes);

 

다운로드받는 잠시 동안이라도 접속정보는 숨겨야 한다. MRU의 메뉴 텍스트에도 접속정보를 숨겨야 한다. ArrangeMenu 함수를 다음과 같이 수정한다.

 

void CMru::ArrangeMenu()

{

     int i;

     TCHAR szItem[MAX_PATH+12];

     TCHAR EncPath[MAX_PATH];

 

     if (arMru.num) {

          while (DeleteMenu(hMenu,0,MF_BYPOSITION));

     }

 

     for (i=0;i<arMru.num;i++) {

          EncodePath(arMru.Get(i),EncPath);

          if (i < 9) {

              wsprintf(szItem,"&%c %s",i+1+’0’,EncPath);

          } else {

              wsprintf(szItem,"&%c %s",i-9+’A’,EncPath);

          }

          AppendMenu(hMenu,MF_STRING,MenuID+i,szItem);

     }

}

 

타이틀 바나 MRU FTP 경로에 접속정보가 나타나지 않으므로 일단 구두 미화원에게는 접속정보를 들키지 않게 되었다. 하지만 단순히 보여줄 때 출력되는 문자열을 조작했을 뿐이지 레지스트리에는 여전히 완전한 정보가 저장되어 있다. 파일을 다시 다운로드받을 수 있어야 하므로 접속정보 자체를 없애버릴 수는 없으며 결국 남들이 알아보지 못하게 암호화를 하는 수 밖에 없다. 암호화를 위해 다음 네 함수를 Util.cpp에 작성한다.

 

void EncodeBinary(PBYTE buf, int Length, BOOL bEncode)

{

     int i;

     BYTE b;

     int Carry;

 

     for (i=0;i<Length;i++) {

          b=buf[i];

          if (bEncode == TRUE) {

              if (b >= 128) Carry = 1; else Carry = 0;

              b = b * 2 + Carry;

              b = b ^ 0xdb;

              if (b % 2 == 1) Carry = 128; else Carry = 0;

              b = b / 2 + Carry;

              b = b ^ 0xbd;

              buf[i] = b;

          } else {

              b = b ^ 0xbd;

              if (b >= 128) Carry = 1; else Carry = 0;

              b = b * 2 + Carry;

              b = b ^ 0xdb;

              if (b % 2 == 1) Carry = 128; else Carry = 0;

              b = b / 2 + Carry;

              buf[i] = b;

          }

     }

}

 

int BinaryToTextEnccode(PBYTE Src, int length, PBYTE Dest, BOOL bEncode)

{

     BYTE s1, s2, s3, s4, d1, d2, d3, d4;

     BYTE *buf;

     int si,di;

     int OriginalLen;

 

     if ((bEncode == FALSE) && (length < 12))

          return -3;

 

     if (bEncode) {

          buf=(PBYTE)malloc(length+8);

          memcpy(buf+6, Src, length);

          buf[0]=0x57;

          buf[1]=0xEC;

          buf[2]=0xFF;

          buf[3]=(length & 0xff0000) >> 16;

          buf[4]=(length & 0xff00) >> 8;

          buf[5]=(length & 0xff);

          buf[length+6]=0;

          buf[length+7]=0;

 

          for (si=0,di=0;si<length+6;si+=3,di+=4) {

              s1=buf[si];

              s2=buf[si+1];

              s3=buf[si+2];

              d1=(s1 >> 2) + 48;

              d2=(((s1 & 0x3) << 4) | (s2 >> 4)) + 48;

              d3=(((s2 & 0xf) << 2) | (s3 >> 6)) + 48;

              d4=(s3 & 0x3f) + 48;

              Dest[di]=d1;

              Dest[di+1]=d2;

              Dest[di+2]=d3;

              Dest[di+3]=d4;

          }

          Dest[di]=0;

          free(buf);

          return 0;

     } else {

          if ((Src[0] != ‘E’) || (Src[1] != ‘n’) || (Src[2] != ‘c’) || (Src[3] != ‘o’)) {

              return -1;

          }

          for (si=4,di=-3;si<length;si+=4,di+=3) {

              s1=Src[si]-48;

              s2=Src[si+1]-48;

              s3=Src[si+2]-48;

              s4=Src[si+3]-48;

              d1=(s1 << 2) | ((s2 & 0x30) >> 4);

              d2=(s2 << 4) | ((s3 & 0x3c) >> 2);

              d3=(s3 << 6) | (s4 & 0x3f);

 

              if (si==4) {

                   OriginalLen = (d1 << 16) + (d2 << 8) + d3;

                   if ((OriginalLen+2)/3*4+8 != length) {

                        return -2;

                   }

              } else {

                   Dest[di]=d1;

                   Dest[di+1]=d2;

                    Dest[di+2]=d3;

              }

          }

          return OriginalLen;

     }

}

 

void EncodeText(TCHAR *Text, TCHAR *EncText)

{

     int len;

     TCHAR *temp;

 

     len=lstrlen(Text)+1;

     temp=(TCHAR *)malloc(len);

     lstrcpy(temp,Text);

     EncodeBinary((PBYTE)temp,len,TRUE);

     BinaryToTextEnccode((PBYTE)temp,len,(PBYTE)EncText,TRUE);

     free(temp);

}

 

int DecodeText(TCHAR *EncText, TCHAR *Text)

{

     int len;

     len=BinaryToTextEnccode((PBYTE)EncText,lstrlen(EncText),(PBYTE)Text,FALSE);

     if (len < 0)

          return len;

     EncodeBinary((PBYTE)Text,len,FALSE);

     return 0;

}

 

아주 단순한 수준의 암호화 함수인데 보안상의 이유로 이 함수들에 대한 상세한 분석은 하지 않는다. 관심 있으면 직접 분석해보되 그래도 명색이 암호화 함수인지라 그다지 쉽게 분석되지는 않을 것이다. 코드 자체만으로는 분석하기가 어렵고 디버거의 도움을 좀 받으면 얼마든지 분석할 수 있을 것이다. 이 함수들은 3년 전에 만들었는데 가끔 쓸 일이 있어 다시 보면 만든 사람도 헷갈린다. 이 함수보다 조금 더 복잡한 방식의 암호화 함수를 직접 실무에 사용하기도 했었는데 아직까지는 별 문제가 없는 것 같다.

이 함수로 FTP 경로를 레지스트리에 저장하기 전에 암호화하고 실제 사용할 때 암호를 풀어서 사용하면 된다. 그러면 당근이 실행중일 때 내부적으로만 FTP 경로를 잠시 유지할 뿐 나머지 경우는 암호화되어 있으므로 접속정보가 노출되지 않는다. MRU의 저장 및 로드 함수를 다음과 같이 수정한다.

 

void CMru::LoadMRU()

{

     int i;

     TCHAR SubKey[32];

     TCHAR Path[MAX_PATH];

     TCHAR Text[MAX_PATH];

 

     arMru.num=SHRegReadInt(SHCU,Key,"nFiles",0);

     arMru.num=min(arMru.num, MaxMru);

     for (i=0;i<arMru.num;i++) {

          wsprintf(SubKey,"File%d",i);

          SHRegReadString(SHCU,Key,SubKey,"",Path,MAX_PATH);

          if (strncmp(Path,"Enco",4)==0) {

              DecodeText(Path,Text);

          } else {

              lstrcpy(Text,Path);

          }

          arMru.Set(i,Text);

     }

     ArrangeMenu();

}

 

void CMru::SaveMRU()

{

     int i;

     TCHAR SubKey[32];

     TCHAR EncText[MAX_PATH];

 

     SHRegWriteInt(SHCU,Key,"nFiles",arMru.num);

     for (i=0;i<arMru.num;i++) {

          wsprintf(SubKey,"File%d",i);

          if (strnicmp(arMru.Get(i),"ftp",3)==0) {

              EncodeText(arMru.Get(i),EncText);

          } else {

              lstrcpy(EncText,arMru.Get(i));

          }

          SHRegWriteString(SHCU,Key,SubKey,EncText);

     }

}

 

로컬 파일에 대한 경로는 굳이 암호화할 필요가 없으며 FTP 경로만 암호화하였다. 마지막으로 암호화할 가장 중요한 대상은 FTP 접속정보인 arFtp 배열이다. 이 배열에 FTP 서버에 대한 모든 정보가 들어 있으므로 근본적으로 보호해야 할 가장 중요한 정보이다. 그래서 이 배열을 저장하는 코드를 아직 작성하지 않았는데 SOption Load, Save 함수에 arFtp 저장 코드를 다음과 같이 작성하도록 하자.

 

void SOption::Load(TCHAR *Key)

{

     TCHAR EncText[400];

     ....

     lstrcpy(szKey,Key);

     lstrcat(szKey,"Ftp");

     j=SHRegReadInt(SHCU,szKey,"Num",0);

     for (i=0;i<j;i++) {

          wsprintf(szSub,"Name-%d",i);

          SHRegReadString(SHCU,szKey,szSub,"",EncText,32);

          DecodeText(EncText,arFtp[i].Name);

          wsprintf(szSub,"Addr-%d",i);

          SHRegReadString(SHCU,szKey,szSub,"",EncText,256);

          DecodeText(EncText,arFtp[i].Addr);

          wsprintf(szSub,"User-%d",i);

          SHRegReadString(SHCU,szKey,szSub,"",EncText,32);

          DecodeText(EncText,arFtp[i].User);

          wsprintf(szSub,"Pass-%d",i);

          SHRegReadString(SHCU,szKey,szSub,"",EncText,32);

          DecodeText(EncText,arFtp[i].Pass);

          wsprintf(szSub,"Port-%d",i);

          arFtp[i].Port=SHRegReadInt(SHCU,szKey,szSub,0);

     }

}

 

void SOption::Save(TCHAR *Key)

{

     TCHAR EncText[400];

     ....

     lstrcpy(szKey,Key);

     lstrcat(szKey,"Ftp");

     for (i=0;;i++) {

          if (lstrlen(arFtp[i].Name)==0) {

              SHRegWriteInt(SHCU,szKey,"Num",i);

              break;

          }

          wsprintf(szSub,"Name-%d",i);

          EncodeText(arFtp[i].Name,EncText);

          SHRegWriteString(SHCU,szKey,szSub,EncText);

          wsprintf(szSub,"Addr-%d",i);

          EncodeText(arFtp[i].Addr,EncText);

          SHRegWriteString(SHCU,szKey,szSub,EncText);

          wsprintf(szSub,"User-%d",i);

          EncodeText(arFtp[i].User,EncText);

          SHRegWriteString(SHCU,szKey,szSub,EncText);

          wsprintf(szSub,"Pass-%d",i);

          EncodeText(arFtp[i].Pass,EncText);

          SHRegWriteString(SHCU,szKey,szSub,EncText);

          wsprintf(szSub,"Port-%d",i);

          SHRegWriteInt(SHCU,szKey,szSub,arFtp[i].Port);

     }

}

 

사용자 ID, 비밀 번호는 물론이고 접속명, 서버의 주소까지 모두 암호화했다. 단 접속 포트는 정수값이고 일반적으로 21번이기 때문에 굳이 암호화를 할 필요가 없어 정수값을 바로 기록하였다. 여러 개의 FTP 서버를 등록한 후 레지스트리 편집기로 어떻게 접속정보가 저장되어 있는지 보도록 하자.

언뜻 봐서는 도대체 무슨 정보인지 알기가 무척 어렵다. 하지만 당근은 이 암호화된 문자열로부터 접속정보를 해독할 수 있으므로 FTP 접속은 이상없이 할 수 있다. 보안 문제가 일단은 해결되었다.

그러나 여기서 작성한 암호화 함수의 수준은 아주 초급적인 수준에 불과해서 암호에 대해 조금이라도 연구해 본 적이 있는 사람이라면 어렵지 않게 해독할 수 있다. 레지스트리 구성도 아주 직관적으로 되어 있는데 접속정보를 저장하는 서브키 이름은 Ftp이고 Addr, User, Pass 같은 이름을 주어 나 여기 있소하고 알려 주는 셈이다. 접속정보를 안전하게 지키려면 이 함수들보다는 훨씬 더 정교하고 강력한 수준의 암호화를 구현할 필요가 있다. 이미 공개된 암호화 알고리즘도 많이 있으므로 그 중 하나를 채택해서 적용하기만 하면 된다. 레지스트리 경로도 얼른 알 수 없는 이름으로 바꾸거나 아니면 아예 레지스트리를 사용하지 말아야 한다.

그러나 당근 1.0은 암호화가 필요하다는 것과 비록 초급 수준이지만 암호화를 하는 예를 보였으므로 여기까지만 암호화를 하기로 한다. 어차피 소스가 공개되어 있기 때문에 아무리 강력한 암호화를 하더라도 소스의 암호 해독 루틴을 떼어 내면 모든 암호를 풀 수 있어 현 시점에서 더 이상의 강력한 암호화 방법을 적용하는 것은 아무 의미가 없다. 다음 버전에서는 이보다 훨씬 더 강력한 방법으로 암호화를 할 것이다.