正解判定ロジック(JavaScript)

2023/01/02

その他

謎解きや暗号のゲームを出題・作成するに当たっては、サイト等で正誤判定したいというケースは自然に発生します。


挑戦者が入力した回答と答えが一致するかどうかを判定するロジック自体は簡単ですが、JavaScriptの場合、基本的にソースコードはすべてユーザーから見えるものとなるため、「正しい答えと一致する」という判定ロジックのコードによって正しい答えそのものが見えてしまうというのが問題となります。


これを回避するためには、正解判定の処理自体をサーバの中に投げるなどの対策が必要になりますが、多くのブログサービス等ではサーバにそういったロジックを仕込ませてもらえません。自前でサーバを立てることやサーバで処理するための負荷対策など、やや高い技術や手間が求められます。


・・というような課題のもと、JavaScriptで簡易に安全な正誤判定する方法を紹介します。

難しくない内容ですが、意外に同種の解説が見当たらなかったので。


基本的なコンセプトは、入力のプレーンテキストではなく「ハッシュ化された文字列同士で正解判定すれば良い」というものです。

そうすれば、コード部分にはハッシュ後の文字列のみ記載すれば良く、ハッシュ前のアンサーワードはコードからバレることはありません。


しかし、実際に上記を実装しようとすると、既存のハッシュ化ロジックを組み込むのはそこそこ面倒くさいということに気づきます。

いま欲しいのはゲームのための簡易で実用的な「ハッシュっぽいもの」であり、苦労してかつ計算負荷を高めてまでガチでセキュアなハッシュ関数を実装しなければならないわけではありません。


そこで、ハッシュっぽいものを簡単に作ってみます。

任意の単語を別の文字列や数に変換できて、変換先で衝突が起こりにくく、逆変換が難しければ良いという性質を満たしそうなものとして、冪剰余計算を使います。


modexp(n, k, m)という関数をnのk乗 (mod m)とするとき、k -> modexp(n, k, m)の計算は簡単にできるが、modexp(n, k, m) -> k の逆変換は簡単に計算できないだろう、という目論見です。


mが素数だったり素因数分解ができてしまうと逆変換が用意になるため、mとしてはなるべく大きな素数の積を用いる必要があります。RSA暗号と同様の注意点となります。


適切に数学的あるいは計算量分析を行って検証したわけではないので、実際どれくらいの難度になるのか分かりません。ただ、ちょっとした手間で解けるような問題ではないと思います。


実装としては、回答の文字列をそれぞれ文字コードに変換し、

nの初期値: N0
N1 = 回答1文字目の文字コードをkとしてmodexp(N0, k, m)
N2 = 回答2文字目の文字コードをkとしてmodexp(N1, k, m)
N3 = 回答3文字目の文字コードをkとしてmodexp(N2, k, m)

としていく感じです。nの方を回していくのは、kの方だと計算結果が左右可換なので、例えば文字列ABと文字列BAの計算結果が同じになってしまう(より一般に、アナグラム文字列を同一視してしまう)からです。


実際のコードとしては、下記のように書けます。

modexp(n, k, m)

function modexp(base, exponent, modulus)

{
    var a = base % modulus;
    var b = exponent;
    var result = 1;
    var x = a;


    while(b > 0){
        var lead = b % 2;
        b = Math.floor(b / 2);
        if (lead === 1) {result = result * x; result = result % modulus;}
        x = x * x;
        x = x % modulus;
    }

    return result;

}


ハッシュっぽいもの

function hash(a)

{
    var n = [nの初期値]; var m = [大きな素数の積];
    for (i = 0; i < a.length; ++ i)
    {
        var k = a.charCodeAt(i);
        n = modexp(n, k, m);
    }
    return ("xxxxxxxxxxxx" + n.toString(16)).slice(-12);
}

※ 結果を12桁に揃えたいため、桁数が足りないときはxで埋めています。

たとえば

nの初期値 = 1337

m = 99524506582793

とするとBREADの変換結果は57e91aee4c83になるため、入力結果がBREADになるか確かめるにはBREADという文字列をコードに記載することなく、hash(BREAD)が57e91aee4c83となることを記載すれば良くなります。


実際にこれを使って作ったのがパンなつ謎正答チェッカーです。


パンが無いならつくれ!が出題していたパンなつ謎の正解チェッカーで、公式アカウントの運用終了によりDMでの正解判定がなくなったので、チェックツールを作成しました。


このツールでは、回答の表記ゆれに対応して、複数正答がある場合に対応した形で実装しています。詳細は上記ページのソースコードを参照ください。


また、もう一つの実装例がCrypto Brellaです。


このサイトでは回答は数字・アルファベットに限っているため、表記揺れへの対応としてハッシュ(もどき)化前にすべて大文字への変換および数字・アルファベット以外の文字列の削除を行っています。


また、このサイトでは正解した際に次の問題のページにジャンプする機能も備えていますが、次の問題のURL を「アンサーワード(ハッシュ化前)の先頭にxを付けた文字列をハッシュ化したもの」としているため、直接次のページのURLをコードに記載することなく、JavaScriptのみで次のページにジャンプさせられるようになっています。


詳細はソースコードhttps://crypted.wp-x.jp/wp-content/themes/coral-dark-child/myjs.jsを御覧ください。 


組み込みやすく、個人的に気に入っている仕組みです。


もしも標準的なハッシュ関数を組み込むことにハードルがないならば、独自のハッシュもどきを作成するよりも標準的なハッシュ関数を用いるほうがより堅牢で良いと思います。ただしその場合、ソルトを加えるなどして微調整しないと、アンサーワードに使われるような英単語についてはハッシュ値との対応表が作成されていることが多く、ハッシュ値を検索したらアンサーワードがヒットするというような事故が起こり得ます。



(画像はMidjourney)