自作ゲームの保存しているデータの難読化を行う

前回からプレイヤーのデータの保存する方法を見てきました

ただし、このデータは比較的簡単にデータの解析と改ざんが行えてしまいます

その対策のため、今回はデータの難読化について見ていきたいと思います

PlayerPrefsに保存されているデータについて

PlayerPrefsで保存したデータの保存場所は

公式のリファレンスの通り保存場所はしっかり明記されており、

さらにはデータの暗号化をしていないことも明記されています

特にunityroomに投稿する場合はWebGLで投稿するため、

保存場所はindexedDBとなるため「F12」キーで簡単に覗けてしまいます

例えば、RPGなどではを改ざんされれば、かわいいものの上げると

  • パラメータを異常値に設定しほぼ無敵に近いキャラを作る
  • いきなりラストダンジョンに潜入できる

などのチートもやりたい放題です

最悪の場合はゲームの進行ができなくなり、クラッシュなども十分にあり得ます。

そのためにも、改ざん対策はいれておきましょう

難読化方法

難読化実装は、一見難しく見えますがライブラリーも比較的充実しているので比較的簡単に実装することができます

その中でも今回は、AES暗号化方式を使用することにします

AES暗号化方式 は共通鍵暗号方式で

暗号キーでデータの暗号化を行い

復号キーで暗号データからデータを復号して取り出します。

実装方法

実装は1から実装せず今回はこちらからお借りしました

ありがとうございます

これを汎用クラスとして改良しマネジャークラスとして汎用化しました

public class AESManager
{
	/// <summary>
	/// 対称鍵暗号を使って文字列を暗号化する
	/// </summary>
	/// <param name="text">暗号化する文字列</param>
	/// <param name="iv">対称アルゴリズムの初期ベクター</param>
	/// <param name="key">対称アルゴリズムの共有鍵</param>
	/// <returns>暗号化された文字列</returns>
	public static string Encrypt(string text)
	{
		using(RijndaelManaged myRijndael = new RijndaelManaged())
		{
			// ブロックサイズ(何文字単位で処理するか)
			myRijndael.BlockSize = 128;
			// 暗号化方式はAES-256を採用
			myRijndael.KeySize = 256;
			// 暗号利用モード
			myRijndael.Mode = CipherMode.CBC;
			// パディング
			myRijndael.Padding = PaddingMode.PKCS7;

			myRijndael.IV = Encoding.UTF8.GetBytes(Const.AES_IV_256);
			myRijndael.Key = Encoding.UTF8.GetBytes(Const.AES_Key_256);

			// 暗号化
			ICryptoTransform encryptor =               myRijndael.CreateEncryptor(myRijndael.Key, myRijndael.IV);

			byte[] encrypted;
			using(MemoryStream mStream = new MemoryStream())
			{
				using(CryptoStream ctStream = new CryptoStream(mStream, encryptor, CryptoStreamMode.Write))
				{
					using(StreamWriter sw = new StreamWriter(ctStream))
					{
						sw.Write(text);
					}
					encrypted = mStream.ToArray();
				}
			}
			// Base64形式(64種類の英数字で表現)で返す
			return (System.Convert.ToBase64String(encrypted));
		}
	}

	/// <summary>
	/// 対称鍵暗号を使って暗号文を復号する
	/// </summary>
	/// <param name="cipher">暗号化された文字列</param>
	/// <param name="iv">対称アルゴリズムの初期ベクター</param>
	/// <param name="key">対称アルゴリズムの共有鍵</param>
	/// <returns>復号された文字列</returns>
	public static string Decrypt(string cipher)
	{
		using(RijndaelManaged rijndael = new RijndaelManaged())
		{
			// ブロックサイズ(何文字単位で処理するか)
			rijndael.BlockSize = 128;
			// 暗号化方式はAES-256を採用
			rijndael.KeySize = 256;
			// 暗号利用モード
			rijndael.Mode = CipherMode.CBC;
			// パディング
			rijndael.Padding = PaddingMode.PKCS7;

			rijndael.IV = Encoding.UTF8.GetBytes(Const.AES_IV_256);
			rijndael.Key = Encoding.UTF8.GetBytes(Const.AES_Key_256);

			ICryptoTransform decryptor = rijndael.CreateDecryptor(rijndael.Key, rijndael.IV);

			string plain = string.Empty;
			using(MemoryStream mStream = new MemoryStream(System.Convert.FromBase64String(cipher)))
			{
				using(CryptoStream ctStream = new CryptoStream(mStream, decryptor, CryptoStreamMode.Read))
				{
					using(StreamReader sr = new StreamReader(ctStream))
					{
						plain = sr.ReadLine();
					}
				}
			}
			return plain;
		}
	}
}

これで、文字列であれば汎用的に暗号化してデーターのやり取りができそうですね

ただし、この時注意する点があり暗号化及び復号化するキーは絶対に公開しないようにしましょう

暗号化するキーがばれると簡単に復号化されるので暗号化する意味がなくなります

例で言えば、自宅のカギをなくして金品を根こそぎ奪われるのに等しいので注意しましょう

実際に使う

それでは、このサンプルコード応用して前回のデータの保存に暗号化を入れてみましょう

↓データの保存

// クラスにパラメータセット
var playerData = new PlayerData()
{
    name   = "ああああ";
    level  = 1;
    now_hp = 100;
    equip  = "ヒノキの棒";
    items = new string [] { "金貨", "ポーション" };
    skills = new int[] { 0 };
}

// Json化
var saveData = JsonUtility.ToJson(playerData);

// 文字列形式で保存
// PlayerPrefs.SetString("PlayerData", saveData);
//        ↓
// こんな感じで変更します
PlayerPrefs.SetString("PlayerData", AESManager.Encrypt(saveData));

↓データ取得

// 文字列形式で読み込み
var saveData = PlayerPrefs.SetString("PlayerData", AESManager.Decrypt(saveData));

// Jsonを展開 ここで展開するクラスを記載しておきます


PlayerData playerData = null;

try
{
    playerData = JsonUtility.FromJson<PlayerData>(saveData);
}
// Jsonへの展開失敗 改ざんの可能性あり
catch(Exception e)
{
    // 例外が発生するのでここで処理
    Debug.LogException(e);
    Debug.Log("改ざんされたため 冒険の書は消えてしまいました");
    playerData  = new PlayerData();
}

この通りマネージャ化することで簡単に実装することができました

これでデータを除かれてもJson形式ではなく意味が分からない文字があるだけですので

最低限の安心はとることができます

まとめ

事実上前回の続きとなりましたが、

データの難読化はゲームを健全にプレイしたいのであればほぼ必須の技能になってきます

ここまでして暗号化してもやろうと思えばデータの解析は可能ですが、

やらないよりは遥かにましであり、可読にも時間を要するのである程度効果的であるといえます

チートをされても楽しんでくれればいいと思う方もいるかもしれませんが

ぜひとも、端末に保存するデータがある場合は難読化を検討ください。


コメントを残す

メールアドレスが公開されることはありません。

ABOUT US

vuniformity誰でもない人
トレンドの行く末を見守っている
仮名を名乗るエンジニア

ゲーム開発は仕事であり趣味である
プログラムだけでなく多種多様なスキルを数多く持つ

ゲーム開発は
ソーシャルゲームを開発運用の経験アリ
ゲーム以外にも経験アリ
Webサービス保守開発等に携わる

ゲームプレイの主な戦場は
FGO
FEH
MTGA
マビノギ
ここでは主にunityroomで公開しているゲーム作り直しの軌跡を綴っていきます