게임 세이브 파일을 암호화하면 게임 진행 상황을 보호하고 부정 행위를 방지할 수 있습니다. AES를 이용하여 파일을 암호화하고 읽는 방법을 정리하였습니다.
📺 미리보기
· 암호화 이전
· 암호화 이후
📖 구현 내용
- string 문자열을 쉽게 암호화하고, 복호화 할 수 있습니다.
- AES를 사용하기위한 키와 이니셜벡터를 스크립트에 하드코딩 하지 않습니다.
- 전역 함수를 사용하여 컴포넌트를 찾거나 레퍼런스 하지 않고 직접 호출하여 편리하게 사용 가능합니다.
⚒️ 구현
· AES.cs
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
/// <summary>
/// AES 암호화 및 복호화
/// </summary>
public static class AES
{
// PlayerPrefs에 저장할 key와 iv의 이름
private const string KEY_PREF_KEY = "AES_KEY";
private const string IV_PREF_KEY = "AES_IV";
// AES 키와 초기화 벡터를 저장하는 변수
private static byte[] key;
private static byte[] iv;
static AES()
{
LoadKeyAndIV();
}
/// <summary>
/// 평문을 암호화하여 Base64로 리턴
/// </summary>
/// <param name="plaintext">암호화 할 평문</param>
/// <returns>Base64</returns>
public static string Encrypt(string plaintext)
{
// AES 를 이용하여 암호화
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
// 인터페이스
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
byte[] encryptedBytes = null;
// MemoryStream과 CryptoStream을 사용하여 암호화
using (var memoryStream = new System.IO.MemoryStream())
{
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
using (var streamWriter = new System.IO.StreamWriter(cryptoStream))
{
streamWriter.Write(plaintext);
}
encryptedBytes = memoryStream.ToArray();
}
}
// Base64 문자열로 인코딩하여 반환합니다.
return System.Convert.ToBase64String(encryptedBytes);
}
}
/// <summary>
/// Base64로 암호화된 문자열을 평문으로 복호화
/// </summary>
/// <param name="encryptedText">복호화 할 Base64 문자열</param>
/// <returns>평문</returns>
public static string Decrypt(string encryptedText)
{
// Base64 -> 평문
byte[] cipherBytes = System.Convert.FromBase64String(encryptedText);
// AES 를 이용하여 복호화
using (Aes aesAlg = Aes.Create())
{
// 키와 이니셜벡터
aesAlg.Key = key;
aesAlg.IV = iv;
// 인터페이스
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// MemoryStream과 CryptoStream을 사용하여 복호화
using (var memoryStream = new System.IO.MemoryStream(cipherBytes))
{
using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
using (var streamReader = new System.IO.StreamReader(cryptoStream))
{
return streamReader.ReadToEnd();
}
}
}
}
}
// PlayerPrefs에서 key와 iv를 불러옴
//만약 저장되어 있지 않으면 무작위로 생성
private static void LoadKeyAndIV()
{
// PlayerPrefs에서 가져오기
string keyStr = PlayerPrefs.GetString(KEY_PREF_KEY, null);
string ivStr = PlayerPrefs.GetString(IV_PREF_KEY, null);
// 만약 둘 중 하나라도 없다면?
if (string.IsNullOrEmpty(keyStr) || string.IsNullOrEmpty(ivStr))
{
// 무작위로 생성
using (Aes aesAlg = Aes.Create())
{
key = aesAlg.Key;
iv = aesAlg.IV;
}
// PlayerPrefs에 저장
PlayerPrefs.SetString(KEY_PREF_KEY, System.Convert.ToBase64String(key));
PlayerPrefs.SetString(IV_PREF_KEY, System.Convert.ToBase64String(iv));
}
// 저장된 key와 iv를 불러옴
else
{
key = System.Convert.FromBase64String(keyStr);
iv = System.Convert.FromBase64String(ivStr);
}
}
}
// PlayerPrefs에 저장할 key와 iv의 이름
private const string KEY_PREF_KEY = "AES_KEY";
private const string IV_PREF_KEY = "AES_IV";
- key와 iv를 직접 하드코딩하지않고 우회하여 PlayerPrefs에서 가져오기 위한 키 입니다.
- 하드코딩보다 보다 안전하다고 판단하여 PlayerPrefs에 저장합니다.
- 하지만 PlayerPrefs도 항상 안전한것은 아닙니다.
/// <summary>
/// 평문을 암호화하여 Base64로 리턴
/// </summary>
/// <param name="plaintext">암호화 할 평문</param>
/// <returns>Base64</returns>
public static string Encrypt(string plaintext)
{
// AES 를 이용하여 암호화
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
// 인터페이스
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
byte[] encryptedBytes = null;
// MemoryStream과 CryptoStream을 사용하여 암호화
using (var memoryStream = new System.IO.MemoryStream())
{
using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
{
using (var streamWriter = new System.IO.StreamWriter(cryptoStream))
{
streamWriter.Write(plaintext);
}
encryptedBytes = memoryStream.ToArray();
}
}
// Base64 문자열로 인코딩하여 반환합니다.
return System.Convert.ToBase64String(encryptedBytes);
}
}
- 평문 문자열은 Base64 형태로 암호화하는 함수입니다.
- AES 암호화하기위한 키와 이니셜벡터는 정적 생성자에의해 자동으로 준비되며 평문 문자열을 넣으면 유연하게 실행됩니다.
/// <summary>
/// Base64로 암호화된 문자열을 평문으로 복호화
/// </summary>
/// <param name="encryptedText">복호화 할 Base64 문자열</param>
/// <returns>평문</returns>
public static string Decrypt(string encryptedText)
{
// Base64 -> 평문
byte[] cipherBytes = System.Convert.FromBase64String(encryptedText);
// AES 를 이용하여 복호화
using (Aes aesAlg = Aes.Create())
{
// 키와 이니셜벡터
aesAlg.Key = key;
aesAlg.IV = iv;
// 인터페이스
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// MemoryStream과 CryptoStream을 사용하여 복호화
using (var memoryStream = new System.IO.MemoryStream(cipherBytes))
{
using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
{
using (var streamReader = new System.IO.StreamReader(cryptoStream))
{
return streamReader.ReadToEnd();
}
}
}
}
}
- Base64 형태의 문자열을 평문으로 복호화하는 함수입니다.
- AES 복호화하기위한 키와 이니셜벡터는 위 함수와 동일하게 정적 생성자에의해 자동으로 준비되며 암호화 된 Base64문을 넣으면 유연하게 실행됩니다.
// PlayerPrefs에서 key와 iv를 불러옴
//만약 저장되어 있지 않으면 무작위로 생성
private static void LoadKeyAndIV()
{
// PlayerPrefs에서 가져오기
string keyStr = PlayerPrefs.GetString(KEY_PREF_KEY, null);
string ivStr = PlayerPrefs.GetString(IV_PREF_KEY, null);
// 만약 둘 중 하나라도 없다면?
if (string.IsNullOrEmpty(keyStr) || string.IsNullOrEmpty(ivStr))
{
// 무작위로 생성
using (Aes aesAlg = Aes.Create())
{
key = aesAlg.Key;
iv = aesAlg.IV;
}
// PlayerPrefs에 저장
PlayerPrefs.SetString(KEY_PREF_KEY, System.Convert.ToBase64String(key));
PlayerPrefs.SetString(IV_PREF_KEY, System.Convert.ToBase64String(iv));
}
// 저장된 key와 iv를 불러옴
else
{
key = System.Convert.FromBase64String(keyStr);
iv = System.Convert.FromBase64String(ivStr);
}
}
- PlayerPrefs에서 AES에 사용할 키와 이니셜벡터를 가져옵니다.
- 만약 값이 없으면, 무작위로 생성되어 저장되며, 그 값을 가져오도록 합니다.
✅ 적용
· 세이브 & 로드
- 세이브 파일에 암호화를 적용하여 플레이어가 세이브파일을 변조하여 부적절한 플레이를 하는것을 방지합니다.
- 세이브 및 로드 기능은 "[유니티] 게임 저장 시스템(3) - 매니저"를 확인하세요.
public void SaveGameData(int slotId)
{
GameDataCore gameDataCore = new GameDataCore();
// Universal Data
{
...
}
// Scene Individual Data
{
...
}
// 파일 저장
string ToJsonData = JsonUtility.ToJson(gameDataCore);
File.WriteAllText(_FILE_PATH + slotId, AES.Encrypt(ToJsonData));
}
- File.WriteAllText를 이용하여 파일을 저장할 때, 저장할 대상인 ToJsonData를 AES.Encrypt를 호출하여 암호화하여 저장합니다.
Dks2PM8EAflvsRbbHntmdLlgxaXQdHmxOP2+vXgZTOs6O1Fk0RXlUl+APRRusoOZQBsfdWQOnZxI/wQoRweMoZiKk15MJh57ub05ZfqcELbDbANF3bK5sRfuEjDapfy7SL8msnNH0vLZ/CLnclx91 ...
- 다음과같이 해석할 수 없는 값으로 저장된것을 볼 수 있습니다.
public void LoadGameData(int slotId, string? forceSceneName = null)
{
if (FileExists(slotId) == false)
return;
...
// 파일 읽기
string fromJson = File.ReadAllText(_FILE_PATH + slotId);
GameDataCore gameDataCore = JsonUtility.FromJson<GameDataCore>(AES.Decrypt(fromJson));
// Universal Data
{
...
}
// Scene Individual Data
{
...
}
}
- 세이브파일을 로드 할때는 암호화 된 값을 AES.Decrypt 함수를 호출하여 복호화하여 읽습니다.
'unity tools & functions' 카테고리의 다른 글
[유니티] 유니티 에디터 스크린샷 캡쳐 도구 (0) | 2023.06.28 |
---|---|
[유니티] Hex를 RGB Color로 변환 (0) | 2023.06.27 |
[유니티] InputField Tab Navigation, Button Event (0) | 2023.06.22 |
[유니티] 오브젝트 위치 자동 정렬 (0) | 2023.05.09 |
[유니티] UI 창 드래그 (0) | 2023.04.03 |