From 8f91bb22945f105f2815a6004901dc93080a41ca Mon Sep 17 00:00:00 2001 From: umanicof Date: Tue, 3 Dec 2019 07:15:51 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=83=8F=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E5=80=A4=E3=81=AB=E3=82=88=E3=82=8B=E6=A4=9C=E8=A8=BC=E3=81=AE?= =?UTF-8?q?=E4=BB=95=E7=B5=84=E3=81=BF=E3=82=92=E7=94=A8=E6=84=8F=E3=81=97?= =?UTF-8?q?=E3=80=81=E3=83=8F=E3=82=A4=E3=82=B9=E3=82=B3=E3=82=A2=E3=81=AE?= =?UTF-8?q?=E3=83=A1=E3=83=A2=E3=83=AA=E6=94=B9=E3=81=96=E3=82=93=E3=81=A8?= =?UTF-8?q?=E9=80=9A=E4=BF=A1=E3=83=91=E3=82=B1=E3=83=83=E3=83=88=E6=94=B9?= =?UTF-8?q?=E3=81=96=E3=82=93=E3=81=AE=E5=AF=BE=E7=AD=96=E3=82=92=E8=A1=8C?= =?UTF-8?q?=E3=81=84=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82=20=E6=A4=9C?= =?UTF-8?q?=E8=A8=BC=E3=82=92=E6=9C=89=E5=8A=B9=E3=81=AB=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=AB=E3=81=AF=E3=80=81RankingLoader.cs=20=E3=81=AE=20Enabl?= =?UTF-8?q?edVerifyHash=20=E3=82=92=20true=20=E3=81=AB=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=81=8F=E3=81=A0=E3=81=95=E3=81=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sample/SampleSceneManager.cs | 6 ++ .../Scripts/RankingLoader.cs | 6 ++ .../Scripts/RankingRecord.cs | 71 +++++++++++++++++++ .../Scripts/RankingRecord.cs.meta | 11 +++ .../Scripts/RankingSceneManager.cs | 70 ++++++++++++++---- .../Scripts/StringExtensions.cs | 47 ++++++++++++ .../Scripts/StringExtensions.cs.meta | 11 +++ 7 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs create mode 100644 Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs.meta create mode 100644 Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs create mode 100644 Assets/naichilab/unity-simple-ranking/Scripts/StringExtensions.cs.meta 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..4c1e5e9 --- /dev/null +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs @@ -0,0 +1,71 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + + +namespace naichilab +{ + /// + /// ランキングのレコード + /// ・主にハッシュ値を作成する目的で用意しました + /// + public class RankingRecord + { + public string ObjectID { get; private set; } + public string Name { get; private set; } + public IScore Score { get; private set; } + public string Hash { get; private set; } + + /// + /// コンストラクタ + /// ・DBから復元する場合はhashの引数があるものを使用します + /// + /// + /// + /// + /// + public RankingRecord(string objectId, string name, IScore score) + { + ObjectID = objectId; + Name = name; + Score = score; + Hash = CalcHash(); + } + public RankingRecord(string objectId, string name, IScore score, string hash) + { + ObjectID = objectId; + Name = name; + Score = score; + Hash = hash; + } + + /// + /// ハッシュ計算 + /// ・計算が推測できないよう、通信パケットに含まれていないクライアントキーを混ぜ合わせています + /// + /// + string CalcHash() + { + return (ObjectID + Name + Score.TextForSave + NCMB.NCMBSettings.ClientKey).ToHMACSHA256(); + } + + /// + /// ハッシュ値の検証 + /// + /// true:OK、false:NG + public bool VerifyHash() + { + return CalcHash() == Hash; + } + + /// + /// 名前変更 + /// ・ハッシュ値が再計算されることに注意してください + /// + public void ChangeName(string name) + { + Name = name; + 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..cc458ec 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; @@ -45,10 +56,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 +86,7 @@ void Start() { sendScoreButton.interactable = false; _board = RankingLoader.Instance.CurrentRanking; - _lastScore = RankingLoader.Instance.LastScore; + _lastScoreRecord = new RankingRecord(ObjectID, "default", RankingLoader.Instance.LastScore); // 後で書き換えるので名前は適当に登録しています Debug.Log(BoardIdPlayerPrefsKey + "=" + PlayerPrefs.GetString(BoardIdPlayerPrefsKey, null)); @@ -79,10 +95,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 +113,17 @@ 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.ObjectId, + _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 +137,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 +167,11 @@ public void SendScore() private IEnumerator SendScoreEnumerator() { + if (EnabledVerifyHash && !_lastScoreRecord.VerifyHash()) { // メモリが書き換えられてないかのチェック + highScoreLabel.text = "検証エラー"; + yield break; + } + sendScoreButton.interactable = false; highScoreLabel.text = "送信中..."; @@ -154,7 +183,9 @@ private IEnumerator SendScoreEnumerator() } _ncmbRecord[COLUMN_NAME] = InputtedNameForSave; - _ncmbRecord[COLUMN_SCORE] = _lastScore.Value; + _ncmbRecord[COLUMN_SCORE] = _lastScoreRecord.Score.Value; + _lastScoreRecord.ChangeName(InputtedNameForSave); // 名前変更 + _ncmbRecord[COLUMN_HASH] = _lastScoreRecord.Hash; NCMBException errorResult = null; yield return _ncmbRecord.YieldableSaveAsync(error => errorResult = error); @@ -169,7 +200,7 @@ private IEnumerator SendScoreEnumerator() //ObjectIDを保存して次に備える ObjectID = _ncmbRecord.ObjectId; - highScoreLabel.text = _lastScore.TextForDisplay; + highScoreLabel.text = _lastScoreRecord.Score.TextForDisplay; yield return StartCoroutine(LoadRankingBoard()); } @@ -193,7 +224,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 +248,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.ObjectId, + 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: From 5bbb387dbcf61db10a4a90014475ebabb79e2ee3 Mon Sep 17 00:00:00 2001 From: umanicof Date: Tue, 3 Dec 2019 09:14:20 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ObjectID=E3=81=8C=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E3=81=A7=E4=BB=98=E4=B8=8E=E3=81=95=E3=82=8C=E3=82=8B=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=81=AB=E3=83=8F=E3=83=83=E3=82=B7=E3=83=A5=E5=80=A4?= =?UTF-8?q?=E3=81=AB=E3=81=9A=E3=82=8C=E3=81=8C=E5=87=BA=E3=81=A6=E4=B8=8D?= =?UTF-8?q?=E5=85=B7=E5=90=88=E3=81=AB=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=81=9F=E3=80=82ObjectID=E3=81=AF=E3=83=8F=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E5=80=A4=E3=81=AB=E5=90=AB=E3=82=81=E3=81=AA=E3=81=84?= =?UTF-8?q?=E4=BB=95=E6=A7=98=E3=81=AB=E5=A4=89=E6=9B=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/RankingRecord.cs | 18 ++++++------------ .../Scripts/RankingSceneManager.cs | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs index 4c1e5e9..e9acc91 100644 --- a/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingRecord.cs @@ -11,8 +11,7 @@ namespace naichilab /// public class RankingRecord { - public string ObjectID { get; private set; } - public string Name { get; private set; } + public string Name { get; set; } public IScore Score { get; private set; } public string Hash { get; private set; } @@ -20,20 +19,17 @@ public class RankingRecord /// コンストラクタ /// ・DBから復元する場合はhashの引数があるものを使用します /// - /// /// /// /// - public RankingRecord(string objectId, string name, IScore score) + public RankingRecord(string name, IScore score) { - ObjectID = objectId; Name = name; Score = score; Hash = CalcHash(); } - public RankingRecord(string objectId, string name, IScore score, string hash) + public RankingRecord(string name, IScore score, string hash) { - ObjectID = objectId; Name = name; Score = score; Hash = hash; @@ -46,7 +42,7 @@ public RankingRecord(string objectId, string name, IScore score, string hash) /// string CalcHash() { - return (ObjectID + Name + Score.TextForSave + NCMB.NCMBSettings.ClientKey).ToHMACSHA256(); + return (Name + Score.TextForSave + NCMB.NCMBSettings.ClientKey).ToHMACSHA256(); } /// @@ -59,12 +55,10 @@ public bool VerifyHash() } /// - /// 名前変更 - /// ・ハッシュ値が再計算されることに注意してください + /// ハッシュ再計算 /// - public void ChangeName(string name) + public void RefreshHash() { - Name = name; Hash = CalcHash(); } } diff --git a/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs b/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs index cc458ec..a773faa 100644 --- a/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs +++ b/Assets/naichilab/unity-simple-ranking/Scripts/RankingSceneManager.cs @@ -49,6 +49,11 @@ private string ObjectID PlayerPrefs.SetString(BoardIdPlayerPrefsKey, _objectid = value); } } + private void CrearObjectID() + { + PlayerPrefs.SetString(BoardIdPlayerPrefsKey, null); + } + private string BoardIdPlayerPrefsKey { @@ -86,7 +91,7 @@ void Start() { sendScoreButton.interactable = false; _board = RankingLoader.Instance.CurrentRanking; - _lastScoreRecord = new RankingRecord(ObjectID, "default", RankingLoader.Instance.LastScore); // 後で書き換えるので名前は適当に登録しています + _lastScoreRecord = new RankingRecord("default", RankingLoader.Instance.LastScore); // 後で書き換えるので名前は適当に登録しています Debug.Log(BoardIdPlayerPrefsKey + "=" + PlayerPrefs.GetString(BoardIdPlayerPrefsKey, null)); @@ -114,8 +119,7 @@ IEnumerator GetHighScoreAndRankingBoard() var s = _board.BuildScore(_ncmbRecord[COLUMN_SCORE].ToString()); if (s != null) { - hiScoreRecord = new RankingRecord(_ncmbRecord.ObjectId, - _ncmbRecord[COLUMN_NAME].ToString(), + hiScoreRecord = new RankingRecord(_ncmbRecord[COLUMN_NAME].ToString(), s, _ncmbRecord.ContainsKey(COLUMN_HASH) ? _ncmbRecord[COLUMN_HASH].ToString() : ""); if (EnabledVerifyHash && !hiScoreRecord.VerifyHash()) { @@ -184,7 +188,8 @@ private IEnumerator SendScoreEnumerator() _ncmbRecord[COLUMN_NAME] = InputtedNameForSave; _ncmbRecord[COLUMN_SCORE] = _lastScoreRecord.Score.Value; - _lastScoreRecord.ChangeName(InputtedNameForSave); // 名前変更 + _lastScoreRecord.Name = InputtedNameForSave; // 名前変更 + _lastScoreRecord.RefreshHash(); // ハッシュ再計算 _ncmbRecord[COLUMN_HASH] = _lastScoreRecord.Hash; NCMBException errorResult = null; @@ -194,6 +199,7 @@ private IEnumerator SendScoreEnumerator() { //NCMBのコンソールから直接削除した場合に、該当のobjectIdが無いので発生する(らしい) _ncmbRecord.ObjectId = null; + CrearObjectID(); yield return _ncmbRecord.YieldableSaveAsync(error => errorResult = error); //新規として送信 } @@ -253,8 +259,8 @@ private IEnumerator LoadRankingBoard() IScore highScore = _board.BuildScore(r[COLUMN_SCORE].ToString()); if (highScore == null) continue; - RankingRecord highScoreRecord = new RankingRecord(r.ObjectId, - r[COLUMN_NAME].ToString(), + // ハッシュ検証 + RankingRecord highScoreRecord = new RankingRecord(r[COLUMN_NAME].ToString(), highScore, r.ContainsKey(COLUMN_HASH) ? r[COLUMN_HASH].ToString() : ""); if (EnabledVerifyHash && !highScoreRecord.VerifyHash()) continue;