이번 글에서는 "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비트로 이루어진 문자열 집합을 사용해 데이터를 인코딩하는 방식을 의미합니다.
인코딩의 기본 방식은 바이너리 데이터의 비트 배열을 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 의 정보가 어디에 저장되는지 정도만 알고 있는 해커(?) 는 데이터 변조를 못하고 포기하게 될 것입니다. 이것은 큰 차이입니다.
이 방식은 멀티 플레이 게임이 아닌 싱글 플레이 게임에서 간단하게 사용할 때 적합한 방식이라고 생각합니다.
'게임 프로그래밍 > 유니티 활용' 카테고리의 다른 글
[유니티 활용] Platform Effector 2D 를 이용한 관통 플랫폼 만들기 (1) | 2022.12.22 |
---|---|
[유니티 활용] Rigidbody2d 를 이용한 캐릭터의 이동과 점프 (4) | 2022.12.21 |
[유니티 활용] PlayerPrefs 와 JSON 을 이용한 인벤토리 데이터 관리 (2) | 2022.12.19 |
[유니티 활용] 2D 배경 스크롤과 장애물 생성 및 이동 (0) | 2022.12.18 |
[유니티 활용] 2D 캐릭터 점프 구현 (feat. Animator) (1) | 2022.12.17 |
댓글