PlayerPrefs + XOR 연산
PlayerPrefs를 사용하여 게임 내 재화를 저장하는 방법을 구현해 보았다.
이 방법은 비트 연산을 활용하여 보안 기능을 추가한 방식이다.
public class GoldManager : SingleTon<GoldManager>
{
[SerializeField] private TextMeshProUGUI goldText;
private int currentGold;
private readonly int goldKey = 123456; // 암호화 키
public int CurrentGold
{
get => currentGold ^ goldKey; // 복호화
private set => currentGold = value ^ goldKey; // 암호화
}
private void Start()
{
LoadGold(); // 게임 시작 시 저장된 골드 불러오기
}
public void AddGold(int amount)
{
CurrentGold += amount;
SaveGold();
Debug.Log($"{amount} 골드 벌었습니다.");
}
public void RemoveGold(int amount)
{
CurrentGold -= amount;
SaveGold();
Debug.Log($"{amount} 골드 사용했습니다.");
}
private void SaveGold()
{
PlayerPrefs.SetInt("Gold", currentGold); // 암호화된 값 저장
PlayerPrefs.Save();
}
private void LoadGold()
{
if (PlayerPrefs.HasKey("Gold"))
{
currentGold = PlayerPrefs.GetInt("Gold"); // 암호화된 값 불러오기
}
else
{
currentGold = 0; // 기본값 설정
}
}
}
XOR 연산
XOR 연산은 같은 비트일 경우 0, 다른 비트일 경우 1이 되는 논리 연산이다.
이 특성을 활용하면 암호화된 값을 XOR 연산을 한 번 더 수행하여 원래 값으로 복원할 수 있다.
예를 들어, currentGold가 1000일 경우, 아래처럼 goldkey(123456)를 XOR 연산하면 암호화된 값을 얻을 수 있다.
1000 ^ 123456
00000000 00000000 00000011 11101000 (1000)
^ 00000000 00000001 11100010 01000000 (123456)
------------------------------------------------------------
00000000 00000001 11100001 10101000 (암호화된 값)
그리고 다시 XOR 연산을 수행하면 원래 값으로 복구된다.
XOR 연산을 이용한 암호화는 단순하지만 보안성이 높지 않다.
평문 데이터가 일정한 패턴을 가지면 키를 유추할 가능성이 존재하기 때문이다.
또한, PlayerPrefs는 암호화되지 않은 상태로 저장되므로,
단순히 XOR 연산만으로는 충분한 보안성을 제공하지 않는다.
또한 PlayerPrefs는 int, float, string 타입만 저장할 수 있기 때문에 복잡한 객체 저장이 어려운 단점이 있다.
XOR 연산을 추가하면 최소한의 보안 강화 효과를 얻을 수 있지만,
중요한 데이터 저장에는 이보다 강력한 암호화 기법을 사용해야 한다.
JSON + AES - 256 암복호화
JSON
JSON(JavaScript Object Notation)은 데이터를 저장하고 전송하기 위한 경량 데이터 포맷이다.
가독성이 좋고 쉽게 다룰 수 있어 많이 사용된다.
- 키-값 (key-value) 쌍으로 데이터 저장
- 가벼운 텍스트 형식으로 데이터를 쉽게 읽고 쓸 수 있다.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using TMPro;
using UnityEngine;
// 골드 데이터를 저장할 클래스 (JSON 변환용)
[System.Serializable]
public class GoldData
{
public int gold;
}
public class GoldManager : SingleTon<GoldManager>
{
[SerializeField] private TextMeshProUGUI goldText;
private string filePath;
// AES 암호화를 위한 Key(32바이트)와 IV(16바이트) 설정
private static readonly string _aesKey =
Convert.ToBase64String(Encoding.UTF8.GetBytes("A4FAa5K09Hsr1oZoa08Tsfqdm2ywB1Cl".PadRight(32, '0')));
private static readonly string _aesIv =
Convert.ToBase64String(Encoding.UTF8.GetBytes("ZNOjdMdYi2FqProV".PadRight(16, '0')));
private int currentGold;
public int CurrentGold
{
get => currentGold;
private set => currentGold = value;
}
private void Start()
{
// 골드 데이터를 JSON 파일로 저장할 경로 설정
filePath = Application.persistentDataPath + "/goldData.json";
LoadGold();
}
private void Update()
{
goldText.text = CurrentGold.ToString();
}
// 골드 추가 함수
public void AddGold(int amount)
{
CurrentGold += amount;
SaveGold();
}
// 골드 차감 함수
public void RemoveGold(int amount)
{
CurrentGold -= amount;
SaveGold();
}
// 현재 골드를 JSON 파일로 저장
private void SaveGold()
{
GoldData data = new GoldData { gold = currentGold };
string json = JsonUtility.ToJson(data);
string encryptedJson = Encrypt(json);
File.WriteAllText(filePath, encryptedJson);
}
// JSON 파일에서 골드를 불러오는 함수
private void LoadGold()
{
if (File.Exists(filePath))
{
string encryptedJson = File.ReadAllText(filePath);
string json = Decrypt(encryptedJson);
GoldData data = JsonUtility.FromJson<GoldData>(json);
currentGold = data.gold;
}
else
{
currentGold = 0;
}
}
// AES 암호화 함수
private static string Encrypt(string plainText)
{
byte[] encrypted;
using (Aes aes = Aes.Create())
{
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Key = Convert.FromBase64String(_aesKey);
aes.IV = Convert.FromBase64String(_aesIv);
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter sw = new StreamWriter(cs))
{
sw.Write(plainText);
}
encrypted = ms.ToArray();
}
}
}
return Convert.ToBase64String(encrypted);
}
// AES 복호화 함수
private static string Decrypt(string encryptedText)
{
string decrypted;
byte[] cipher = Convert.FromBase64String(encryptedText);
using (Aes aes = Aes.Create())
{
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Key = Convert.FromBase64String(_aesKey);
aes.IV = Convert.FromBase64String(_aesIv);
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream ms = new MemoryStream(cipher))
{
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
using (StreamReader sr = new StreamReader(cs))
{
decrypted = sr.ReadToEnd();
}
}
}
}
return decrypted;
}
}
AES 암복호화
AES(Advanced Encryption Standard)는 데이터 암호화에 널리 사용되는 대칭키 암호화 알고리즘이다.
대칭키 암호화란 암호화와 복호화에 동일한 키를 사용하는 방식을 의미한다.
AES는 보안성이 뛰어나고 성능이 좋아서 보안 데이터에 널리 활용된다.
AES 키 길이 종류
알고리즘 | 키 길이 | 블록 크기 |
AES - 128 | 128비트 (16바이트) | 128비트 |
AES - 192 | 192비트 (24바이트) | 128비트 |
AES - 256 | 256비트 (32바이트) | 128비트 |
키 길이가 길수록 보안성이 높아지지만, 연산 속도는 느려질 수 있다.
AES-256이 가장 보안이 강력하며, 실제로 많이 사용된다.
AES 중요 개념
1. Secret Key (비밀 키)
- 암호화와 복호화에 사용되는 핵심 키다.
- AES-256의 경우 32바이트(256비트) 길이를 사용한다.
- 키가 노출되면 암호화된 데이터를 쉽게 복호화할 수 있기 때문에 보안 관리가 중요하다.
2. IV (Initialization Vector, 초기화 벡터)
- CBC(Cipher Block Chaining) 모드에서 각 블록의 암호화를 다르게 만들기 위해 사용된다.
- 16바이트(128비트) 길이를 갖는다.
- 고정된 값보다는 랜덤하게 생성하여 보안성을 높이는 것이 좋다.
3. Cipher Mode (암호화 모드)
- AES는 여러 가지 암호화 모드를 지원하는데, 보안성과 성능을 고려하여 CBC 모드를 많이 사용한다.
- CBC 모드는 이전 블록의 암호화 결과를 현재 블록의 암호화에 반영하여 보안성을 높인다.
4. Padding Mode (패딩 방식)
- AES는 블록 단위(128비트)로 데이터를 암호화하기 때문에, 데이터 길이가 블록 크기의 배수가 아닐 경우 빈 공간을 채워주는 패딩 방식이 필요하다.
- 가장 일반적으로 사용되는 방식은 PKCS7이다.
'유니티' 카테고리의 다른 글
[유니티] #3 수박 게임 만들기 (사운드, 애니메이션, 이펙트) (0) | 2025.03.04 |
---|---|
[유니티] #2 수박 게임 만들기 (UI) (0) | 2025.02.26 |
[유니티] C#에서 여러 값을 반환하는 방법 : out, Tuple (0) | 2025.02.24 |
[유니티] #1 수박 게임 만들기 (최적화) (0) | 2025.02.15 |
[유니티] 디자인 패턴 : 오브젝트 풀링 (Object Pooling) (0) | 2025.02.15 |