diff --git a/Assets/naichilab/unity-simple-ranking/Sample/SampleSceneManager.cs b/Assets/naichilab/unity-simple-ranking/Sample/SampleSceneManager.cs index 1f0109a..10c80bd 100644 --- a/Assets/naichilab/unity-simple-ranking/Sample/SampleSceneManager.cs +++ b/Assets/naichilab/unity-simple-ranking/Sample/SampleSceneManager.cs @@ -6,6 +6,12 @@ public class SampleSceneManager : MonoBehaviour { + // メモリを書き換えてハイスコアを改ざんするハッキングが存在します。 + // unity-simple-ranking 内で取り回しているスコアのメモリは書き換えを検知するようにしていますが、 + // このクラスの score は検知していません。 + // Webアプリであれば書き換えは難しい気がしますが、PC/iOS/Androidなどであれば、暗号化・もしくは書き換えを + // 検知する仕組みにすると安全です。 + public Text scoreText; [NonSerialized] int score = 0; diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/RankingLoader.cs b/Assets/naichilab/unity-simple-ranking/Scripts/RankingLoader.cs index 6107c3d..7ac91cd 100644 --- a/Assets/naichilab/unity-simple-ranking/Scripts/RankingLoader.cs +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingLoader.cs @@ -16,6 +16,12 @@ public class RankingLoader : MonoBehaviour /// [SerializeField] public RankingBoards RankingBoards; + /// + /// ハッシュ値による検証有効 + /// ・検証無効であってもハッシュ値の計算・登録は行われます + /// + [SerializeField] public bool EnabledVerifyHash; + /// /// 表示対象のボード /// diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs new file mode 100644 index 0000000..e9acc91 --- /dev/null +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs @@ -0,0 +1,65 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + + +namespace naichilab +{ + /// + /// ランキングのレコード + /// ・主にハッシュ値を作成する目的で用意しました + /// + public class RankingRecord + { + public string Name { get; set; } + public IScore Score { get; private set; } + public string Hash { get; private set; } + + /// + /// コンストラクタ + /// ・DBから復元する場合はhashの引数があるものを使用します + /// + /// + /// + /// + public RankingRecord(string name, IScore score) + { + Name = name; + Score = score; + Hash = CalcHash(); + } + public RankingRecord(string name, IScore score, string hash) + { + Name = name; + Score = score; + Hash = hash; + } + + /// + /// ハッシュ計算 + /// ・計算が推測できないよう、通信パケットに含まれていないクライアントキーを混ぜ合わせています + /// + /// + string CalcHash() + { + return (Name + Score.TextForSave + NCMB.NCMBSettings.ClientKey).ToHMACSHA256(); + } + + /// + /// ハッシュ値の検証 + /// + /// true:OK、false:NG + public bool VerifyHash() + { + return CalcHash() == Hash; + } + + /// + /// ハッシュ再計算 + /// + public void RefreshHash() + { + Hash = CalcHash(); + } + } +} diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs.meta b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs.meta new file mode 100644 index 0000000..3e5c78f --- /dev/null +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b593a22851ee1ad4990386b87fd82762 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs b/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs index e6a56a4..a773faa 100644 --- a/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs @@ -5,6 +5,7 @@ using NCMB; using NCMB.Extensions; + namespace naichilab { public class RankingSceneManager : MonoBehaviour @@ -12,7 +13,17 @@ public class RankingSceneManager : MonoBehaviour private const string OBJECT_ID = "objectId"; private const string COLUMN_SCORE = "score"; private const string COLUMN_NAME = "name"; + private const string COLUMN_HASH = "hash"; + + // 表示するランキングデータの行数 + private const int MAX_VIEW_ROW = 30; + // 読み込むランキングデータの行数 + // ・ハッシュ値による検証有効時のみ使用します。 + // 通信パケット改ざんが行われた場合、サーバ側ではじかない限りデータが登録されることは避けられません。 + // 表示行数分だけ読み込むと、不正なデータがはじかれた結果、表示に必要なデータが足りなくなってしまいます。 + // 仕方なく読み込み行数を増やしておくことで対応しておきます。 + private const int MAX_READ_ROW = 60; [SerializeField] Text captionLabel; [SerializeField] Text scoreLabel; @@ -38,6 +49,11 @@ private string ObjectID PlayerPrefs.SetString(BoardIdPlayerPrefsKey, _objectid = value); } } + private void CrearObjectID() + { + PlayerPrefs.SetString(BoardIdPlayerPrefsKey, null); + } + private string BoardIdPlayerPrefsKey { @@ -45,10 +61,15 @@ private string BoardIdPlayerPrefsKey } private RankingInfo _board; - private IScore _lastScore; + private RankingRecord _lastScoreRecord; // メモリ書き換えを警戒してハッシュ値を含むレコードで保持しています private NCMBObject _ncmbRecord; + private bool EnabledVerifyHash + { + get { return RankingLoader.Instance.EnabledVerifyHash; } + } + /// /// 入力した名前 /// @@ -70,7 +91,7 @@ void Start() { sendScoreButton.interactable = false; _board = RankingLoader.Instance.CurrentRanking; - _lastScore = RankingLoader.Instance.LastScore; + _lastScoreRecord = new RankingRecord("default", RankingLoader.Instance.LastScore); // 後で書き換えるので名前は適当に登録しています Debug.Log(BoardIdPlayerPrefsKey + "=" + PlayerPrefs.GetString(BoardIdPlayerPrefsKey, null)); @@ -79,10 +100,11 @@ void Start() IEnumerator GetHighScoreAndRankingBoard() { - scoreLabel.text = _lastScore.TextForDisplay; + scoreLabel.text = _lastScoreRecord.Score.TextForDisplay; captionLabel.text = string.Format("{0}ランキング", _board.BoardName); //ハイスコア取得 + RankingRecord hiScoreRecord = null; { highScoreLabel.text = "取得中..."; @@ -96,8 +118,16 @@ IEnumerator GetHighScoreAndRankingBoard() _ncmbRecord = hiScoreCheck.Result.First(); var s = _board.BuildScore(_ncmbRecord[COLUMN_SCORE].ToString()); - highScoreLabel.text = s != null ? s.TextForDisplay : "エラー"; - + if (s != null) { + hiScoreRecord = new RankingRecord(_ncmbRecord[COLUMN_NAME].ToString(), + s, + _ncmbRecord.ContainsKey(COLUMN_HASH) ? _ncmbRecord[COLUMN_HASH].ToString() : ""); + if (EnabledVerifyHash && !hiScoreRecord.VerifyHash()) { + hiScoreRecord = null; + } + } + + highScoreLabel.text = hiScoreRecord != null ? hiScoreRecord.Score.TextForDisplay : "エラー"; nameInputField.text = _ncmbRecord[COLUMN_NAME].ToString(); } else @@ -111,26 +141,24 @@ IEnumerator GetHighScoreAndRankingBoard() yield return StartCoroutine(LoadRankingBoard()); //スコア更新している場合、ボタン有効化 - if (_ncmbRecord == null) + if (hiScoreRecord == null) { sendScoreButton.interactable = true; } else { - var highScore = _board.BuildScore(_ncmbRecord[COLUMN_SCORE].ToString()); - if (_board.Order == ScoreOrder.OrderByAscending) { //数値が低い方が高スコア - sendScoreButton.interactable = _lastScore.Value < highScore.Value; + sendScoreButton.interactable = _lastScoreRecord.Score.Value < hiScoreRecord.Score.Value; } else { //数値が高い方が高スコア - sendScoreButton.interactable = highScore.Value < _lastScore.Value; + sendScoreButton.interactable = hiScoreRecord.Score.Value < _lastScoreRecord.Score.Value; } - Debug.Log(string.Format("登録済みスコア:{0} 今回スコア:{1} ハイスコア更新:{2}", highScore.Value, _lastScore.Value, + Debug.Log(string.Format("登録済みスコア:{0} 今回スコア:{1} ハイスコア更新:{2}", hiScoreRecord.Score.Value, _lastScoreRecord.Score.Value, sendScoreButton.interactable)); } } @@ -143,6 +171,11 @@ public void SendScore() private IEnumerator SendScoreEnumerator() { + if (EnabledVerifyHash && !_lastScoreRecord.VerifyHash()) { // メモリが書き換えられてないかのチェック + highScoreLabel.text = "検証エラー"; + yield break; + } + sendScoreButton.interactable = false; highScoreLabel.text = "送信中..."; @@ -154,7 +187,10 @@ private IEnumerator SendScoreEnumerator() } _ncmbRecord[COLUMN_NAME] = InputtedNameForSave; - _ncmbRecord[COLUMN_SCORE] = _lastScore.Value; + _ncmbRecord[COLUMN_SCORE] = _lastScoreRecord.Score.Value; + _lastScoreRecord.Name = InputtedNameForSave; // 名前変更 + _lastScoreRecord.RefreshHash(); // ハッシュ再計算 + _ncmbRecord[COLUMN_HASH] = _lastScoreRecord.Hash; NCMBException errorResult = null; yield return _ncmbRecord.YieldableSaveAsync(error => errorResult = error); @@ -163,13 +199,14 @@ private IEnumerator SendScoreEnumerator() { //NCMBのコンソールから直接削除した場合に、該当のobjectIdが無いので発生する(らしい) _ncmbRecord.ObjectId = null; + CrearObjectID(); yield return _ncmbRecord.YieldableSaveAsync(error => errorResult = error); //新規として送信 } //ObjectIDを保存して次に備える ObjectID = _ncmbRecord.ObjectId; - highScoreLabel.text = _lastScore.TextForDisplay; + highScoreLabel.text = _lastScoreRecord.Score.TextForDisplay; yield return StartCoroutine(LoadRankingBoard()); } @@ -193,7 +230,7 @@ private IEnumerator LoadRankingBoard() MaskOffOn(); var so = new YieldableNcmbQuery(_board.ClassName); - so.Limit = 30; + so.Limit = EnabledVerifyHash ? MAX_READ_ROW : MAX_VIEW_ROW; if (_board.Order == ScoreOrder.OrderByAscending) { so.OrderByAscending(COLUMN_SCORE); @@ -217,6 +254,17 @@ private IEnumerator LoadRankingBoard() int rank = 0; foreach (var r in so.Result) { + if (rank >= MAX_VIEW_ROW) break; + + IScore highScore = _board.BuildScore(r[COLUMN_SCORE].ToString()); + if (highScore == null) continue; + + // ハッシュ検証 + RankingRecord highScoreRecord = new RankingRecord(r[COLUMN_NAME].ToString(), + highScore, + r.ContainsKey(COLUMN_HASH) ? r[COLUMN_HASH].ToString() : ""); + if (EnabledVerifyHash && !highScoreRecord.VerifyHash()) continue; + var n = Instantiate(rankingNodePrefab, scrollViewContent); var rankNode = n.GetComponent(); rankNode.NoText.text = (++rank).ToString(); diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs b/Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs new file mode 100644 index 0000000..7ae537c --- /dev/null +++ b/Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.Security.Cryptography; +using System.Text; + +namespace naichilab +{ + // + /// string型の拡張メソッド + /// + public static class StringExtensions + { + /// + /// 空チェック + /// + /// + public static bool IsNullOrEmpty(this string self) + { + return string.IsNullOrEmpty(self); + } + + /// + /// ハッシュ化(HMACSHA256) + /// 参考:http://hensa40.cutegirl.jp/archives/4066 + /// 環境的に SHA256CryptoServiceProvider が存在しないので HMACSHA256 を使用 + /// + /// + public static string ToHMACSHA256(this string self) + { + // パスワードをUTF-8エンコードでバイト配列として取り出す + byte[] byteValues = System.Text.Encoding.UTF8.GetBytes(self); + + // HMACSHA256のハッシュ値を計算する + HMACSHA256 crypto256 = new HMACSHA256(byteValues); + byte[] hash256Value = crypto256.ComputeHash(byteValues); + + // HMACSHA256の計算結果をUTF8で文字列として取り出す + StringBuilder hashedText = new StringBuilder(); + for (int i = 0; i < hash256Value.Length; i++) { + // 16進の数値を文字列として取り出す + hashedText.AppendFormat("{0:X2}", hash256Value[i]); + } + return hashedText.ToString(); + } + } +} diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs.meta b/Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs.meta new file mode 100644 index 0000000..0f3cc5a --- /dev/null +++ b/Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e2093e4e1904f34c8f29020ed24f5ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: