Session IDの作り方

 どんな風に作ったりするのか疑問だったのでちょっと調べた。

Kahuaの場合

Kahua-1.0.7.3/src/kahua/session.scm
Sessionクラス()を取り扱うファイル
Kahua-1.0.7.3/src/kahua/gsid.scm
SessionIDを取り扱うファイル

継続サーバということで、継続のためのContinuation ID、通常のSession IDとして使うState IDの二つのIDを利用するらしい。Continuation IDは再利用するらしい。

  • gsid.scm 46行 グローバルセッションの形
(define (make-gsid worker-id body)
  (format "1-~a-~a" worker-id body))

こんな感じで、1はバージョン、bodyにはContinuation IDもしくはState IDが入るらしい。

  • session.scm 66行 初期化
;;; initialization --------------------------------------------
;;
;; application server must tell this module its worker Id.

(define worker-id (make-parameter #f))

(define session-server-id (make-parameter #f))

(define (session-manager-init wid . maybe-ssid)
  (let-optionals* maybe-ssid ((ssid #f))
    (worker-id wid)
    (when (string? ssid) (session-server-id ssid))
    #t))
  • session.scm 77行 IDの範囲
(define-constant IDRANGE #x10000000)
  • session.scm 134行 Continuation IDの作成
(define (make-cont-key)
  (check-initialized)
  (let ((id (make-gsid (worker-id)
                       (number->string (random-integer IDRANGE) 36))))
    (if (cont-key->session id) (make-cont-key) id)))
  • session.scm 263行 State IDの作成
;; Creates local key.  Only used in process-local mode.
(define (make-cont-key)
  (check-initialized)
  (let ((id (make-gsid (worker-id)
                       (number->string (random-integer IDRANGE) 36))))
    (if (cont-key->session id) (make-cont-key) id)))

とりあえず、make-gsidの部分を読むと、random-integerを使ってIDを作成している。random-integerはsrfi-27で定義されている手続きで、Gaucheでは?メルセンヌ・ツイスタ - Wikipediaを使って乱数を生成する。

Rails(2.1.0)の場合

rails/vendor/rails/actionpack/lib/action_controller/cgi_ext/session.rb
CGI::Sessionクラス
SecureRandom
標準添付の乱数生成ライブラリ。opensslか/dev/urandomを利用する

Railsではcgiやcgi/sessionモジュールを再定義している。

class CGI #:nodoc:
  # * Expose the CGI instance to session stores.
  # * Don't require 'digest/md5' whenever a new session id is generated.
  class Session #:nodoc:
    begin
      require 'securerandom'

      # Generate a 32-character unique id using SecureRandom.
      # This is used to generate session ids but may be reused elsewhere.
      def self.generate_unique_id(constant = nil)
        SecureRandom.hex(16)
      end
    rescue LoadError
      # Generate an 32-character unique id based on a hash of the current time,
      # a random number, the process id, and a constant string. This is used
      # to generate session ids but may be reused elsewhere.
      def self.generate_unique_id(constant = 'foobar')
        md5 = Digest::MD5.new
        now = Time.now
        md5 << now.to_s
        md5 << String(now.usec)
        md5 << String(rand(0))
        md5 << String($$)
        md5 << constant
        md5.hexdigest
      end
    end

    # Make the CGI instance available to session stores.
    attr_reader :cgi
    attr_reader :dbman
    alias_method :initialize_without_cgi_reader, :initialize
    def initialize(cgi, options = {})
      @cgi = cgi
      initialize_without_cgi_reader(cgi, options)
    end

    private
      # Create a new session id.
      def create_new_id
        @new_session = true
        self.class.generate_unique_id
      end

  # ...
end
  • cgi/session.rb
    # Create a new session id.
    #
    # The session id is an MD5 hash based upon the time,
    # a random number, and a constant string.  This routine
    # is used internally for automatically generated
    # session ids.
    def create_new_id
      require 'digest/md5'
      md5 = Digest::MD5::new
      now = Time::now
      md5.update(now.to_s)
      md5.update(String(now.usec))
      md5.update(String(rand(0)))
      md5.update(String($$))
      md5.update('foobar')
      @new_session = true
      md5.hexdigest
    end
    private :create_new_id

securerandomがあればそれを、そうでなければruby添付のCGI::Session#create_new_idの内容を再定義して利用する。

  • securerandomがない場合
"#{現在時刻の文字列表現}#{現在時刻のマイクロ秒}#{乱数}#{PID}#{定数(種)}" # これをMD5

constantがfoobar固定でない分親切。

Ethnaの場合

Ethna-2.3.5/class/Ethna_Session.php
セッションクラス
Ethna-2.3.5/class/Ethna_Util.php
Utility
    /**
     *  セッションを開始する
     *
     *  @access public
     *  @param  int     $lifetime   セッション有効期間(秒単位, 0ならセッションクッキー)
     *  @return bool    true:正常終了 false:エラー
     */
    function start($lifetime = 0, $anonymous = false)
    {
        if ($this->session_start) {
            // we need this?
            $_SESSION['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
            $_SESSION['__anonymous__'] = $anonymous;
            return true;
        }

        if (is_null($lifetime)) {
            ini_set('session.use_cookies', 0);
        } else {
            ini_set('session.use_cookies', 1);
        }

        session_set_cookie_params($lifetime);
        session_id(Ethna_Util::getRandom());
        session_start();
        $_SESSION['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['__anonymous__'] = $anonymous;
        $this->session_start = true;

        return true;
    }
    // {{{ getRandom
    /**
     *  ランダムなハッシュ値を生成する
     *
     *  決して高速ではないので乱用は避けること
     *
     *  @access public
     *  @param  int     $length ハッシュ値の長さ(〜64)
     *  @return string  ハッシュ値
     */
    function getRandom($length = 64)
    {
        static $srand = false;

        if ($srand == false) {
            list($usec, $sec) = explode(' ', microtime());
            mt_srand((float) $sec + ((float) $usec * 100000) + getmypid());
            $srand = true;
        }

        $value = "";
        for ($i = 0; $i < 2; $i++) {
            // for Linux
            if (file_exists('/proc/net/dev')) {
                $rx = $tx = 0;
                $fp = fopen('/proc/net/dev', 'r');
                if ($fp != null) {
                    $header = true;
                    while (feof($fp) === false) {
                        $s = fgets($fp, 4096);
                        if ($header) {
                            $header = false;
                            continue;
                        }
                        $v = preg_split('/[:\s]+/', $s);
                        if (is_array($v) && count($v) > 10) {
                            $rx += $v[2];
                            $tx += $v[10];
                        }
                    }
                }
                $platform_value = $rx . $tx . mt_rand() . getmypid();
            } else {
                $platform_value = mt_rand() . getmypid();
            }
            $now = strftime('%Y%m%d %T');
            $time = gettimeofday();
            $v = $now . $time['usec'] . $platform_value . mt_rand(0, time());
            $value .= md5($v);
        }

        if ($length < 64) {
            $value = substr($value, 0, $length);
        }
        return $value;
    }
    // }}}

/proc/net/devを/dev/urandomの代わりにして利用しているように見える。MD5ハッシュ値を求めている。

  • /proc/net/dev の内容
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo:    7545      45    0    0    0     0          0         0     7545      45    0    0    0     0       0          0
  eth2:136544103  116979    0    0    0     0          0         0 10058659   73982    0    0    0     0       0          0
  • MD5でハッシュ値を求める値
platform_value = "#{/proc/net/dev packets}#{/proc/net/dev bytes}#{乱数}#{PID}" # for linux
platform_value = "#{乱数}#{PID}" # other
"#{現在時刻の文字列表現}#{現在時刻のマイクロ秒}#{platform_value}#{乱数}" # これをMD5

見てわかったこと。

乱数性質の無作為性、予測不可能性、再現不可能性のうち、予測不可能性が満たされていればよいのかなと思った。なので、そうなるようにsession idを作ればよい。
てか、こうして見るとKahuaのSession IDってこれでいいのっておもう。メルセンヌツイスタは予測不可能性を持たないらしいし(Wikipediaの知識だけど)。見落としているのかな?

まとめ

kahua
body = "#{36進の乱数}"
"#{バージョン}-#{worker-id}-#{body}"
Rails
SecureRandom.hex(16)
# もしくは
"#{現在時刻の文字列表現}#{現在時刻のマイクロ秒}#{乱数}#{PID}#{定数(種)}" # これをMD5
Ethna
platform_value = "#{/proc/net/dev packets}#{/proc/net/dev bytes}#{乱数}#{PID}" # for linux
platform_value = "#{乱数}#{PID}" # other
"#{現在時刻の文字列表現}#{現在時刻のマイクロ秒}#{platform_value}#{乱数}" # これをMD5

追記

Karreta.jpのcookieのx-kahua-sgsidみたら、gsidとは似ても似つかないものが。。。つか、ハイフン区切りじゃないし。。loginしないと駄目なのか、ぜんぜん勘違いしているのか。。。kahua-spvr.scmのhandle-commonに行き着くまでに何かしているのかなぁ。

2xw4b9kheqw8r