はじめに
以前、天鳳の牌山の検証を行いました。
仕組みとしては、事前に公開されているハッシュ値と、対局後の牌譜から生成したハッシュ値を比較するものです。
この値は、乱数種をハッシュ化したものであり、また、乱数種から牌山を再現することができます。
ただし現状、牌譜からハッシュ値を生成する方法は、天鳳のサイトの「牌譜URL to SHA512」を使用するしかありません。
現時点で、色々とググってみましたが、牌譜からハッシュ値の生成に成功している人はいませんでした。
今回は、牌譜の乱数種からハッシュ値の生成に成功しましたのでまとめました。
他にも私のブログで、C++について解説している記事がありますのでご覧ください。
検証について
通常の牌山の検証手順は、天鳳のサイトに書いてあります。
プログラミング知識など必要ありませんので、興味がありましたら是非試してください。
ただし上記の手順で天鳳が用意している「牌譜URL to SHA512」というツールが、そもそも不正がないのかという疑惑が残ります。
「牌譜URL to SHA512」 を利用して1から3のハッシュを生成できますが、2については取得することが出来ません。
本記事では、ハッシュの元となるソースを乱数種から取得してみました。
天鳳のツールは利用せず、1(乱数種)を元に2(ソース)を取得するプログラミングを組み、検証しました。
それによりツール自体が問題ないことを証明しようと思います。(乱数種→ソース→ハッシュが正しくできること)
マニアックな内容になっておりますが参考にしてください。
ちなみにつのださんにツイートしていただきました、感激です😆
ハッシュについて
ざっくりハッシュそのものの特徴として、
天鳳の場合、以下の情報をSHA512でハッシュ化しているとのことです。
SHA512のソースは4(座席並べ替え情報) + 624*8(乱数種) = 4996bytes
https://tenhou.net/stat/rand/
SHA512は牌譜と座席から再計算可能です
これしかヒントがなく、全く分からん・・
ハッシュ値とは、以下のような値です。
3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4
上記と同じハッシュ値を生成するには、ソースが必要になります。(SHA512を行う元の文字列)
ハッシュ値からは、ソースを取得できないため、色々と試してみます。
以降、結論を書いていますが、ここに辿り着くのにかなりの時間を費やしました・・😖
使用した対局
私の以下の対局を使用しました。
対局前に、ハッシュ値の一覧を取得しました。
天鳳のサイトの「牌譜URL to SHA512」を使用して、上記のオレンジ色のハッシュ値が取得できました。
以下のハッシュ値です。
3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4
対局後の牌譜の中に記載されている乱数種は以下でした。
この乱数種から、上記のハッシュ値を取得したいと思います。
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
乱数種のソース
牌譜の取得
まずは対局の牌譜を取得します。
有料会員であれば対局の牌譜のダウンロードは可能です。
自分のIDを入力し、ダウンロードリンクから、mjlogファイルを取得することができます。
ダウンロードしたファイルはgzip圧縮されていますので、拡張子を変更して解凍します。
ファイルをテキストエディタで開くと、XML形式のデータを確認できます。
先頭部分に、対局で使用した乱数種が記載されています。
<mjloggm ver="2.3"><SHUFFLE seed="mt19937ar-sha512-n288-base64,乱数種" ref=""/>
ソースの取得
以降、C++でプログラミングしていきます。
設定の方法などは、以下の記事を参考にしてください。
乱数種のソースは以下とのこと。
624*8(乱数種) = 4992bytes
XMLに記載されている乱数種は、Base64でエンコードされているため、デコードを行います。
全てのソースは最後にも記載しますが、乱数種と事前に公開されているハッシュ値を定義しています。
#include <iostream>
#include <windows.h>
#include <openssl/sha.h>
#include "base64.h"
#define MTRAND_N 624
// 乱数種
char MTseed_b64[] = "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";
// 事前に公開されているハッシュ値
char beforeHash[] = "3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4";
int main() {
int i;
// 2496byte+1(終了コード追加用の+1)
unsigned char MTseed[MTRAND_N * 4 + 1];
// base64でデコード
base64Decode(MTseed_b64, (char*)MTseed);
}
デコードされた情報は、2496byte+1のデータが取れました。
このデータを16進の文字列で表示します。
// 乱数種のバイナリデータを16進の文字列で表示
// 2496byteを2文字の16進で表示→4992文字
char seedStr16[4992 + 1] = "";
// 終了コードは除く
for (int i = 0; i < sizeof(MTseed) - 1; ++i) {
// 16進の文字列で代入
char buf[3] = "";
sprintf_s(buf, sizeof(buf), "%02x", MTseed[i]);
// 文字列を格納
strcat_s(seedStr16, buf);
}
// 16進の乱数種を表示
printf("seed:\r\n%s\r\n", seedStr16);
実行を行うと、乱数種の16進の文字列が取得できました。
以下をbytesと呼ぶとややこしいので「文字」と呼びますが、4992文字取得できました。
3833a3078d60fd1b823409d277626820cbe68e5f69ac98cf3c57b62aa3fe68e9bcafc2797261534755bae1912db00beb53689bc8953b6a77a376dc677d1bb289cc7bfe98bfd00f3373de70d564b996b3b08a35cb343184bebf56b9d7d5c3cbaa34e80c1e3956ce2c7b3518e421ed6fc1f38004cc7274f8936b34ebaf76e0d5fb32a68a5c656d7cf9d2615287e72c6d2d9e480780b80958f38fedaf8023e1aebd1be203c599c014f93402189719c5aa6731e507ccd849fb4a775029ce227da00cf741115c09f07d1d6d19c9760c8a8e0467c3ada841c68847d9337ca11cd53622bb5ff1cd87e16bd2539108f094f88e56547f7dcdd35aec7b994edf0cc9fc3073d63069d9494ae80624bda424e4d57a8870a42b604fcd7adf6e73e0da51d2043e770103886a0c04c92c7b8c87432ce1998ed5fa1619e2ebdfc96b16210b10931ee3471828c51abf775911b41508417abffae654893c72332eacf8d5bfe7f79ce659a92f9d43a45cb20f969c5b254e8c8f88453e5031af380420be692df434deffa4e50ea761d968c3b22195e616786560728483f81710e03e93b53fc02963eff9379f4dfb993b9967011cdc7c4682b41f5231b0d6d50aa151a615e3b3e23797308b0b742654a3b5481d06e95e491bc25ad467d0ee8e3addb6377b039e87507f200697f53525fbc0039f604c975e0895f00d77bb8582b5e265bd85d2e6e49209d299885e64b1e91bb1ae5bebf6fb8259eb7c7807e7068f7796cafae202494495168293d57f7ad846b54770cb2644f094853418365530bf3888694959fd06aec321b1e0f0defbb633116aef106309f053f7b8e5f15fa789a3179272d43519e47c56ee10d2c5a7aa5e8ecbd609302bf68a2f5c1cad962d3a72e3fce9791d03aa7055a1a086d44a5478c9faaa5875f8afe42a37413e447364fcac4b8c68afbe5c321735e90838eb22d64dd18fb5c4e355f331279ddfaf7cfaa72d308c441d1349e09a29a63178f872935b45f07d018f1855c25e4de6c0c8065cecd84e7c071920b583c3145de7a0c4f2b3955117d5901bb92405e1c0c632fcf9e4625e243cae07f9222269cd2e5551834b9b9db1ce2137373a05155cd44484adf57f576523f61e0d6fa4130f7a499ea8cc73036075a645effe578996ed22e4a7f708508e98dfffa12c96857d5801d3455fb33cb4de3f1dea4ae96676a19690212e4b1ed30a5f4e4d9451f2836581b4206227853d6be50cfb7c48b3f915e2dedbe066ff0e20c2cf47c401f28988ac1adc2cddca047c1a5e809bbdf9dda45b088efbd3e4d108600bd53f23db42e410a1b22fe33258b527343217b92e6a6ddddf3436958714f843eef0ae6bc5d03c13fe7dbbc2bd4f8acba512bb60a6ede45e17eae08e06ddc2c6d6fb4f0d0ba77bc5bd2a3e119c56f3fb37b9c6cca91aed6a6132c83315cd49e470e6d1fa681819fc6ea390c09a0853d0855d0f3ce0435c1cde3adae329bda9e874018b5de27f978bb0c965b1d26b67b7f7ffe5882184eb91847f2bb31f9b6f46ce3dd7ee23821a500e172eb6e29326d2aeb5d35a9a5b7ed4bfdfa38b32f64f23e366f243b8de98247086a1c8da943f218da376b337e282caae5499f8e1ebe57572236a8197d2903e3257321997f1a246bcf8818452dd6a97c0a361bef54a139c32639e6cbaf5d7517c2cfd1b8d702cb64174950885f40d2d107870c006e26ada3dd1b7dfa81b450c112cad3c45698e67f3221fb50e70e6c3b4cf8598c2287bad741e46ad9b69dafb461b1d90139b21636e66d14e19b7d9b4f8364b295ab35d2295642fa80f9a01ce4af29565460da4f54468e986df3d2a4a7925dcd4e7d29ab06ba72aa74e6be4d98e829afa30ac9b6f5f12ea4430e014e414d4322d7b35f269f33f4df7941238f7b53392fdce9e58dfb662fc12592fc668f72c8041b067ffc23323d652a59010754fd1fd38a5b3d888144f76c90fbb6b302e8d3c5e4116c6514dcb0d68378e8b462defef84f9ee4cd61a1490412a20b28004cb62708dfe352168e51abf98999b4dfba3961eb5c14e71167485526436dce6eb32053b05c27c82211de3c0c5ff18ca9c51f989d67ecc5bd669271e429591940f65f35f9db9df4c6dea4a389ca55036e16a44d2be5d26c379d711e156bd458c060c645629db486cd418b66afa62002c449de2f883865503c9aa971d3ca49e73dce8abe8e423091b27ec6e9d22ad1db2f09697617f45a2cba3915cd24bf0ff8f1cebd0b6224d35bbc7b07041c48e02fa1a440eef544c8a41b6eb837ffb2ab5b318325584eb46a21920020c73f7453c947202520e93a1d14e0fd87240089cf6c5804704c5449809c1a719256b015b05873f231833e3eb818f369d82b399192ac83a5dd4b223d6e74980306ac9c4855893687c113820d4ad1de4eb666c9be5edeceec50fd04b98484f283af1b2d016601f2355c986b938890f59f7e8d9ee1a35d4c1bcbca687d916e299d181dc8892e3b6675774a7fcb726c7e266ef261cdc3353473df27c46b8e268e3a1aacd0d9946fff7cb17e65b24d2a59a2853611ddf8a4b5c6373f77593da177b32b977efb34712b8a7df98589dd0b6d10fb780b16b7e597a7753b10350fb3fb3c13a868c5e9d62f25497f7381e221812a1dc5a6e23825111c9066e99938fdceab5648d38a59c31ddd6f1ccbb282f9b9ca6e3a762063e52fdc99740150131ab7f66a5283cb41324725f8088f5bfb1259553e53a526dbe0453ba153e6ffc75c5cc694f08d3fd602f9edc97b8ef09ec599791bc71f7bb13ca0c707e1e39a2f578fe7c8b67985d5d07279162b77d06133f827da767af46acd03de56ec17d04df5ab37f0bb83d6d08eb26f234e8699e576aee8f91547249c124e786049c73885eb7ada8e9be8a4d120b6a48365c377dc5fa760cee83e9aeafb137eb871a1f655e4da32fd513b8be42013175dc77d1d0d3669de6a46887afbdf6781e24baf0982a4b99ef62113f4cf2dc15b75d1abf8457eefa9a1323f6eae67150338dee3f59f6bde23d2cba2d5912812dffce4f9674fe2c19841a4a10487bace50ca7d1a39d9971e50fd3ff079fc2be006f66b4117ce46ae3e705b0a9c65afc60a7681c6673522eaacdf94fb2dc3aec230093133977824571695bd7b9a1de29ca12207d7bfb202dcd4ed024592736f5ee20afe191a6516204da670c43e9156cd11918a54c56fedc90631707a872130f8000766e50b415b614594a60caceef37d8c89aae09dfbfd360f16018ae8895234623a66308efca060ba1163f75ade6300506dd1e69ff498db71653dc3c48f017024d0e2f224f010f24f20068df48a4e2a78e98777f6784f10efb952f20ac95e815fbc370637ee0edabcf45f44c2916956d725038e8467ad7fd7fab0ee92ec164f28d74c1d542423d88bfa897926a8838e439b9908958e436199a6765fc49b57ec8a03169d5dcb3b0e494590bf4082f063a84ee8fff7d5c20bc56819ab86560f46d4fe7f869b40fe60e9ed8afeaa72ef332d783133e9f9d3234b1e2
ポイントとしては、大文字ではなく小文字の16進で取得します。
(この文字列をソースにSHA512でハッシュ化するので、大文字の場合、結果が変わるため)
この文字列が、乱数種のソースになります。
座席並べ替え情報のソース
座席並べ替え情報のソースは以下とのこと。
4(座席並べ替え情報)= 4bytes
こちらについては、どうやって取得するのか分かりませんでした🤔
ただし乱数種と同じであれば、2bytesの4文字の16進文字列が取得できれば良いと推測しました。
2bytesは256*256で、65,536通りしか無いので、総当たりで取得したいと思います。
ハッシュ値の取得
実装
乱数種のソースは取得できたとして、座席並べ替え情報のソースは総当たりで、ハッシュ値を計算したいと思います。
????(座席並べ替え情報) + 624*8(乱数種) = SHA512のソース
これで、事前に取得したハッシュ値と同じ文字列を生成することができれば、検証はOKです。
(対局後の牌譜から、事前に公開されているハッシュ値が生成できたことになるので)
ソースコードは以下です。
// 座席並べ替え情報は総当たりで計算
// 2byte: 256*256 = 65536
// 0000 ~ ffff
for (int i = 0; i < 65536; ++i) {
// 座席並べ替え情報
char buf[5];
sprintf_s(buf, sizeof(buf), "%04x", i);
// SHA512のソース(座席並べ替え情報 + 乱数種)
// 4996文字
char source[4996 + 1] = "";
strcat_s(source, buf);
strcat_s(source, seedStr16);
// SHA512でハッシュ化
unsigned char bufHash[SHA512_DIGEST_LENGTH];
SHA512((BYTE*)source, sizeof(source) - 1, (BYTE*)bufHash);
// ハッシュを16進文字列で取り出し(2文字ずつ)
char hashStr16[SHA512_DIGEST_LENGTH * 2 + 1] = "";
for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {
char buf[3];
sprintf_s(buf, sizeof(buf), "%02x", (unsigned char)bufHash[i]);
strcat_s(hashStr16, buf);
}
// ハッシュの表示
//printf("hash:%s\r\n", hashStr16);
// ハッシュの比較
if (!strcmp(beforeHash, hashStr16)) {
// ハッシュを発見!
printf("\r\n========\r\nHash Found!\r\nsource:%s", source);
break;
}
}
SHA512について補足
SHA512とはSHA-2の一種であり、出力は512ビット(64bytes)です。
1byteを16進の2文字で出力し、128文字のハッシュ値になります。
実行
ソースコードを実行してみましょう。
総当りと言ってもパターンが多くないので、直ぐにハッシュ値は見つかります。
========
Hash Found!
source:20133833a3078d60fd1b823409d277626820cbe68e5f69ac98cf3c57b62aa3fe68e9bcafc2797261534755bae1912db00beb53689bc8953b6a77a376dc677d1bb289cc7bfe98bfd00f3373de70d564b996b3b08a35cb343184bebf56b9d7d5c3cbaa34e80c1e3956ce2c7b3518e421ed6fc1f38004cc7274f8936b34ebaf76e0d5fb32a68a5c656d7cf9d2615287e72c6d2d9e480780b80958f38fedaf8023e1aebd1be203c599c014f93402189719c5aa6731e507ccd849fb4a775029ce227da00cf741115c09f07d1d6d19c9760c8a8e0467c3ada841c68847d9337ca11cd53622bb5ff1cd87e16bd2539108f094f88e56547f7dcdd35aec7b994edf0cc9fc3073d63069d9494ae80624bda424e4d57a8870a42b604fcd7adf6e73e0da51d2043e770103886a0c04c92c7b8c87432ce1998ed5fa1619e2ebdfc96b16210b10931ee3471828c51abf775911b41508417abffae654893c72332eacf8d5bfe7f79ce659a92f9d43a45cb20f969c5b254e8c8f88453e5031af380420be692df434deffa4e50ea761d968c3b22195e616786560728483f81710e03e93b53fc02963eff9379f4dfb993b9967011cdc7c4682b41f5231b0d6d50aa151a615e3b3e23797308b0b742654a3b5481d06e95e491bc25ad467d0ee8e3addb6377b039e87507f200697f53525fbc0039f604c975e0895f00d77bb8582b5e265bd85d2e6e49209d299885e64b1e91bb1ae5bebf6fb8259eb7c7807e7068f7796cafae202494495168293d57f7ad846b54770cb2644f094853418365530bf3888694959fd06aec321b1e0f0defbb633116aef106309f053f7b8e5f15fa789a3179272d43519e47c56ee10d2c5a7aa5e8ecbd609302bf68a2f5c1cad962d3a72e3fce9791d03aa7055a1a086d44a5478c9faaa5875f8afe42a37413e447364fcac4b8c68afbe5c321735e90838eb22d64dd18fb5c4e355f331279ddfaf7cfaa72d308c441d1349e09a29a63178f872935b45f07d018f1855c25e4de6c0c8065cecd84e7c071920b583c3145de7a0c4f2b3955117d5901bb92405e1c0c632fcf9e4625e243cae07f9222269cd2e5551834b9b9db1ce2137373a05155cd44484adf57f576523f61e0d6fa4130f7a499ea8cc73036075a645effe578996ed22e4a7f708508e98dfffa12c96857d5801d3455fb33cb4de3f1dea4ae96676a19690212e4b1ed30a5f4e4d9451f2836581b4206227853d6be50cfb7c48b3f915e2dedbe066ff0e20c2cf47c401f28988ac1adc2cddca047c1a5e809bbdf9dda45b088efbd3e4d108600bd53f23db42e410a1b22fe33258b527343217b92e6a6ddddf3436958714f843eef0ae6bc5d03c13fe7dbbc2bd4f8acba512bb60a6ede45e17eae08e06ddc2c6d6fb4f0d0ba77bc5bd2a3e119c56f3fb37b9c6cca91aed6a6132c83315cd49e470e6d1fa681819fc6ea390c09a0853d0855d0f3ce0435c1cde3adae329bda9e874018b5de27f978bb0c965b1d26b67b7f7ffe5882184eb91847f2bb31f9b6f46ce3dd7ee23821a500e172eb6e29326d2aeb5d35a9a5b7ed4bfdfa38b32f64f23e366f243b8de98247086a1c8da943f218da376b337e282caae5499f8e1ebe57572236a8197d2903e3257321997f1a246bcf8818452dd6a97c0a361bef54a139c32639e6cbaf5d7517c2cfd1b8d702cb64174950885f40d2d107870c006e26ada3dd1b7dfa81b450c112cad3c45698e67f3221fb50e70e6c3b4cf8598c2287bad741e46ad9b69dafb461b1d90139b21636e66d14e19b7d9b4f8364b295ab35d2295642fa80f9a01ce4af29565460da4f54468e986df3d2a4a7925dcd4e7d29ab06ba72aa74e6be4d98e829afa30ac9b6f5f12ea4430e014e414d4322d7b35f269f33f4df7941238f7b53392fdce9e58dfb662fc12592fc668f72c8041b067ffc23323d652a59010754fd1fd38a5b3d888144f76c90fbb6b302e8d3c5e4116c6514dcb0d68378e8b462defef84f9ee4cd61a1490412a20b28004cb62708dfe352168e51abf98999b4dfba3961eb5c14e71167485526436dce6eb32053b05c27c82211de3c0c5ff18ca9c51f989d67ecc5bd669271e429591940f65f35f9db9df4c6dea4a389ca55036e16a44d2be5d26c379d711e156bd458c060c645629db486cd418b66afa62002c449de2f883865503c9aa971d3ca49e73dce8abe8e423091b27ec6e9d22ad1db2f09697617f45a2cba3915cd24bf0ff8f1cebd0b6224d35bbc7b07041c48e02fa1a440eef544c8a41b6eb837ffb2ab5b318325584eb46a21920020c73f7453c947202520e93a1d14e0fd87240089cf6c5804704c5449809c1a719256b015b05873f231833e3eb818f369d82b399192ac83a5dd4b223d6e74980306ac9c4855893687c113820d4ad1de4eb666c9be5edeceec50fd04b98484f283af1b2d016601f2355c986b938890f59f7e8d9ee1a35d4c1bcbca687d916e299d181dc8892e3b6675774a7fcb726c7e266ef261cdc3353473df27c46b8e268e3a1aacd0d9946fff7cb17e65b24d2a59a2853611ddf8a4b5c6373f77593da177b32b977efb34712b8a7df98589dd0b6d10fb780b16b7e597a7753b10350fb3fb3c13a868c5e9d62f25497f7381e221812a1dc5a6e23825111c9066e99938fdceab5648d38a59c31ddd6f1ccbb282f9b9ca6e3a762063e52fdc99740150131ab7f66a5283cb41324725f8088f5bfb1259553e53a526dbe0453ba153e6ffc75c5cc694f08d3fd602f9edc97b8ef09ec599791bc71f7bb13ca0c707e1e39a2f578fe7c8b67985d5d07279162b77d06133f827da767af46acd03de56ec17d04df5ab37f0bb83d6d08eb26f234e8699e576aee8f91547249c124e786049c73885eb7ada8e9be8a4d120b6a48365c377dc5fa760cee83e9aeafb137eb871a1f655e4da32fd513b8be42013175dc77d1d0d3669de6a46887afbdf6781e24baf0982a4b99ef62113f4cf2dc15b75d1abf8457eefa9a1323f6eae67150338dee3f59f6bde23d2cba2d5912812dffce4f9674fe2c19841a4a10487bace50ca7d1a39d9971e50fd3ff079fc2be006f66b4117ce46ae3e705b0a9c65afc60a7681c6673522eaacdf94fb2dc3aec230093133977824571695bd7b9a1de29ca12207d7bfb202dcd4ed024592736f5ee20afe191a6516204da670c43e9156cd11918a54c56fedc90631707a872130f8000766e50b415b614594a60caceef37d8c89aae09dfbfd360f16018ae8895234623a66308efca060ba1163f75ade6300506dd1e69ff498db71653dc3c48f017024d0e2f224f010f24f20068df48a4e2a78e98777f6784f10efb952f20ac95e815fbc370637ee0edabcf45f44c2916956d725038e8467ad7fd7fab0ee92ec164f28d74c1d542423d88bfa897926a8838e439b9908958e436199a6765fc49b57ec8a03169d5dcb3b0e494590bf4082f063a84ee8fff7d5c20bc56819ab86560f46d4fe7f869b40fe60e9ed8afeaa72ef332d783133e9f9d3234b1e2
コード上では、ハッシュ値が見つかった場合、SHA512のソース文字列を表示するようにしました。
ちなみに今回の座席並べ替え情報は、「2013」の四文字でした。
追記:
座席並べ替え情報は、「0123」で四人の座席を表しているのかもしれません。
そうすると24パターンだけで良いかもしれないですね。
このソースを、SHA512に変換するサイトで試してみましょう。
もちろん特定した「SHA512のソース文字列」で、事前に公開されていたハッシュ値と同じ文字列を生成することができました。
ソース全体
#include <iostream>
#include <windows.h>
#include <openssl/sha.h>
#include "base64.h"
#define MTRAND_N 624
// 乱数種
char MTseed_b64[] = "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";
// 事前に公開されているハッシュ値
char beforeHash[] = "3879799ad111291b8801436354903f077c263bac9d8ea4d85e32e438982f79239fdee38d41b67f1176554a9f231655c389a90354951e94967f791de4db5f9ee4";
int main() {
int i;
// 2496byte+1(終了コード追加用の+1)
unsigned char MTseed[MTRAND_N * 4 + 1];
// base64でデコード
base64Decode(MTseed_b64, (char*)MTseed);
// 乱数種のバイナリデータを16進の文字列で表示
// 2496byteを2文字の16進で表示→4992文字
char seedStr16[4992 + 1] = "";
// 終了コードは除く
for (int i = 0; i < sizeof(MTseed) - 1; ++i) {
// 16進の文字列で代入
char buf[3] = "";
sprintf_s(buf, sizeof(buf), "%02x", MTseed[i]);
// 文字列を格納
strcat_s(seedStr16, buf);
}
// 16進の乱数種を表示
//printf("seed:\r\n%s\r\n", seedStr16);
// 座席並べ替え情報は総当たりで計算
// 2byte: 256*256 = 65536
// 0000 ~ ffff
for (int i = 0; i < 65536; ++i) {
// 座席並べ替え情報
char buf[5];
sprintf_s(buf, sizeof(buf), "%04x", i);
// SHA512のソース(座席並べ替え情報 + 乱数種)
// 4996文字
char source[4996 + 1] = "";
strcat_s(source, buf);
strcat_s(source, seedStr16);
// SHA512でハッシュ化
unsigned char bufHash[SHA512_DIGEST_LENGTH];
SHA512((BYTE*)source, sizeof(source) - 1, (BYTE*)bufHash);
// ハッシュを16進文字列で取り出し(2文字ずつ)
char hashStr16[SHA512_DIGEST_LENGTH * 2 + 1] = "";
for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {
char buf[3];
sprintf_s(buf, sizeof(buf), "%02x", (unsigned char)bufHash[i]);
strcat_s(hashStr16, buf);
}
// ハッシュの表示
//printf("hash:%s\r\n", hashStr16);
// ハッシュの比較
if (!strcmp(beforeHash, hashStr16)) {
// ハッシュを発見!
printf("\r\n========\r\nHash Found!\r\nsource:%s", source);
break;
}
}
return 0;
}
さいごに
今回は、牌譜からハッシュ値の生成を検証しました。
天鳳では、牌操作が無いことを証明できたと思います。
「配牌を選択して提供することは不可能」
https://tenhou.net/support.html
「次のツモ牌をすりかえることは不可能」
「牌山を見て乱数種を逆算することは不可能」
ということで、天鳳やろっと✋
そしてつのださんからのお言葉、本気で嬉しかったです。検証してよかった😄
他にも私のブログで、C++について解説している記事がありますのでご覧ください。
コメント