Deff_Dev

[Unity/C#] AES 암호화 알고리즘 본문

Unity(유니티)/유니티 공부

[Unity/C#] AES 암호화 알고리즘

Deff_a 2024. 6. 23. 15:04

Json 저장 및 Base-64를 이용한 암호화 관련 글

 

[Unity/C#] Json 저장 및 base-64를 이용한 암호화/복호화

게임 데이터를 저장할 때, Json을 많이 사용하다. 하지만 데이터를 그냥 저장하게 된다면 플레이어가 데이터를 확인하고 임의로 수정할 수 있는 보안 문제가 발생할 수 있습니다. 따라서 Json 데

deff-dev.tistory.com

 

기존에는 Base-64를 이용한 인코딩을 통해 데이터를 저장했으나, 이 방법은 보안성이 부족하여 AES 암호화 알고리즘을 도입하여 데이터 저장 및 불러오기 기능을 보호하도록 개선했다.


AES (Advanced Encryption Standard)

  • 대칭키 암호화 알고리즘
  • 키 값을 가지고 암호화/복호화를 하므로 키 값을 모른다면 데이터를 확인 할 수 없다.
  • 암호화 키는 128, 192, 256 세 가지 중 하나가 될 수 있으며, 각각 AES-128, AES-192, AES-256라고 불린다.

 

AES 암호화 알고리즘을 사용하기 위해서는 Key 값과 IV 값을 가지고 있어야 한다.

 

Key

  • 데이터를 암호화/복호화를 하기 위해 필요한 값 즉, 키 값을 모른다면 데이터를 저장하거나 불러올 수 없다.

IV   

  • 초기화 벡터로, 암호화 과정에서 입력 데이터의 패턴을 깨뜨려 동일한 텍스트가 동일하게 암호화되지 않도록 한다.

AES 암호화 알고리즘 적용

 

라이브러리

using System.Security.Cryptography;

C# 에서 AES 암호화 알고리즘을 사용하기 위해서는 위 라이브러리를 선언해야된다.

 

Key, IV 생성

 

Key 값과 IV 값을 무작위로 생성해야 예측이 불가능할 것이다.

 

RNGCryptoServiceProvider를 이용하여 지정된 Bit 수 만큼 랜덤 바이트 배열을 생성하는 방법을 사용했다.

    private byte[] key; // 암호화에 사용되는 키
    private byte[] iv; // 초기화 벡터 
    private readonly string keyPath = Path.Combine(Application.persistentDataPath, "aesKey.dat");
    private readonly string ivPath = Path.Combine(Application.persistentDataPath, "aesIV.dat");

    public AESCrypto()
    {
        if (File.Exists(keyPath) && File.Exists(ivPath)) // 키와 IV가 존재하는 지 확인
        {
            // 존재한다면 해당 키와 IV를 읽어옴
            key = File.ReadAllBytes(keyPath);
            iv = File.ReadAllBytes(ivPath);
        }
        else
        {
            // 없다면 다시 생성
            key = GenerateRandomBytes(32); // 256-bit key
            iv = GenerateRandomBytes(16);  // 128-bit IV

            File.WriteAllBytes(keyPath, key);
            File.WriteAllBytes(ivPath, iv);
        }
    }
    
    // 지정된 길이의 랜덤 바이트 배열을 생성
    private byte[] GenerateRandomBytes(int length)
    {
        byte[] randomBytes = new byte[length];
        
        // RNGCryptoServiceProvider : 난수 발생기
        using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
        {
            rng.GetBytes(randomBytes);
        }
        return randomBytes;
    }

 

 

생성자를 통해 기존에 생성된 Key, IV 데이터가 있다면 해당 데이터를 불러오고 데이터가 존재하지 않다면 새로운 데이터를 생성한다.

 

System.Security.Cryptography의 객체들은 네이티브 리소스를 사용하여 가비지 컬렉터(GC)가 자동으로 리소스를 해제하지 않기 때문에 수동으로 리소스를 해제해줘야 한다.

 

※ 네이티브 리소스 : 운영 체제나 하드웨어와 직접적으로 상호작용하는 리소스

 

그렇기 때문에 Using을 사용하여 객체를 생성하고 객체 사용이 끝나면 자동으로 리소스를 해제해준다.

 

암호화 / 복호화

 

Aes 객체와  Base-64 문자열을 이용하여 암호화/복호화 한다.

public string EncryptString(string plainText)
{
    // AES 객체와 같은 암호화 객체는 네이티브 리소스를 사용하므로 사용후에 반드시 해제해야한다.
    // using을 사용하여 작업이 끝난 후에 자동으로 리소스가 해제되게 설계했다.
    // 네이티브 리소스는 운영 체제나 하드웨어와 직접적으로 상호작용하는 리소스 (GC에 의해 자동으로 관리되지 않기 떄문에 수동으로 해제해줘야 함)
    using (Aes aesAlg = Aes.Create()) // AES 알고리즘 생성
    {
        // 키, IV 설정
        aesAlg.Key = key;
        aesAlg.IV = iv;

        // 암호화 변환기를 생성
        ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
        // 평문 텍스트를 암호화
        byte[] encrypted = encryptor.TransformFinalBlock(Encoding.UTF8.GetBytes(plainText), 0, plainText.Length);
        
        // 암호화된 바이트 배열을 Base64 문자열로 변환 후 반환
        return System.Convert.ToBase64String(encrypted);
    }
}

public string DecryptString(string cipherText) // 복호화 함수
{
    // Base64 문자열을 바이트 배열로 변환
    byte[] buffer = System.Convert.FromBase64String(cipherText);

    using (Aes aesAlg = Aes.Create()) // AES 알고리즘 생성
    {
        // 키, IV 설정
        aesAlg.Key = key;
        aesAlg.IV = iv;

        // 복호화 변환기를 생성
        ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
        // 암호화된 바이트 배열을 복호화 
        byte[] decrypted = decryptor.TransformFinalBlock(buffer, 0, buffer.Length);

        // 복호화된 바이트 배열을 UTF-8 문자열로 변환 후 반환
        return Encoding.UTF8.GetString(decrypted);
    }
}

 

암호화할 사용한 키와 동일한 키를 복호화 과정에서 사용해야 데이터를 불러올 수 있다.


실제 적용

 

PlayerDataManager.cs

더보기
using UnityEngine;
using System.IO;

public class PlayerDataManager : MonoBehaviour
{
    private AESCrypto crypto;
    private string path;

    private void Awake()
    {
        crypto = new AESCrypto();
        path = Path.Combine(Application.persistentDataPath, "PlayerData.json");
    }
    public void DataSave(PlayerSaveData playerData)
    {
        string json = JsonUtility.ToJson(playerData, true); // 데이터 직렬화
        string encryptedJson = crypto.EncryptString(json); // 직렬화 된 데이터 암호화
        
        File.WriteAllText(path, encryptedJson);   
    }

    public void DataLoad()
    {
        if (!File.Exists(path))
        {
            Debug.Log("데이터가 존재하지 않습니다 !");
            return;
        }
        
        string encryptedJson = File.ReadAllText(path); // 암호화된 데이터 읽어옴
        string json =crypto.DecryptString(encryptedJson); // 복호화 및 역직렬화
        
        PlayerSaveData playerData = JsonUtility.FromJson<PlayerSaveData>(json);
    }

}

 

PlayerData 클래스는 임의로 넣은 데이터 클래스로 저장을 원하는 데이터를 넣으면 된다.

AESCrypto.cs

더보기
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;

public class AESCrypto
{ 
    private byte[] key; // 암호화에 사용되는 키
    private byte[] iv; // 초기화 벡터 
    private readonly string keyPath = Path.Combine(Application.persistentDataPath, "aesKey.dat");
    private readonly string ivPath = Path.Combine(Application.persistentDataPath, "aesIV.dat");

    public AESCrypto()
    {
        if (File.Exists(keyPath) && File.Exists(ivPath)) // 키와 IV가 존재하는 지 확인
        {
            // 존재한다면 해당 키와 IV를 읽어옴
            key = File.ReadAllBytes(keyPath);
            iv = File.ReadAllBytes(ivPath);
        }
        else
        {
            // 없다면 다시 생성
            key = GenerateRandomBytes(32); // 256-bit key
            iv = GenerateRandomBytes(16);  // 128-bit IV

            File.WriteAllBytes(keyPath, key);
            File.WriteAllBytes(ivPath, iv);
        }
    }
    
    // 지정된 길이의 랜덤 바이트 배열을 생성
    private byte[] GenerateRandomBytes(int length)
    {
        byte[] randomBytes = new byte[length];
        
        // RNGCryptoServiceProvider : 난수 발생기
        using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
        {
            rng.GetBytes(randomBytes);
        }
        return randomBytes;
    }
    
    public string EncryptString(string plainText)
    {
        // AES 객체와 같은 암호화 객체는 네이티브 리소스를 사용하므로 사용후에 반드시 해제해야한다.
        // using을 사용하여 작업이 끝난 후에 자동으로 리소스가 해제되게 설계했다.
        // 네이티브 리소스는 운영 체제나 하드웨어와 직접적으로 상호작용하는 리소스 (GC에 의해 자동으로 관리되지 않기 떄문에 수동으로 해제해줘야 함)
        using (Aes aesAlg = Aes.Create()) // AES 알고리즘 생성
        {
            // 키, IV 설정
            aesAlg.Key = key;
            aesAlg.IV = iv;

            // 암호화 변환기를 생성
            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
            // 평문 텍스트를 암호화
            byte[] encrypted = encryptor.TransformFinalBlock(Encoding.UTF8.GetBytes(plainText), 0, plainText.Length);
            
            // 암호화된 바이트 배열을 Base64 문자열로 변환 후 반환
            return System.Convert.ToBase64String(encrypted);
        }
    }

    public string DecryptString(string cipherText) // 복호화 함수
    {
        // Base64 문자열을 바이트 배열로 변환
        byte[] buffer = System.Convert.FromBase64String(cipherText);

        using (Aes aesAlg = Aes.Create()) // AES 알고리즘 생성
        {
            // 키, IV 설정
            aesAlg.Key = key;
            aesAlg.IV = iv;

            // 복호화 변환기를 생성
            ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
            // 암호화된 바이트 배열을 복호화 
            byte[] decrypted = decryptor.TransformFinalBlock(buffer, 0, buffer.Length);

            // 복호화된 바이트 배열을 UTF-8 문자열로 변환 후 반환
            return Encoding.UTF8.GetString(decrypted);
        }
    }
}

데이터 파일
Json 데이터

 


[참고 블로그]

 

[Unity] 유니티 데이터 저장하기[1] - (PlayerPrefs + 암호화)

유니티를 이용해서 게임/ 어플리케이션을 제작할 경우 생성되는 데이터들...저 역시 개발을 진행 할 수록 어플리케이션이 종료되어도 유지되는 데이터들을어딘가에 저장해야하는 상황이 발생

ljhyunstory.tistory.com