UserPref 암호화

|

출처 : http://ikpil.com/1342



포스팅 목적

  • 기록하기 위해
  • 피드백을 받기 위해. 버그 있을 때, 수정 할 수 있습니다. 링크도 가져 가세요.
  • 공유 - 불펌 개념이 없으니, 마음데로 사용하세요.

개발 이유

게임 정보를 서버에 다 저장해서 쓰려고 했는데, 회의 중 인터넷이 끊어져도 게임하는데 지장이 없어야 한다는 결론이 내려졌습니다. 그래서 로컬에 데이터를 저장해야 하는데, 유니티가 기본 제공하는 PlayerPrefs 은 손 쉽게 변조가 가능하기 때문에, 변조를 못하게 막아야 할 필요가 있었습니다.

그래서 유니티 보안 문서들을 보았고, 그 중에 PlayerPrefs 먼저 할 필요가 있어서, 기존에 다른 사람이 만든 코드를 찾아 보았습니다. 하지만 키를 숨기는 개념이나 value 암호화 개념을 사용하는건 찾지 못했습니다. 그래서 직접 개발(짜집기)을 하게 되었습니다.

다음에는 모듈(DLL) 암호화를 할 필요가 있습니다.

링크 1 : http://unitystudy.net/bbs/board.php?bo_table=newwriting&wr_id=355
링크 2 : ?http://www.slideshare.net/williamyang3910/unitekorea2013-protecting-your-android-content-21713675?

설명

Unity PlayerPrefs 특성상 key-value 형태로 로컬에 데이터를 저장하는데, 암호화가 되어 있지 않습니다. 그래서 다음과 같은 절차를 거쳐 PlayerPrefs 를 사용합니다.

저장 할 때

  1. 저장 할 Key 의 해쉬값을 얻고, 그 해쉬를 hideKey 로 사용합니다. 왜냐하면, 해커가 Key 를 보고, 용도를 파악 할 수 있기 때문입니다. 추가로 salt 가 있는 이유는 공개 해쉬 알고리즘을 사용하기 때문에, 무작위 대입으로 키를 손쉽게 알 수 있기 때문입니다.
    예) hideKey = Hash(Key + salt)

  2. 저장해야 할 Value 의 해쉬값을 얻고, Value + Hash(Value) 로 만든 뒤 암호화 합니다.
    Value 의 Hash 를 얻는 이유는 복호화 후 Value 가 변조되었는지 확인이 필요하기 때문이다.
    예) encryptValue = Encrypt(Value + Hash(Value)) 

3. 이렇게 얻어진 hideKey - encryptValue 를 PlayerPrefs 으로 저장합니다.

불러 올 때

  1. hideKey 를 똑같이 만듭니다.
  2. hideKey 로 암호화된 값을 얻고, 복호화 후 원본 saved Value + saved Hash(Value) 를 구합니다.
  3. saved Value, saved Hash(Value) 로 분리 한 뒤, 서로 비교하여 변했는지 검사합니다. 
    예) Hash(saved Value) == saved Hash(Value)

코드 : SecurityPlayerPrefs , 갱신 : 2015.03.03 23:16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
using UnityEngine;
using System.Security.Cryptography;
using System.Text;
using System.IO;
  
public class SecurityPlayerPrefs
{
 
    private static string _saltForKey;
 
    private static byte[] _keys;
    private static byte[] _iv;
    private static int keySize = 256;
    private static int blockSize = 128;
    private static int _hashLen = 32;
 
    static SecurityPlayerPrefs()
    {
        // 8 바이트로 하고, 변경해서 쓸것
        byte[] saltBytes = new byte[] { 25, 36, 77, 51, 43, 14, 75, 93 };
 
        // 길이 상관 없고, 키를 만들기 위한 용도로 씀
        string randomSeedForKey = "5b6fcb4aaa0a42acae649eba45a506ec";
 
        // 길이 상관 없고, aes에 쓸 key 와 iv 를 만들 용도
        string randomSeedForValue = "2e327725789841b5bb5c706d6b2ad897";
 
        {
            Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(randomSeedForKey, saltBytes, 1000);
            _saltForKey = System.Convert.ToBase64String(key.GetBytes(blockSize / 8));
        }
 
        {
            Rfc2898DeriveBytes key = new Rfc2898DeriveBytes(randomSeedForValue, saltBytes, 1000);
            _keys = key.GetBytes(keySize / 8);
            _iv = key.GetBytes(blockSize / 8);
        }
    }
 
    public static string MakeHash(string original)
    {
        using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
        {
            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(original);
            byte[] hashBytes = md5.ComputeHash(bytes);
 
            string hashToString = "";
            for (int i = 0; i < hashBytes.Length; ++i)
                hashToString += hashBytes[i].ToString("x2");
 
            return hashToString;
        }
    }
 
    public static byte[] Encrypt(byte[] bytesToBeEncrypted)
    {
        using (RijndaelManaged aes = new RijndaelManaged())
        {
            aes.KeySize = keySize;
            aes.BlockSize = blockSize;
 
            aes.Key = _keys;
            aes.IV = _iv;
 
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
 
            using (ICryptoTransform ct = aes.CreateEncryptor())
            {
                return ct.TransformFinalBlock(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length);
            }
        }
    }
 
    public static byte[] Decrypt(byte[] bytesToBeDecrypted)
    {
        using (RijndaelManaged aes = new RijndaelManaged())
        {
            aes.KeySize = keySize;
            aes.BlockSize = blockSize;
 
            aes.Key = _keys;
            aes.IV = _iv;
 
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
 
            using (ICryptoTransform ct = aes.CreateDecryptor())
            {
                return ct.TransformFinalBlock(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length);
            }
        }
    }
 
    public static string Encrypt(string input)
    {
        byte[] bytesToBeEncrypted = Encoding.UTF8.GetBytes(input);
        byte[] bytesEncrypted = Encrypt(bytesToBeEncrypted);
 
        return System.Convert.ToBase64String(bytesEncrypted);
    }
 
    public static string Decrypt(string input)
    {
        byte[] bytesToBeDecrypted = System.Convert.FromBase64String(input);
        byte[] bytesDecrypted = Decrypt(bytesToBeDecrypted);
 
        return Encoding.UTF8.GetString(bytesDecrypted);
    }
 
    private static void SetSecurityValue(string key, string value)
    {
        string hideKey = MakeHash(key + _saltForKey);
        string encryptValue = Encrypt(value + MakeHash(value));
 
        PlayerPrefs.SetString(hideKey, encryptValue);
    }
 
    private static string GetSecurityValue(string key)
    {
        string hideKey = MakeHash(key + _saltForKey);
 
        string encryptValue = PlayerPrefs.GetString(hideKey);
        if (true == string.IsNullOrEmpty(encryptValue))
            return string.Empty;
 
        string valueAndHash = Decrypt(encryptValue);
        if (_hashLen > valueAndHash.Length)
            return string.Empty;
 
        string savedValue = valueAndHash.Substring(0, valueAndHash.Length - _hashLen);
        string savedHash = valueAndHash.Substring(valueAndHash.Length - _hashLen);
 
        if (MakeHash(savedValue) != savedHash)
            return string.Empty;
 
        return savedValue;
    }
 
    public static void DeleteKey(string key)
    {
        PlayerPrefs.DeleteKey(MakeHash(key + _saltForKey));
    }
 
    public static void DeleteAll()
    {
        PlayerPrefs.DeleteAll();
    }
 
    public static void Save()
    {
        PlayerPrefs.Save();
    }
 
    public static void SetInt(string key, int value)
    {
        SetSecurityValue(key, value.ToString());
    }
 
    public static void SetLong(string key, long value)
    {
        SetSecurityValue(key, value.ToString());
    }
 
    public static void SetFloat(string key, float value)
    {
        SetSecurityValue(key, value.ToString());
    }
 
    public static void SetString(string key, string value)
    {
        SetSecurityValue(key, value);
    }
 
    public static int GetInt(string key, int defaultValue)
    {
        string originalValue = GetSecurityValue(key);
        if (true == string.IsNullOrEmpty(originalValue))
            return defaultValue;
 
        int result = defaultValue;
        if (false == int.TryParse(originalValue, out result))
            return defaultValue;
 
        return result;
    }
 
    public static long GetLong(string key, long defaultValue)
    {
        string originalValue = GetSecurityValue(key);
        if (true == string.IsNullOrEmpty(originalValue))
            return defaultValue;
 
        long result = defaultValue;
        if (false == long.TryParse(originalValue, out result))
            return defaultValue;
 
        return result;
    }
 
    public static float GetFloat(string key, float defaultValue)
    {
        string originalValue = GetSecurityValue(key);
        if (true == string.IsNullOrEmpty(originalValue))
            return defaultValue;
 
        float result = defaultValue;
        if (false == float.TryParse(originalValue, out result))
            return defaultValue;
 
        return result;
    }
 
    public static string GetString(string key, string defaultValue)
    {
        string originalValue = GetSecurityValue(key);
        if (true == string.IsNullOrEmpty(originalValue))
            return defaultValue;
 
        return originalValue;
    }
}

결과



여담

  • UnityEditor 에서 PlayerPrefs 을 수정/보기 가능하면, 개발이 좀 더 편해집니다.
    : 누군가 만들면, 알려 주세요.

  • 믿을건 그나마 서버 밖에 없습니다.

:wq!



And