유니티, C#) AES 암호화/복호화 + JSON 데이터 저장/불러오기

2025. 2. 10. 08:05·기능 구현 방법 정리

게임을 꺼도 유지되어야 할 데이터는
간단한 싱글게임에서는 PlayerPrefs를 이용해 저장하고는 했습니다.

 

하지만 이 경우 변조가 매우 쉽기에 최소한의 보안조치로

데이터를 암호화하여 세이브 파일로 저장하는 방법을 공부해 보았습니다.


해당 내용은 통신용 패킷 암호화/복호화에도 활용이 가능합니다.
 
그 과정을 간단히 말해보자면,
 
1) 데이터 저장
데이터 > JSON 형식으로 직렬화 > 암호화 > 파일 출력으로 저장
 
2) 데이터 불러오기
파일 불러오기 > 복호화 > JSON에서 데이터 형식으로 변환 > 데이터
 
저장과 불러오기는 역순으로 진행됩니다.
 
 

1. 데이터 타입 클래스 선언

여기서는 클래스로 데이터를 담아두고 저장 및 불러오는 형태로 썼습니다.
아래 예시 코드의 PlayerData, Container 클래스를 참조하여 필요에 맞게 변형하면 됩니다.
 
 

2. JSON 형식으로 변환 / 원래 데이터 형식으로 복구

JSON이란?
https://www.json.org/json-ko.html

 

JSON

JSON (JavaScript Object Notation)은 경량의 DATA-교환 형식이다. 이 형식은 사람이 읽고 쓰기에 용이하며, 기계가 분석하고 생성함에도 용이하다. JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1

www.json.org

 

UnityEngine에 포함된 JsonUtility 클래스를 이용하면 간단히 한 줄씩으로로 표현할 수 있습니다.
 
dataTemp = 게임에서 사용 가능한 데이터 형식
jsonData = JSON 형식으로 변환한 데이터
Container  = dataTemp의 데이터 타입(클래스 이름)
 
1) 사용자 정의 데이터 타입 > JSON
string jsonData = JsonUtility.ToJson(dataTemp);
 
2) JSON > 사용자 정의 데이터 타입
Container dataTemp = JsonUtility.FromJson<Container>(jsonData);
 
 

3. 암호화 / 복호화

암호화 알고리즘에는 여러 방식이 있습니다.
여기서는 고급 암호화 표준(Advanced Encryption Standard, AES) 방식을 사용하였습니다.

 

1) 암호화 알고리즘들의 종류 및 특징, 용어 설명

AES에 관련된 페이지이나, 해당 참고자료 하단의 링크 텍스트를 통해 다른 암호화 알고리즘 및 용어에 대한 문서로도 이동이 가능합니다.
https://ko.wikipedia.org/wiki/%EA%B3%A0%EA%B8%89_%EC%95%94%ED%98%B8%ED%99%94_%ED%91%9C%EC%A4%80

 

고급 암호화 표준 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 고급 암호화 표준Advanced Encryption Standard(Rijndael) SubBytes 스텝: AES 라운드 4단계 중 하나설계자Vincent Rijmen, Joan Daemen최초 출판일1998기원스퀘어(Square)차기 방식Anubis,

ko.wikipedia.org

 

2) AES 클래스 및 기초적인 구조
해당 매뉴얼의 예제를 참고하여 아래 예시 코드를 작성하였습니다.

https://learn.microsoft.com/ko-kr/dotnet/api/system.security.cryptography.aes?view=net-8.0

 

Aes 클래스 (System.Security.Cryptography)

모든 AES(Advanced Encryption Standard) 구현에서 상속해야 하는 추상 기본 클래스를 나타냅니다.

learn.microsoft.com

 

제가 작성한 아래 예시 코드에서는 공부하면서

기본적인 사용 및 이해를 위한 주석을 달아두었지만,

 

해당 분야의 지식이 짧아 다소 난해하였기에

제 해석에 틀린 부분이 있다면 지적해 주시면 감사하겠습니다.
 
 

4. 파일 입/출력

System.IO 네임스페이스가 필요합니다.
 
savePath = 세이브 파일 경로를 나타내는 string 문자열
encryptedData = 암호화된 JSON (string 타입) 데이터
 
1) 파일 쓰기
File.WriteAllText(savePath, encryptedData);

 

2) 파일 읽기
string encryptedData = File.ReadAllText(savePath);
 
 

< 예시 코드 >

위에서 얘기한 내용을 모아서 하나의 예시 코드를 작성해 보았습니다.
 
유니티에서 코드를 그대로 사용할 경우 아래 코드의
데이터 클래스 (PlayerData, Container),
저장 경로 (savePath),
키 (encryption_Key)
만 조정하여 간단하게 암호화할 수 있습니다.
 
C#에서 사용하려면,

1) 유니티에서 사용하는 네임스페이스 지우기
using UnityEngine;
 
2) 해당 네임스페이스와 관련된 빨간줄이 뜨는 부분 지우기
 : MonoBehaviour 상속받는 클래스 지우기
Awake(){} 안의 내용은 복사해서 빼두고 메서드 지우기
 
3) UnityEngine.JsonUtility 대신에 C# 에서 JSON을 사용하는 방법을 찾아 교체
 
4) 복사해 두었던 Awake() 메서드 내부의 내용을 Main() 메서드에 가져다 쓰면 됩니다.

다만 파일 경로는 변경해야 할 것입니다.

 

그 외 게임에서 사용하는 코드라 프로그램 목적에 맞게 바꿔야 하는 부분도 있을 것입니다.

using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using UnityEngine;


public class DataSaveLoad : MonoBehaviour
{
    // 플레이어 데이터를 담아둘 글로벌 변수
    public PlayerData playerData;

    // 세이브 파일 저장 위치
    private string savePath;

    private void Awake()
    {
        // Application.persistentDataPath 때문에 여기서 초기화
        savePath = Application.persistentDataPath + "/savefile.txt";

        // 시작할 때 저장한 데이터 불러오기
        LoadData(playerData);
    }

    // 게임을 꺼도 저장해야 할 데이터들 (예시)
    [Serializable]
    public class PlayerData
    {
    	// private로 선언해도 public에 비해 외부 접근이 까다로울 뿐 변조, 해킹이 가능
        // 추가적인 보안 방법들이 있기는 하나 이를 다 적용한다 한들 100% 안전하지 않고 비용을 많이 먹어서 여기서는 다루지 않음
        // 정말 제대로 된 암호화를 원한다면 key, 암호화된 데이터를 외부 서버에 저장 및 암호화된 데이터로만 주고받는 방법을 권장
        // 현실로 비유하자면 기기, 스크립트 내에 보관하는 것은 창고의 문을 잠궜어도 열쇠를 문 앞 화분 밑에 숨겨놓는 것과 같다고 보면 된다
        string name;
        int gold;
        int crystal;
        // 데이터 종류에는 배열, 리스트,
        // 유니티 내부 변수 타입(Vector3, Quaternion, Rect, Color, Sprite 등)도 사용 가능
                
        // 데이터 종류를 추가/제거하려면,
        // 1. 변수 추가/제거
        // 2. 바로 아래 PlayerData(){}의 매개변수, (클래스 내 변수)=(매개변수) 구문 추가/제거
        // 3. 아래의 해당 데이터 관련한 get, set 프로퍼티 사용 구문 추가/제거
        // 4. Container 클래스 내 1:1 매칭되는 동일한 타입의 public 변수 추가/제거
        // 5. Container 클래스 내 Income, Export 메서드 내에 프로퍼티 사용 구문 추가/제거

        // 데이터 형식
        public PlayerData(string name, int gold, int crystal)
        {
            this.name = name;
            this.gold = gold;
            this.crystal = crystal;
        }

        // 데이터 불러오기(Get), 데이터 쓰기(Set)
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public int Gold
        {
            get { return gold; }
            set { gold = value; }
        }
        public int Crystal
        {
            get { return crystal; }
            set { crystal = value; }
        }
    }

    // PlayerData 클래스와 동일한 타입의 변수들을 갖되 이를 public으로 선언
    // 이유1. 클래스 내 변수를 JsonUtility.ToJson()을 통해 JSON으로 변환하기 위해서는 변수의 접근제한자가 public이어야 함
    // 이유2. 암호화해서 불러오고 저장하는데 그 변수들에 접근하기 쉬워서는 의미가 희석되기에
    //        임시로 생성한 Container를 통해서 저장/불러오기 후 그 값을
    //        변수들의 접근제한자가 private인 PlayerData로 넘겨줘서 게임에서 쓰기
    [Serializable]
    public class Container
    {
        public string name;
        public int gold;
        public int crystal;

        public void Income(PlayerData playerData)
        {
            playerData.Name = name;
            playerData.Gold = gold;
            playerData.Crystal = crystal;
        }

        public void Export(PlayerData playerData)
        {
            name = playerData.Name;
            gold = playerData.Gold;
            crystal = playerData.Crystal;
        }
    }

    // 데이터를 저장
    public void SaveData(PlayerData data)
    {
        // 새로운 임시 데이터 컨테이너 생성
        Container dataTemp = new Container();
        // playerData에서 저장에 필요한 데이터들을 옮기기
        dataTemp.Export(data);

        // 1. 데이터를 JSON 형식(string)으로 변환
        string jsonData = JsonUtility.ToJson(dataTemp);

        // 2. JSON 데이터 > 암호화된 JSON 데이터
        // 암호화/복호화 키 (LoadData() 내의 Key와 동일해야 함!!!)
    	string encryption_Key = "여기에키입력해주기";
        string encryptedData = Encrypt(jsonData, encryption_Key);

        // 3. 파일로 저장
        File.WriteAllText(savePath, encryptedData);
    }

    // 데이터를 불러오기
    public void LoadData(PlayerData data)
    {
        // 테스트용 데이터 저장 파일 삭제
        //File.Delete(savePath);

        // 세이브 파일이 없다면?
        if (!File.Exists(savePath))
        {
            // 초기화 데이터로 세이브 파일 생성
            PlayerData initData = new PlayerData("기본이름", 0, 0);
            SaveData(initData);
            //Debug.Log("데이터 생성");
        }

        // 1. 파일 읽기
        string encryptedData = File.ReadAllText(savePath);

        // 2. 암호화된 데이터를 복호화 > JSON 형식의 데이터
        // 암호화/복호화 키 (SaveData() 내의 Key와 동일해야 함!!!)
    	string encryption_Key = "여기에키입력해주기";
        string jsonData = Decrypt(encryptedData, encryption_Key);

        // 3. JSON 형식의 데이터 > 정의한 데이터 타입으로 변환
        Container dataTemp = JsonUtility.FromJson<Container>(jsonData);

        // 임시 컨테이너에 담긴 데이터를 게임 데이터로 옮겨줘서 로드 완료
        dataTemp.Income(data);
    }

    // 데이터 암호화
    private string Encrypt(string plainText, string key)
    {
        using (Aes aes = Aes.Create())
        {
            // string > byte[] 변환
            aes.Key = GetFixedLengthKey(key, 16);
            // 랜덤 IV 생성
            // 초기화 벡터를 동일한 값으로 계속 쓰는 것은 보안상 좋지 않음
            // 1번의 암호화 후 복호화까지만 해당 벡터 사용
            aes.GenerateIV();

            // 사용자가 정의한 Key, IV 값을 통해, 스트림 변환을 수행할 암호화기 생성
            ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

            // 암호화에 사용할 스트림 생성

            // 메모리에 백업 저장하는 스트림 생성
            using (var ms = new MemoryStream())
            {
                // IV를 예시로 데이터와 함께 저장 (복호화에 필요)
                // 예시를 들어 데이터 이전에 오도록 했으나
                // 저장 위치는 임의 지정 가능
                ms.Write(aes.IV, 0, aes.IV.Length);

                // 생성한 메모리 스트림에 암호화 스트림 생성
                using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                {
                    using (var sw = new StreamWriter(cs))
                    {
                        sw.Write(plainText);
                    }
                }

                // 암호화가 끝난 데이터를 byte[] 타입 > string으로 변환하여 리턴 
                return Convert.ToBase64String(ms.ToArray());
            }
        }
    }

    // 데이터 복호화
    private string Decrypt(string cipherText, string key)
    {
        using (Aes aes = Aes.Create())
        {
            // 주의: 복호화 때의 키, 초기화 벡터는 암호화 때와 동일해야 함
            aes.Key = GetFixedLengthKey(key, 16);

            // string > byte[] 변환
            byte[] fullCipher = Convert.FromBase64String(cipherText);

            // 암호에 포함된 IV 추출
            // 예시로 암호 처음 16자리에 붙여둠
            // 초기화 벡터에 해당하는 부분을 변수 iv에 복사
            byte[] iv = new byte[16];
            Array.Copy(fullCipher, 0, iv, 0, iv.Length);
            // 초기화 벡터 값 넣어주기
            aes.IV = iv;

            // 암호화된 데이터 추출
            // 초기화 벡터 iv.Length(16)자리를 제외한 길이
            byte[] cipherBytes = new byte[fullCipher.Length - iv.Length];
            // 암호문에 해당하는 부분을 cipherBytes에 복사
            Array.Copy(fullCipher, iv.Length, cipherBytes, 0, cipherBytes.Length);

            // Key, IV 값을 통해, 스트림 변환을 수행할 복호화기 생성
            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

            // 메모리 스트림 생성 및 암호화된 byte[] 타입 데이터를 넣어줌
            using (var ms = new MemoryStream(cipherBytes))
            {
                // 생성한 메모리 스트림에서 복호화
                using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    // 복호화가 끝난 메모리 스트림에서 문자를 읽어오기 위한 텍스트 리더
                    using (var sr = new StreamReader(cs))
                    {
                        // 메모리 스트림의 끝까지 읽어서 리턴
                        return sr.ReadToEnd();
                    }
                }
            }
        }
    }

    // AES 암호화 키 길이 고정
    // 키 길이(length)를 16, 24 ,32바이트 중에 맞춰주기
    private byte[] GetFixedLengthKey(string key, int length)
    {
        // 암호화 키의 타입 변환(string > byte[])
        byte[] keyBytes = Encoding.UTF8.GetBytes(key);
        // 길이가 고정된 byte[] 형태의 키
        byte[] fixedKey = new byte[length];

        // fixedKey의 length 자리까지 byte[] 타입으로 전환한 key를 한자리씩 넣어주기
        // keyBytes가 fixedKey보다 짧으면 남은 자리는 0으로 채워줌
        for (int i = 0; i < length; i++)
        {
            fixedKey[i] = (i < keyBytes.Length) ? keyBytes[i] : (byte)0;
        }
        return fixedKey;
    }
}

 

< 결과 >

세이브 파일 암호화 체크
게임 플레이 도중 골드 획득
다음 실행 때 세이브 파일로부터 불러와서 획득한 재화가 유지됨을 확인

 

저작자표시 비영리 동일조건 (새창열림)

'기능 구현 방법 정리' 카테고리의 다른 글

유니티 UI 이미지 마스크  (0) 2025.01.27
유니티 UI에 스파인 애니메이션 넣기  (0) 2025.01.26
유니티 2D) 부모 오브젝트 주위를 자식 오브젝트들이 원형으로 회전하게 하고 싶다면? (RotateAround 사용 버전도 포함)  (0) 2024.12.12
유니티) 레이어마스크(LayerMask) 사용법, 2D 가장 가까운 적 탐색  (0) 2024.12.02
유니티) 머티리얼(Material) 색상 곱하기  (0) 2024.11.25
'기능 구현 방법 정리' 카테고리의 다른 글
  • 유니티 UI 이미지 마스크
  • 유니티 UI에 스파인 애니메이션 넣기
  • 유니티 2D) 부모 오브젝트 주위를 자식 오브젝트들이 원형으로 회전하게 하고 싶다면? (RotateAround 사용 버전도 포함)
  • 유니티) 레이어마스크(LayerMask) 사용법, 2D 가장 가까운 적 탐색
ybbro
ybbro
대부분의 포스팅은 pc에서 작성되었습니다. 모바일에서 볼 때 설명이 잘리면 데스크탑 모드를 사용해보길 바랍니다.
  • ybbro
    어떻게든 굴리는 게임 공방
    ybbro
  • 전체
    오늘
    어제
    • 전체 N
      • 스파르타코딩클럽_Unity개발과정 N
      • Unity 2D
        • 카드게임
        • 플랫포머 게임
        • 뱀서라이크
      • Unity 3D
        • 닷지
        • 유니티 짱
        • 디펜스 게임
      • Unity 에러 노트
      • 기능 구현 방법 정리
      • 셰이더 그래프
        • 2D
        • 3D
      • 프로그래머스
      • 자료구조
      • 기타
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    앱이 휴대전화와 호환되지 않아 설치되지 않았습니다
    다크모드
    세이브
    직렬화
    갤럭시 S24
    잔상
    UI
    스파인
    hello
    unity
    유니티 애니메이터 파라미터 초기화
    룰렛
    64비트
    텍스트매시프로
    sprite mask
    무료스킨
    대시
    마스크
    유니티
    삭제
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ybbro
유니티, C#) AES 암호화/복호화 + JSON 데이터 저장/불러오기
상단으로

티스토리툴바