게임 세이브 파일을 암호화하면 게임 진행 상황을 보호하고 부정 행위를 방지할 수 있습니다. 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에 사용할 키와 이니셜벡터를 가져옵니다.
  • 만약 값이 없으면, 무작위로 생성되어 저장되며, 그 값을 가져오도록 합니다.

 

✅ 적용

· 세이브 & 로드

 

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 함수를 호출하여 복호화하여 읽습니다.
bonnate