이번에는 저번에 세이브 로드를 암호화 하는 과정을 구상만 했었는데,
실제로 프로젝트에 암호화를 적용해야할 일이 생겼다.
우리 프로젝트의 Save Json 원본이다.
여기서 값만 수정한다면, 게임에서도 수정된 파일이 그대로 실행되었다.
이를 방지하기 위해 암호화 알고리즘을 적용했다.
private const int SaveSlot = 0;
private byte[] key;
private byte[] iv;
InitializeKeyAndIV 메서드는 암호화에 사용될 키(key)와 초기화 벡터(IV)를 설정한다.
만약 이전에 저장된 키와 IV가 없다면 새로 생성하고, 있으면 기존의 값을 사용한다.
private void InitializeKeyAndIV()
{
if (PlayerPrefs.HasKey("EncryptionKey") && PlayerPrefs.HasKey("EncryptionIV"))
{
key = Convert.FromBase64String(PlayerPrefs.GetString("EncryptionKey"));
iv = Convert.FromBase64String(PlayerPrefs.GetString("EncryptionIV"));
}
else
{
using (var aesAlg = Aes.Create())
{
aesAlg.GenerateKey();
aesAlg.GenerateIV();
key = aesAlg.Key;
iv = aesAlg.IV;
SaveKeyAndIVToPlayerPrefs(key, iv);
}
}
}
이 키와 IV는 PlayerPrefs에 Base64 인코딩 형태로 저장되어 애플리케이션 재시작 후에도 사용할 수 있다.
private void SaveKeyAndIVToPlayerPrefs(byte[] key, byte[] iv)
{
PlayerPrefs.SetString("EncryptionKey", Convert.ToBase64String(key));
PlayerPrefs.SetString("EncryptionIV", Convert.ToBase64String(iv));
PlayerPrefs.Save();
}
이 글을 보고 구현하시는 분들은 PlayerPrefs보다 서버를 이용할 수 있으면 서버에 저장하거나, 안전한 곳에 관리하시길 바랍니다.
SaveData 메서드는 제공된 데이터를 JSON 형식으로 직렬화하고, AES 알고리즘을 사용하여 암호화한다.
public void SaveData(DataManager data)
{
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
string encrypted = Encrypt(json);
string hash = ComputeSha256Hash(encrypted);
string path = GetSaveFilePath();
File.WriteAllText(path, encrypted);
File.WriteAllText(path + ".hash", hash);
}
private string Encrypt(string plainText)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
}
return Convert.ToBase64String(msEncrypt.ToArray());
}
}
}
암호화된 데이터는 지정된 파일 경로에 저장되며, 데이터의 무결성을 검증하기 위해 SHA-256 해시 값도 함께 저장된다.
private string ComputeSha256Hash(string rawData)
{
using (SHA256 sha256hash = SHA256.Create())
{
byte[] bytes = sha256hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
return builder.ToString();
}
}
LoadData 메서드는 저장된 파일에서 암호화된 데이터와 해시 값을 읽어온다.
읽어온 데이터의 해시 값이 저장된 해시 값과 일치하는지 확인하여 파일의 무결성을 검증합니다.
public DataManager LoadData()
{
string path = GetSaveFilePath();
if (File.Exists(path))
{
string encrypted = File.ReadAllText(path);
string hash = File.ReadAllText(path + ".hash");
string newHash = ComputeSha256Hash(encrypted);
if (hash != newHash)
{
Debug.LogError("파일 무결성 검증 실패");
return null;
}
string decrypted = Decrypt(encrypted);
return JsonConvert.DeserializeObject<DataManager>(decrypted);
}
return null;
}
무결성이 확인되면 데이터를 복호화하여 원래의 형태로 복원합니다.
private string Decrypt(string cipherText)
{
string plaintext = null;
byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherTextBytes))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
plaintext = srDecrypt.ReadToEnd();
}
}
}
}
return plaintext;
}
DeleteData 메서드는 저장된 데이터 파일과 해당 파일의 해시 값을 삭제한다.
혹시나 데이터를 삭제할 일이 필요하다면, 유니티와 연결하기 위해 임시로 구현했다.
public void DeleteData()
{
string path = GetSaveFilePath();
if (File.Exists(path))
{
File.Delete(path);
}
if (File.Exists(path + ".hash"))
{
File.Delete(path + ".hash");
}
}
Json 파일 내부 암호화 완료.
Json 파일을 SHA-256으로 해싱하여 무결성 검사를 진행한다.