みなさんは「2ちゃんねる」(2ch)を利用したことがあるだろうか。

まぁ、2chの存在くらいは誰もが知っていると思うが、匿名掲示板である2chには「トリップ」というなりすまし防止機能がある。

俺もかつて2chを利用したことがあり、自分のトリップというのを持っていたのだが、なんとなくこの機能を当ブログに導入してみたくなった。

ということで、今回はPHPを使ってトリップ機能を実現する関数と、それをWordPressに導入する方法を紹介する。

トリップとは

トリップは2chで使われているなりすまし防止機能である。

2chは匿名掲示板といえども、複数の投稿が同一人物によるものであることや、(2chでの)有名人が本人であることを証明するための機能がいくつか存在し、トリップはその一つである。

トリップは投稿時に名前欄に「#」(半角の井桁)に続けて任意の文字列を付けることで、#以降の部分がある種のハッシュ化処理により「◆〜」(〜の部分は10or12文字の半角文字)という文字列に置き換わるというものだ。
※ #より左の部分はなくてもいい。

例えば「名前#テスト」という名前で投稿すると「名前◆SQ2Wyjdi7M」のように置き換わる。

つまり、#以降の文字列がキーとなる暗号のようなもので、キーとなる文字列を秘密にしていれば、他者が同じ名前で投稿することができないというわけだ。

なお、「◆〜」をコピペして投稿しようとしても、◆が◇に置き換わるようになっている。

トリップ文字列からトリップのキーを計算することはできないが、キーからトリップを計算する方法は知られているので、キーを総当たりで計算することで、自分の好きな文字列を含むトリップのキーを探し出すという一種の遊びも行われてきた。

2chのトリップ機能はこれまで何度かアップデートされており、特に2009年から「12桁トリップ」が実装され、12桁以上のキーでトリップを生成するとトリップは12桁となるように変更された。

※従来の10桁トリップは、キーが11桁以下ならそのまま、12桁以上なら先頭から11桁以下に削ることで、12桁トリップとなった現在でもそのまま使える。というのも、10桁トリップの時代にはキーの先頭10桁より後ろを無視していたからだ。

俺もかつて10桁トリップや12桁トリップを計算したので、せっかくだから有効活用……というわけでもないが、ふと思い出して懐かしくなったので当ブログに導入してみようと思ったのだ。

PHPでトリップ

さて、実は2chのプログラムはPerlで書かれているので、まずはこれをPHPに移植しなければならない。

Perlのコードはネット上に出回っていて、Wikipediaにも掲載されている。

恥ずかしながら俺はPerlはわからないため、これを機に勉強してみようかとも思ったが、面倒なので(おい)探してみるとPHPに移植したコードが見つかった。

このサイトに掲載されているコードを利用させてもらうことにしたが、どうやらリンク先のコードはクラスの一部を抜粋したものであるらしく、そのまま利用できるものではないように思われた。

また、2chのシステムの内部文字エンコーディングは「Shift_JIS」であるため、当ブログをはじめ多くのシステムで使われている(と思う)内部文字エンコーディング「UTF-8」のままで処理すると出力されるトリップが変わってしまう。

というわけで、UTF-8のシステムでも関数ひとつでそのまま使えるように、上記リンク先のコードを独自に改良し、さらに以下のサイトも参考に、厳密に仕様に沿うことを目指した。

以下は俺が書いたコードである。

function hitoricap( $name ) {
    if ( ( $index = strpos($name, '#') ) === false || strlen($name) > $index + 1 )
        return str_replace( '◆', '◇', $name );
    $name = str_replace( '◆', '◇', substr($name, 0, $index) );
    $tripkey = mb_convert_encoding(substr($name, $index + 1), 'SJIS', 'UTF-8');
    if ( strlen($tripkey) >= 12 ) {
        if ( $tripkey[0] === '#' ) { // 10 digits new protocol
            if ( preg_match( '|^#([0-9a-fA-F]{16})([./0-9a-zA-Z]{0,2})$|', $tripkey, $matches ) ) {
                $key = pack('H*', $matches[1]);
                if ( ( $index = strpos($key, chr(128)) ) !== false )
                    $key = substr($key, 0, $index);
                $trip = substr(crypt($key, substr($matches[2].'..', 0, 2)), -10);
            } else
                $trip = '???';
        } elseif ( $tripkey[0] === '$' ) { // reserved
            $trip = '???';
        } else // 12 digits
            $trip = str_replace('+', '.', substr(base64_encode(sha1($tripkey, true)), 0, 12));
    } else { // 10 digits
        $key = htmlspecialchars($tripkey, ENT_QUOTES, 'SJIS');
        $salt = preg_replace( '/[^.-z]/', '.', substr($key.'H.', 1, 2) );
        $map = array(':'=>'A', ';'=>'B', '<'=>'C', '='=>'D', '>'=>'E', '?'=>'F', '@'=>'G', '['=>'a', '\\'=>'b', ']'=>'c', '^'=>'d', '_'=>'e', '`'=>'f');
        $trip = substr(crypt($key, strtr($salt, $map)), -10);
    }
    return $name.'◆'.$trip;
}

行が長くても左右にスクロールして読めるよ。以下同じ。

細かい仕様を解説しておくと、この関数は文字列を渡すと、#以降の部分をトリップに変換して置換し、「名前◆トリップ」という文字列を返すものだ。

10桁トリップの変換にはPerlの crypt() 関数を用いる――これはPHPにも移植されている。

一方、12桁トリップの変換にはより堅牢な「SHA-1+Base64」を用いる。
SHA-1も最近破られたという話だけどね。

2chの仕様に沿い、トリップキーでない部分の◆は◇に置き換える。

トリップマニアのために12桁トリップと同時に導入された、「##」に続けて16進数16桁(と任意で2文字までのSalt文字列)を入力することで、文字コードを直接指定し(指定していれば任意のSaltを使って)10桁トリップを使用することもできる「生コード指定」機能にも対応している。

なお、「##」で始まり「生コード指定」に合致しない形式と、「#$」で始まる形式は、将来の機能拡張のために予約されており、現在はこれらを使用してもトリップは「◆???」となる。

文字エンコーディングをShift_JISとして処理するには、内部エンコーディングを変えてしまうという方法もあるが、副作用があっては困るので、ここでは5行目や20行目(下から7行目)のように都度Shift_JISに変換したりShift_JISとして扱ったりすることで対応している。

WordPressでトリップ

上記のコードをWordPressに導入して、例えばコメント欄の「投稿者名」にトリップ機能を実装する場合、いくつか考えなければならないことがある。

最も簡単な方法は、投稿者名をHTMLに出力する部分に対し、フィルターフックを使うなどしてこの関数を通して変換するものだ。

しかし、この方法だとデータベース上にはトリップのキーが保存されるので、データベースを閲覧できるサイト管理者にはそのトリップのキーが筒抜けとなってしまう。

これは極めて不公平であるし、トリップのキーは容易に知ることができない、というトリップ機能の根幹を揺るがす問題になる。

だから投稿者名はデータベースに書き込む前にトリップに変換し、サーバー側にキーが残らないようにすべきだ。

WordPressにはコメントをデータベースに書き込む前に、その内容を変更するフィルターフックもあり、トリップ機能にも利用できそうだ。

しかし、ここでコメントフォームに入力した「投稿者名」や「メールアドレス」などをCookieに保存する機能を思い出したい。

Cookieにそれらを保存するタイミングを調べたところ、コメントをデータベースに保存した後、そのデータベース上のデータを読み込んでCookieに保存している。

つまり、Cookieに保存されるのはトリップ変換後の投稿者名であり、次回以降のコメント時にCookieに保存された投稿者名を利用することはできないのだ。

これでは不親切というかCookieの意味がないので、Cookieの保存処理と読み込み処理もアクションフック・フィルターフックで変更して、Cookieにはトリップキーのまま保存するようにしたい。

投稿者名を保存するCookieの名前は、既定では comment_author_[COOKIEHASH] (COOKIEHASH はCookie名に付加されるランダムな?文字列)であるが、念のためこのCookieは変更せず、 comment_author_name_[COOKIEHASH] という別の名前で保存することにする。

以上の事項を実現するために、アクションフックとフィルターフックを合わせて5個使用することになる。

少し長くなるが、コードを一つずつ紹介する。

function apply_comment_author_trip( $commentdata ) {
    $commentdata['comment_author'] = hitoricap($commentdata['comment_author']);
    return $commentdata;
}
add_filter( 'preprocess_comment', 'apply_comment_author_trip' );
add_filter( 'wp_update_comment_data', 'apply_comment_author_trip' );

これはコメントをデータベースに保存する前に、上で定義した関数 hitoricap() を使用してキャップを変換する。

フィルターフックが二つあるが、一つ目は新規コメント投稿時、二つ目は「コメント編集フォーム」などでコメントを編集した時にそれぞれ適用される。

function sanitize_comment_tripkey_cookie() {
    $key = 'comment_author_name_'.COOKIEHASH;
    if ( isset($_COOKIE[$key]) )
        $_COOKIE[$key] = esc_attr(wp_unslash($_COOKIE[$key]));
}
add_action( 'sanitize_comment_cookies', 'sanitize_comment_tripkey_cookie' );

これはWordPressコアの機能である「コメントCookieを無害化する」機能を、以下で設定する「投稿者名をトリップキーのまま保存するCookie」に拡張する。

これがないと、XSSの危険性があると思われる。

function set_cookie_comment_author_tripkey( $comment, $user, $cookies_consent = true ) {
    if ( $user->exists() )
        return;
    if ( false === $cookies_consent ) {
        setcookie( 'comment_author_name_'.COOKIEHASH, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
        return;
    }
    $comment_data = wp_unslash($_POST);
    if ( isset($comment_data['author']) && is_string($comment_data['author']) ) {
        $comment_author = trim(strip_tags($comment_data['author']));
        $max_length = 245;
        if ( mb_strlen($comment_author, '8bit') > $max_length )
            $comment_author = mb_substr($comment_author, 0, $max_length);
        $comment_cookie_lifetime = apply_filters( 'comment_cookie_lifetime', 30000000 );
        $secure = ( 'https' === parse_url( home_url(), PHP_URL_SCHEME ) );
        setcookie( 'comment_author_name_'.COOKIEHASH, $comment_author, time() + $comment_cookie_lifetime, COOKIEPATH, COOKIE_DOMAIN, $secure );
    }
}
add_action( 'set_comment_cookies', 'set_cookie_comment_author_tripkey', 10, 3 );

これは上述の通り、投稿者名をトリップキーのままCookieに保存するときのCookieの名前を comment_author_name_[COOKIEHASH] に変更する。

本来はデータベースへの保存後の投稿者名をCookieに保存するので、このコードでも本来のプロセスとできるだけ同じ処理を施そうとしたのだが、あまりに複雑になるので簡略化している。
例えば $max_length には本来は wp_get_comment_fields_max_lengths()['comment_author'] の値が入る。「245」はその既定値。

これでもXSSとかエラーとかにはならない……はず。

function get_cookie_comment_author_tripkey( $comment_author_data ) {
    $key = 'comment_author_name_'.COOKIEHASH;
    if ( isset($_COOKIE[$key]) && trim($_COOKIE[$key]) )
        $comment_author_data['comment_author'] = $_COOKIE[$key];
    return $comment_author_data;
}
add_filter( 'wp_get_current_commenter', 'get_cookie_comment_author_tripkey' );

これは一つ上のコードで投稿者名を保存するCookieの名前を変更したため、Cookieを読み込む箇所でもCookieの名前を変更する。

コメントフォームを出力するための内蔵関数 comment_form() の内部で使われ、最終的に投稿者名フィールドの初期値となる。

……以上のコードをすべて、テーマファイルの functions.php に記述すれば導入完了だ。

おことわり

※手元でいくつかのパターンを試し、トリップが正しく変換されたのを確認しているが、完全に2chと同一の結果になるかどうかは不明。

※上記のコードはいずれも、XSSなどのセキュリティの対策が十分でない可能性があります。上記のコードはあくまでサンプルであり、エスケープや無害化は各自の責任で行なうように。

というか今回は予想以上に複雑な処理が必要になって、WordPressのソースとかなりにらめっこしたが、疲れからか黒塗りの高級車に……じゃなくて、難しくて自信はあんまりない。

とりあえず動いたってことで。

まとめ

今回は2chのトリップをPHPで再現し、それをWordPressに導入するという内容でお送りした。

すでに当ブログには導入済みだが、だからどうってこともない。

今さら2ch、というところもあるが、おかげでPerlがちょっとはわかるようになったので良しとしたい。

私、Perlできます! とは言えないが……

これからも手習いだが、徐々にスキルアップしていきたい。

最近はブログをサボりがちなので、こういう感じにネタを補充していければと思う。

……ここのコメントフォームがトリップに対応してるからって、テスト目的でコメントするなよな!(フリ)