【PHP】今更ながらPHP/JavaScriptチャレンジレスポンス認証を実装してみる

# 1. まず初めに
#### https環境が「普通」になりつつある今更、あえてチャレンジレスポンス認証を実装する必要性は無いような気がします。

しかし、「無いよりマシ」という言葉がある通り、念には念をという事で私が作っているとあるプログラムのログイン画面に勉強も兼ねてチャレンジレスポンス認証を実装してみたいと思います。
# 2. チャレンジレスポンス認証とはなんぞや？
チャレンジレスポンス認証は他にも「チャレンジ・レスポンス認証」「チャレンジレスポンス方式認証」など、いろいろな呼び方があります。

ネットワークスペシャリスト試験(NW)ではチャレンジレスポンス方式という呼び方がされていますが、個人的にチャレンジレスポンス認証の方が言いやすい(?)ので今回はチャレンジレスポンス認証という呼び方をしたいと思います。

そして、中身についてですが、
####・とりあえず認証方法の一種
####・そもそもパスワードをネットワーク上に流さない
####・サーバー側で発生させた疑似乱数を利用してお互いにパスワードからハッシュ値を求め、両者のハッシュ値が一致したらログイン成功
という特徴があります。
# 3. サーバー側のDBに平文でパスワードを保存しないといけないってマ？
よく、「チャレンジレスポンス認証を実装したいけどDBにパスワードを平文で保存するのはリスクが高いじゃん？」などとおっしゃられている方がいますが、そんな事はありません。
ちょっと一工夫すればDBにハッシュ化したパスワードを保存しておく事も可能です。

具体的には、 
#### サーバー側のDBに保存する際にしたハッシュ化をクライアント側でも同じようにやった上で、さらにチャレンジレスポンス認証をすれば良い
という事です。
若干複雑になりそうですが、今回はこれを用いて実装したいと思います。

# 4. コピペ可能なコード
テンプレをパクりに来た方は以下のコードをコピペして頂いて結構です。
といっても、Qittaに直接コードを貼るとchromeの謎のバグで読み込みに数時間かかるようになってしまうので、リンクですが、ご了承ください

https://github.com/ActiveTK/ChallengeResponseExample

# 5. 解説

## まず、サーバー側

```php
if (isset($_POST["_login_trykey"]))
  {
    if (!isset($_POST["password"]) && isset($_POST["_login_trykey"]))
    {
      if (!isset($_SESSION["logincode"]) || empty($_SESSION["logincode"]))
        exit("<p>エラー: セッション切れが発生しました。</p>\n");
      if (md5(password_md5 . $_SESSION["logincode"]) == $_POST["_login_trykey"])
        $_SESSION["login"] = password_md5;
      else
        exit("<p>エラー: パスワードが違います。</p>\n");
    }
    else
      exit("<p>エラー: JavaScriptが無効です。</p>\n");
  }
```
このコードがログイン処理を担当する部分なのですが、実際に認証を行っているのは

```php
if (md5(password_md5 . $_SESSION["logincode"]) == $_POST["_login_trykey"])
````

の部分です。
`` $_SESSION["logincode"] ``にはサーバー側で生成した、疑似乱数が入っています。
パスワードのmd5の語尾に疑似乱数をくっつけて、さらにその文字列のmd5を求めています。
そして、そのmd5がクライアント側で生成したmd5と一致するか確認して認証をしています。

## クライアント側

```JavaScript
// 送信した時に実行される関数
function CSubmit() {
    // パスワードが空だったら怒って送信を止める
    if (_("password").value == "")
    {
      alert("パスワードを空にする事は出来ません！！！！！！！");
      return false;
    }
    // ログインボタンを無効にする(待てない人の連打対策)
    _("login").disabled = "true";
    _("login").value = "ログインしています。。";
    // ここからちょーじゅうよー
    // まず、DBに保存されているパスワードと同じ状態にする為に、パスワードのmd5を算出
    let password_md5 = CybozuLabs.MD5.calc(_("password").value);
    // で、算出したmd5の語尾に疑似乱数をつけ足して、さらにそれのmd5を算出
    let password_md5_and_rand_md5 = CybozuLabs.MD5.calc( password_md5 + "<?=$_SESSION['logincode']?>" );
    // その結果をフォームで送信する為にvalueへ代入
    _("_trykey").value = password_md5_and_rand_md5;
    // パスワード本体を送ってしまうと意味が無いので、フォームごと削除
    _("gopassword").remove();
    // 送信を許可
    return true;
}
```

このコードにはかなりコメントを書いたので、読んでいただければ理解出来ると思いますが、少しだけ解説します。

```JavaScript
let password_md5 = CybozuLabs.MD5.calc(_("password").value);
```

ここではサーバー側と同じ状態にする為に、パスワードのmd5ハッシュを求めています。

```JavaScript
let password_md5_and_rand_md5 = CybozuLabs.MD5.calc( password_md5 + "<?=$_SESSION['logincode']?>" );
```

そして、算出したmd5の語尾に疑似乱数をつけ足して、さらにそれのmd5を算出します。
後はこの結果をうまく送信する処理が書かれています。

# 6. サンプル

https://www.activetk.cf/Files/challenge-response.php

# 7. 参考文献

https://wa3.i-3-i.info/word12765.html

https://itmanabi.com/challenge-response/

https://www.ipa.go.jp/security/awareness/administrator/remote/capter6/5.html

https://qiita.com/lymansouka2017/items/b28ef366130f5e6c1c68