php 5.2系のセッションID 生成に任意のハッシュアルゴリズムを利用する

前回、php のセッションID と エントロピーソース で書いたとおり、php5 では、セッションID の生成アルゴリズムを設定によってある程度カスタマイズできます。

session.entropy_file と、session.entropy_length については前回説明したとおり、セッションID を生成する上でのエントロピーソースを追加するものでした。

今回は残りの session.hash_function と session.hash_bits_per_character について。


session.hash_function は、収集したエントロピーハッシュ値を求めるアルゴリズムを指定することができます。

0 の場合、md5 が。1 の場合、sha1 がそれぞれ利用されます。

さらに、php 5.3系からは、hash_algos() 関数で得られるアルゴリズム名を渡すことで、そのハッシュ関数を利用することができます。これにより、whirlpool や ripemd320, sha512 などといった、 より衝突の起こりにくいハッシュ関数を利用することが出来るのです。


session.hash_bits_per_character は、求めたハッシュ値を、何ビット単位でパッキングしていくかを指定します。
デフォルトの 4 の場合、4bit ごとにまとめて1文字とします。

4bitを表現するには全部で16パタンあればこと足りるので、 0-9 (10) と a-f (6) を用いて表します。まぁ、つまりは16進数表記ですね。5bit の場合は 32パタン必要なので、0-9 (10) と a-v (22) で表します。
また、6bit では、64パタン必要ですが、 0-9 (10) と a-z (26) だけでは足りないので、 A-Z (26) と、さらに '-' (1) と ',' (1) を利用します。

4/5/6 どれを指定しようが、情報量は同じです。ただ、session.hash_bits_per_character が大きい数ほど、当然セッションID の長さ(文字数)は短くなります。


たとえば、md5(128bit) を 4bit ずつパッキングした場合は、ceil(128/4) = 32 なので 32桁。5bit の場合は、ceil(128/5) = 26 なので 26桁。 同じ情報量でも、より大きなbit数でパッキングした方が、最終的に得られるセッションID の桁数(文字数)が少なくなります。
また、sha1(160bit) を 5bit ずつパッキングした場合は、 ceil(160/5) = 32 となり、md5(128bit) を 4bit ずつパッキングした場合と同じ32桁で情報量を増やすことができます。



さて、セッションID は、推測のされにくい(生存期間中は)一意に存在する値が必要になるわけで、 同時に存在するセッションの数が多ければ、相応に大きなハッシュ値が必要になります。

ですが、 php 5.2 で組み込みで用意されるハッシュ関数md5sha1 のみ。 これ以外のハッシュ関数を使いたい場合は 組み込みの生成アルゴリズムを使わずに、php スクリプトでセッションID を生成する必要があります。

ということで、 php 5.2 系で 任意のハッシュ関数を用いてセッションID を生成するコードを書いてみました。

<?php
/**
 * セッションID を生成する
 *
 * @param string  $hash_algorithm ハッシュアルゴリズム
 * @param integer $bits_per_character ハッシュ値を文字列化する際の単位ビット数(4 か 5 のみ指定)
 * @return string セッションID
 */
function createSessionId($hash_algorithm, $bits_per_character=4) {
    $hash = hash_init($hash_algorithm);
    $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
    hash_update($hash, $remote_addr . microtime() . mt_rand());

    $entropy_file = ini_get('session.entropy_file');
    $entropy_length = ini_get('session.entropy_length');
    if (0 < $entropy_length && $entropy_file && is_readable($entropy_file)) {
        $fp = fopen($entropy_file, 'r');
        $buf = fread($fp, $entropy_length);
        fclose($fp);

        if ($buf) {
            hash_update($hash, $buf);
        }
    }

    if (5 == $bits_per_character) {
        return binTo5bitReadable(hash_final($hash, true));
    }

    return hash_final($hash, false);
}

/**
 * バイナリを 5bit ずつ文字列 (0-9 a-v) に変換
 *
 * @param string $binary バイナリ
 * @return string 変換された文字列
 */
function binTo5bitReadable($binary) {

    $table = array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v');
    $bin = unpack('C*', $binary);
    $digit = count($bin);
    $bin = array_reverse($bin);
    $bin[] = 0x00; // 番兵的存在

    $value = '';
    for ($i=0; $i < $digit; ++$i) {
        $idx = $i;
        switch ($i % 5) {
            case 0:
                $n1 = $bin[$idx] & 0x1F;
                $n2 = (($bin[$idx] & 0xE0)>>5) | (($bin[$idx+1] & 0x03)<<3);
                break;
            case 1:
                $n1 = ($bin[$idx] & 0x7C) >> 2;
                $n2 = (($bin[$idx] & 0x80)>>7) | (($bin[$idx+1] & 0x0F)<<1);
                break;
            case 2:
                $n1 = (($bin[$idx] & 0xF0)>>4) | (($bin[$idx+1] & 0x01)<<4);
                $n2 = ($bin[$idx+1] & 0x3E) >> 1;
                break;
            case 3:
                $n1 = (($bin[$idx] & 0xC0)>>6) | (($bin[$idx+1] & 0x07)<<2);
                $n2 = ($bin[$idx+1] & 0xF8) >> 3;
                break;
            case 4:
                continue 2; // next for
        }

        $value = $table[$n2] . $table[$n1] . $value;
    }

    $value = ltrim($value, '0');
    if ('' === $value) {
        return '0';
    }
    return $value;
}

エントロピーソースとしては、IPアドレス、現在の時刻、mt_rand による乱数 と、組み込みのセッションID 生成アルゴリズムをベースに選択。

session.entropy_file および、session.entropy_file が設定されていれば、それも利用します。

hash_final は、第2引数に false を渡す(あるいは指定しない)と、生成されたハッシュ値を16進数の文字列で返します。これは、bits_per_character が 4 の場合になります。

bits_per_character が 5 の場合、32進数表記に変換してあげればよいのですが、そのための関数が存在しないようなので、 binTo5bitReadable というメソッドでごりごり変換しています。

ん? bits_per_character が 6 の場合がない? まず使うことないだろうし、必要になったらその時書くということで^^;