WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Ruby で OpenSSL の乱数生成を利用する

最近、いろいろなサービスで、以下のような予測しづらい URL を使ってファイルを公開する方法が採用されています。

http://www.box.net/public/guv1up9bn7

この方法は URL を誰に教えるかでユーザーが自由に公開範囲を制御できるので、機密性の低いデータの共有にはとても便利な方法です。アクセス制限が必要ないので、サーバーへの付加も小さいですしね(笑)。でも、この URL ってどういうふうに生成しているんでしょう。単純なものなら英数字を乱数で適当にチョイスするだけでできますが、これだとけっこうな確率で「実在する URL」を推測できそうです。通常の乱数は、シードが決まればそれ以降のシーケンスはすべて特定できてしまいますから。

これと似たようなものでセッション ID があります。よく Cookie とかに保存されているやつですね。これはユーザー認証の基礎になるものなので、要求されるセキュリティーレベルも高く、参考になるかもしれません。そこで、試しに Ruby の CGI::Session を調べてみたところ、どうやら以下の文字列をつなぎ合わせて MD5 ハッシュをかけることで生成しているようです。

  • 現在時刻(Time#usec 含む)
  • 普通の乱数
  • プロセス ID
  • 固定の文字列 'foobar'

単純な乱数よりだいぶ良くなっていますが、それでも思ったよりシンプルですね。また、最後に MD5 をかけると、 ID の長さが固定されてしまう(鍵空間の広さを調整できない)という欠点もあります。短期間しか使わないセッション ID なら大丈夫でしょうが、永続的な ID としてはちょっと不安が残ります。

なにか Ruby で簡単に強固な ID を生成する方法はないかなぁ、と考えていたのですが、そういえば Ruby には OpenSSL バインドが標準で添付されていました。強力な暗号化には予測困難で偏りの少ない乱数が不可欠ですから、これは期待できますね。そんなわけで、 Ruby から OpenSSL の乱数生成器を利用する方法を調べてみましたので、本日はそれをご紹介しようと思います。

使い方

調べてみたところ、 C 言語なら OpenSSL の乱数は RAND_bytes という関数で利用できるようです。 Ruby のリファレンスマニュアルには対応するメソッドが記載されていないのですが、ソースを調べたところ OpenSSL::Random.random_bytes というメソッドを見つけました。使い方は以下のとおりです。

str = OpenSSL::Random.random_bytes(length)

引数 length で渡したバイト数分のランダムな文字列を返します。とくに初期化処理も必要なく、 openssl を require してメソッドを呼ぶだけで動いてくれます。結果の文字列はバイナリですので、 Cookie などに格納するときは Base64 あたりでエンコードしてやるとよいでしょう。サンプルとして、上記メソッドで生成したランダムな文字列を表示するスクリプトを書いてみました。

#! /usr/bin/ruby
require 'openssl'
print [OpenSSL::Random.random_bytes(8)].pack("m")

という感じで、実質 1 行で書けてしまいます。素晴らしい。

速度比較

上記のスクリプトを実行してみると明らかにわかるのですが、結果が表示されるまで一瞬待たされます。どうも、起動した後の最初の 1 回だけ異様に時間がかかるようです。シードの初期化をしているのか、それともライブラリ自体をこのタイミングでリンクしているのか。原因はわかりませんが、ちょっと気になりますね。そこで、簡単なベンチマークをとってみることにしました。テスト内容は以下のとおり。

  1. プロセス起動直後の初回の random_bytes 呼び出しにかかる時間。
  2. random_bytes(4) を 10 万回呼び出すのにかかる時間。初回呼び出しのウェイトは含まず。
  3. random_bytes(64) を 10 万回呼び出すのにかかる時間。初回呼び出しのウェイトは含まず。
  4. 通常の rand を 10 万回呼び出すのにかかる時間。

テスト結果は以下のようになりました。テスト環境は Pentium4 3.2GHz, Windows XP, Win32 版 Ruby 1.8.2 で、平均などはとっていませんが、数回試してもだいたい似たような結果になりました。

テスト内容 実行時間(ms)
最初の 1 回 256
4byte の乱数を 10 万回計算 453
64byte の乱数を 10 万回計算 797
通常の rand を 10 万回計算 78

やはり (1) の重さが際立ちますね。約 1/4 秒、 2 回目以降の 5 万回に匹敵するくらいの時間がかかっています。 CGI で使うのはちょっと躊躇してしまう結果です。それ以降も通常の rand と比べると 5 〜 10 倍程度の処理負荷になりますが、これはまだ許容範囲でしょう。 rand の結果にハッシュをかけたりすれば、ほとんど差がなくなりそうな気もします。そのほかに興味深い点は、 (2) と (3) で生成する乱数のサイズが 16 倍になっているにもかかわらず、実行時間は 2 倍程度にしか増えていないあたりでしょうか。長い乱数が必要なときは、むしろ OpenSSL のほうが有利かもしれません。

以上、本日は Ruby で OpenSSL の乱数生成を利用する方法をご紹介しました。初回呼び出しがかなり重いというのは痛い欠点ですが、使い方自体はとても簡単で、いろいろと応用できそうな気がします。例えば、ユーザー登録時のパスワードの自動生成などには、かなり有効なのではないでしょうか。ところで、 Ruby はライブラリが充実しているので、ほかにもいろいろな方法があるかもしれません。もし、もっと優れた方法をご存知の方がおられましたら、ぜひ教えてくださいませ。

追記(2007/3/26)

はてぶでご指摘がありましたが、予測しづらい URL でデータを隠すという方法は、手軽で便利な反面、どんなに強固な鍵を使ってもセキュリティーは最低限のものにしかなりません。 URL は利用者のちょっとした間違い(Referer を辿られたり、うっかり公開ページからリンクしてしまったり、その他もろもろ)で簡単に流出してしまうからです。この手法において鍵を強くすることは、総当り攻撃への耐性を強めるのみに留まります。そんなわけで、この手法(URL に鍵を含める)自体は、最悪ばれても問題ない程度のデータにのみ適用してください。本文でもう少しきちんと触れておくべきでした。申し訳ありません m(_ _)m

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles