본문 바로가기
게임 프로그래밍/유니티 활용

[유니티 활용] Xor 와 Base64Encoding 을 이용한 초 간단 암호화 및 복호화

by 레오란다 2022. 12. 20.
반응형

이번 글에서는 "PlayerPrefs 와 JSON 을 이용한 인벤토리 데이터 관리" 에서 언급한 PlayerPrefs 의 취약한 보안을 보완하기 위한 방법에 대해 알아보도록 하겠습니다.

 

먼저 기본 원리에 대해 알아보도록 하겠습니다.

 

▶ XOR

먼저 XOR 의 특성은 다음과 같습니다. 0 과 1 은 비트입니다.

 

0 xor 1 = 1

1 xor 1 = 0

0 xor 0 = 0

1 xor 0 = 1

 

즉 xor 하는 두 비트가 다르면 1 같으면 0 이 됩니다.

 

예를 들어 [1 0 0 1 1 1 0 0] 에 [0 1 0 1 0 1 0 1] 을 xor 하면

 

이와 같이 전혀 다른 비트 배열이 됩니다. 즉 원래의 데이터가 변형되는 것입니다. 다시 이 결과에 [0 1 0 1 0 1 0 1] 을 xor 하면

 

다시 원래의 비트로 돌아왔습니다.

 

이렇게 원본 데이터에 key 로 사용할 값을 byte 별로 xor 연산을 해주면 아주 간단한 암호화가 가능하게 됩니다.

 

 

▶ Base64 Encoding

Base64 인코딩은 아래의 표와 같이 6비트로 이루어진 문자열 집합을 사용해 데이터를 인코딩하는 방식을 의미합니다.

이미지 출처 : Base64 - Wikipedia

인코딩의 기본 방식은 바이너리 데이터의 비트 배열을 6 비트씩 쪼개어 위의 표에 대응하는 문자를 가져오는 방식의 인코딩입니다.

 

예를 들어 100 bit 의 바이너리 데이터가 있다고 할 때 위의 방식대로 쪼개면 대략 

 

100 / 6 = 16.66666.... 

 

개의 64진수 데이터가 생깁니다. (padding 까지 계산하면 달라질테지만 계산 편의상 무시하겠습니다.)

 

그런데, 컴퓨터에서 문자를 표현하는 기본 단위는 ASCII 의 8비트 이기 때문에 실제로 인코딩 후 사용되는 데이터의 양은 base64 로 변환해 가져온 문자 하나당 8비트를 사용하게 됩니다. 즉,

 

16.66666..... * 8 = 133.33333333 이 됩니다.

 

원래 데이터 100bit 에서 인코딩 된 후의 데이터는 약 133 bit 로 33% 증가한다는 것을 알 수 있습니다. 

 

우리가 하려는 것은 유니티에서 Json 을 인코딩 하는 것이기 때문에 저장해야 할 데이터의 크기가 크다면 이 방식은 적합하지 않다는 것을 알 수 있습니다. 이 방식을 사용하려면 꼭 필요한 것들만 저장하는 설계가 필요합니다.

 

▶ 코드 작성

코드는 지난 글에 작성한 (이 글 시작 부분에 링크가 있습니다.) DataManager 를 수정하도록 하겠습니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DataManager : MonoBehaviour
{
   // 인벤토리에 저장되는 아이템 정보
   [System.Serializable]
   public class InventoryItem
   {
      public string itemName;
      public int stackNum;

      public InventoryItem(string itemName, int stackNum)
      {
         this.itemName = itemName;
         this.stackNum = stackNum;
      }
   }

   // 아이템 목록   
   public class InvBag
   {
      public List<InventoryItem> items;
   }

   InvBag invBag;

   private void Start()
   {
      // 저장된 json 문자열 가져오기. 없으면 빈 문자열이 반환됨
      var json = PlayerPrefs.GetString("GameData", "");
      if (json == "")
      {
         // 아무것도 없으면 인벤토리 가방 생성
         invBag = new InvBag();
         invBag.items = new List<InventoryItem>();

         // 테스트를 위해 가방에 아이템 넣기
         invBag.items.Add(new InventoryItem("Short Sword", 1));
         invBag.items.Add(new InventoryItem("Long Sword", 1));
         invBag.items.Add(new InventoryItem("Heal Potion", 5));
         invBag.items.Add(new InventoryItem("Mana Potion", 10));

         // 가방의 정보를 json 문자열로 가져오기
         json = JsonUtility.ToJson(invBag);

         // json 문자열 암호화
         var encStr = Encrypt(json, 0x22);         
         print(encStr);
         PlayerPrefs.SetString("GameData", encStr);
      }
      else // 저장된 문자열이 있다면...
      {
         // json 문자열 복호화        
         var decStr = Decrypt(json, 0x22);
         print(decStr);         
         invBag = JsonUtility.FromJson<InvBag>(decStr);
      }
   }
   
   string Encrypt(string s, byte key)
   {
      // 입력된 문자열 s 를 byte 배열로 변환합니다.
      var bytes = System.Text.Encoding.UTF8.GetBytes(s);

      for (int i=0; i<bytes.Length; i++)
      {
         // 각 바이트에 인자로 받은 key 를 xor 합니다.
         bytes[i] = (byte)(bytes[i] ^ key);
      }

      // xor한 byte 배열을 다시 base64 인코딩합니다.
      return System.Convert.ToBase64String(bytes);
   }

   string Decrypt(string s, int key)
   {
      // 입력된 base64 인코딩된 문자열 s 를 byte 배열로 변환합니다.
      var bytes = System.Convert.FromBase64String(s);

      for (int i=0; i < bytes.Length; i++)
      {
         // 모든 배열에 인자로 받은 key 를 xor 합니다.         
         bytes[i] = (byte)(bytes[i] ^ key);
      }

      // byte 배열을 string 으로 변환합니다.
      return System.Text.Encoding.UTF8.GetString(bytes);
   }
}
  • Encrypt 와 Decrypt 의 두 번째 인자인 key 값은 동일해야 하며 실제로 사용하실 때는 다른 값(0x00 ~ 0xFF) 로 변경해서 사용하시면 됩니다.

이 방식은 PlayerPrefs 을 이용해 저장했을 때 저장되는 위치를 안다면 모든 정보가 text 형태로 노출되기 때문에 너무 손쉽게 수정할 수 있는 취약점을 간단한 방법을 이용해 막은 것뿐입니다.

 

만약 해커가 이걸 뚫겠다고 하면 base64 인코딩의 특성상 알파벳 대소문자와 숫자 그리고 +, -,  padding 문자열인 = 만 사용되기 때문에 쉽게 base64 인코딩이라는 것을 눈치챌 수 있습니다. base64 디코딩을 해서 얻은 바이너리가 Text 가 아니라면 아마도 또 다른 인코딩이 되어 있다는 것을 알 것이고 가장 손쉽게 사용할 수 있는 xor 도 시도해 볼 것입니다. 왜냐하면 key로 쓸 수 있는 범위가 0x00 ~ 0xFF 까지 한정되어 있기 때문에 프로그램을 이용해 순식간에 key 값을 알아 낼 수 있기 때문입니다.

 

하지만 이 방식의 암호화를 이용하지 않았을 때와 비교하면 위의 작업을 시도하는 해커만 뚫을 수 있고, PlayerPrefs 의 정보가 어디에 저장되는지 정도만 알고 있는 해커(?) 는 데이터 변조를 못하고 포기하게 될 것입니다. 이것은 큰 차이입니다.

 

이 방식은 멀티 플레이 게임이 아닌 싱글 플레이 게임에서 간단하게 사용할 때 적합한 방식이라고 생각합니다.

반응형

댓글