Javaで乱数を生成しない

ちょっと前に「Javaで乱数を生成する」というタイトルで、Javaで疑似乱数を生成する方法について書いてきた。

予測困難な疑似乱数を得るためには、疑似乱数生成器を用いるにせよ、何らかのノイズ(エントロピ)が必要となることがわかった*1。正直、面倒。疑似乱数を生成しなくて済むならば、是非そうしたい。Webアプリケーションであれば、アプリケーションサーバフレームワークが(きっと推測困難な)セッションIDを発行しているので、それを用いればよい。下手に自作しない方がよい。


セッションIDを用いれば(そのまま、あるいは加工して用いれば)よい例としては、Webアプリケーションにおいてよくある“画面遷移強制機能(処理の直前に、必ずある画面を経由させるための機能)”というか、“二重送信防止機能(あるコンテキストで、サブミットのリクエストが複数回送信されても、処理は1回しか実行しない機能)”というか、そういう機能が身近でよいかな。
仕組みとしては、画面内に何らかの値(ここではトークンと呼ぶことにする)を埋め込んでおいて、その画面のフォームがサブミットされた時に、リクエストに含まれるトークンとサーバ側で保持していたトークンとが一致している場合だけ処理を実行する。トークンが間違っていると処理されないため、Webアプリケーションの利用者は、必ずトークンを埋め込んだ画面からサブミットすることが必要となる。また、トークン一致後にサーバ側で保持していたトークンをクリアすることで、利用者がたくさんリクエストを送信したとしても、2番目以降のリクエストは(トークンが正しくても)処理されない。


このような機能を実現するために、Struts 1.xには、トランザクショントークン(Transaction Token)という仕組みが用意されている。例えば Struts 1.3.10 では、次のようにして毎回異なるトークンを生成している。

    /**
     * Generate a new transaction token, to be used for enforcing a single
     * request for a particular transaction.
     *
     * @param request The request we are processing
     */
    public synchronized String generateToken(HttpServletRequest request) {
        HttpSession session = request.getSession();

        return generateToken(session.getId());
    }

    /**
     * Generate a new transaction token, to be used for enforcing a single
     * request for a particular transaction.
     *
     * @param id a unique Identifier for the session or other context in which
     *           this token is to be used.
     */
    public synchronized String generateToken(String id) {
        try {
            long current = System.currentTimeMillis();

            if (current == previous) {
                current++;
            }

            previous = current;

            byte[] now = new Long(current).toString().getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");

            md.update(id.getBytes());
            md.update(now);

            return toHex(md.digest());
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    /**
     * Convert a byte array to a String of hexadecimal digits and return it.
     *
     * @param buffer The byte array to be converted
     */
    private String toHex(byte[] buffer) {
        StringBuffer sb = new StringBuffer(buffer.length * 2);

        for (int i = 0; i < buffer.length; i++) {
            sb.append(Character.forDigit((buffer[i] & 0xf0) >> 4, 16));
            sb.append(Character.forDigit(buffer[i] & 0x0f, 16));
        }

        return sb.toString();
    }


ほらね。セッションIDを使っているでしょ(ソース見る前からそうだと思っていたよ)。セッションIDと時刻のメッセージダイジェスト(ハッシュ値)をトークンにしている。たまたま同じ時刻が取れてしまったら、時刻をインクリメントしているから、結局、毎回異なるトークンが生成されるというわけだ。
セッション内で済むなら、ムダに疑似乱数を発生させず、セッションIDを使っていこう。


ちなみに、データベースを更新する処理の前に必ずこのトークンチェックを実施するという仕組みは、自分のまわりでは、2003年頃には既に開発現場に浸透していたので、CSRFクロスサイトリクエストフォージェリ)対策で困ったことはなかった*2

*1:TomcatのセッションID生成の実装によると、Javaだけを用いるならば、このノイズは“オブジェクトのハッシュコード”だった。

*2:このトークンは推測困難であり、Cookieのように自動で送信されないから、CSRFの対策になる。サーバが生成した(トークン入りの)画面からでないとサブミットできないのだから、攻撃者が生成した罠のページからサブミットできっこない。