포스팅 목적
- 기록하기 위해
- 피드백을 받기 위해. 버그 있을 때, 수정 할 수 있습니다. 링크도 가져 가세요.
- 공유 - 불펌 개념이 없으니, 마음데로 사용하세요.
개발 이유
게임 정보를 서버에 다 저장해서 쓰려고 했는데, 회의 중 인터넷이 끊어져도 게임하는데 지장이 없어야 한다는 결론이 내려졌습니다. 그래서 로컬에 데이터를 저장해야 하는데, 유니티가 기본 제공하는 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 를 사용합니다.
저장 할 때
- 저장 할 Key 의 해쉬값을 얻고, 그 해쉬를 hideKey 로 사용합니다. 왜냐하면, 해커가 Key 를 보고, 용도를 파악 할 수 있기 때문입니다. 추가로 salt 가 있는 이유는 공개 해쉬 알고리즘을 사용하기 때문에, 무작위 대입으로 키를 손쉽게 알 수 있기 때문입니다.
예) hideKey = Hash(Key + salt) - 저장해야 할 Value 의 해쉬값을 얻고, Value + Hash(Value) 로 만든 뒤 암호화 합니다.
Value 의 Hash 를 얻는 이유는 복호화 후 Value 가 변조되었는지 확인이 필요하기 때문이다.
예) encryptValue = Encrypt(Value + Hash(Value))
3. 이렇게 얻어진 hideKey - encryptValue 를 PlayerPrefs 으로 저장합니다.
불러 올 때
- hideKey 를 똑같이 만듭니다.
- hideKey 로 암호화된 값을 얻고, 복호화 후 원본 saved Value + saved Hash(Value) 를 구합니다.
- 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!