5.2以前でも使用できるmcryptについてはこちら。
PHP 5.3でopensslモジュールの機能が拡張され、共通鍵暗号による暗号化が利用可能になった。同時に待望の疑似乱数生成器も用意された。これで今までmcryptが使用できなかったWindowsでも、暗号学的強度を持った乱数が生成できる(実際に試していないが、OpenSSLはWindowsのCryptAPIから乱数を得ている)。
まだマニュアルにも詳しい情報が載っていないので、ソースを読みながら使い方を探ってみた。以下はphp-5.3.3のソースに基づく。ただしコミットログを見る限り仕様も機能もまだ安定しているとは言いがたいので、実際に使用するにはもう少し様子を見た方が良いかもしれない。
暗号化する際に決めておくべきこと:
- 鍵(以下 KEY)… マニュアルでは
$password と表記されているが、いわゆる「パスワード」ではない。暗号化に使用する鍵であり、十分にランダムでなければならない。
- 暗号アルゴリズム+モード(以下 CIPHER)… 利用可能な一覧は openssl_get_cipher_methods から得られる。
暗号化/復号を行う関数 encrypt/decrypt は次のようになる。
//実際の暗号化/復号を行う補助関数
function _callOpenSSL($func, $msg, $iv) {
$opensslFunc = "openssl_{$func}";
return $opensslFunc($msg, CIPHER, KEY, true, $iv);
};
/**
* @param string 暗号化したいメッセージ
* @return array (暗号文, 初期化ベクトル) の配列
*/
function encrypt($msg)
{
//初期化ベクトルを生成
$ivSize = openssl_cipher_iv_length(CIPHER);
$iv = openssl_random_pseudo_bytes($ivSize, $cryptStrong);
if($iv === false || !$cryptStrong) {
// 乱数生成失敗
return false;
}
//ダミーの初期化ベクトル
$dummyIV = str_repeat("x", $ivSize);
//メッセージの暗号化
$cryptMsg = _callOpenSSL('encrypt', $msg, $iv);
//初期化ベクトルの暗号化
$cryptIV = _callOpenSSL('encrypt', $iv, $dummyIV);
return array($cryptMsg, $cryptIV);
}
/**
* @param string 暗号文
* @param string 初期化ベクトル
* @return string 平文
*/
function decrypt($cryptMsg, $cryptIV)
{
//ダミーの初期化ベクトルを生成
$ivSize = openssl_cipher_iv_length(CIPHER);
$dummyIV = str_repeat("x", $ivSize);
//初期化ベクトルの復号
$iv = _callOpenSSL('decrypt', $cryptIV, $dummyIV);
//メッセージの復号
$msg = _callOpenSSL('decrypt', $cryptMsg, $iv);
return $msg;
}
実行:
define('CIPHER', 'aes-128-cbc');
define('KEY', '...暗号化のキー...');
$msg = "めっせーじ";
// 暗号化
$crypt = encrypt($msg);
var_dump(bin2hex($crypt[0]));
// 復号
$plain = decrypt($crypt[0], $crypt[1]);
var_dump($plain);
mcrytと違ってブロック長に合わせたパディング(PKCS#5パディング)はopensslが自動でやってくれるので、自前でbase64エンコードしたりする必要はない。
openssl_random_pseudo_bytes の戻り値は、乱数の生成に失敗した場合は真偽値の false になる。また十分なエントロピーが得られなかった場合は第二引数に指定した変数が false になる(ただ滅多に起こらないはずである:OpenSSL FAQ参照)。
ちなみに鍵の生成も openssl_random_pseudo_bytes で行えばいい。
#コマンドラインにて16バイト=128ビットの乱数を生成
$ php -r 'echo base64_encode(openssl_random_pseudo_bytes(16));
bONq0KiSNIO5ww1ggwdFdQ==
//定数KEYの定義
define('KEY', base64_decode('bONq0KiSNIO5ww1ggwdFdQ=='));
パスワードから鍵を生成する必要がある場合は、自分でPBKDFを実装する必要がある。『PHP PBKDF2』で検索するといくらか実装例が見つかるようだ。またRubyの pkcs5_keyivgen(openssl の EVP_BytesToKey)互換の機能が必要な場合はOpenSSL::Cipher::Cipher#pkcs5_keyivgen の中身などを参照のこと。
openssl_random_pseudo_bytes の中身
実体はOpenSSLの RAND_pseudo_bytes(rand_lib.c) -> ssleay_rand_pseudo_bytes(md_rand.c) -> ssleay_rand_bytes(md_rand.c) で、そこから先は『暗号乱数インフラの初期化処理』を参照のこと。
ssleay_rand_pseudo_bytes と ssleay_rand_bytes の違いは RAND_R_PRNG_NOT_SEEDED を見逃すか否か。エラーがあったこと自体は戻り値で判定できるので、$crypto_strong ではこれを利用している。
ドキュメントには記されていないが、OpenSSL::PKCS5 というクラスが存在し、OpenSSL の PBKDF2 実装にアクセスすることができる。ただし OpenSSL 自体の制約により、バージョン0.9.8kの時点ではハッシュ関数として HMAC-SHA1 しか使用できない。
keys = OpenSSL::PKCS5.pbkdf2_hmac_sha1("password", "saltsalt", 1000, 16)
p keys.unpack("H*").join #=> "e9febff54bfce668fde301acc85563cc"
OpenSSL の将来のバージョン(1.0.0?)ではより汎用的な関数が追加される予定であり、Ruby からも利用できるようになるようだ。
keys = OpenSSL::PKCS5.pbkdf2_hmac("password", "saltsalt", 1000, 32, "sha256")
# => OpenSSL 0.9.8k では NotImplementedError
このあたりがまだ中途半端なので、ドキュメントには記されていないのだろうか。
Ruby のリポジトリをチェックしてみると、2007年4月5日にまず trunk に対して追加され、その後1.8系にバックポートされたらしい。
非常に多機能な暗号ライブラリ LibTomCrypt。各種の対称鍵暗号、公開鍵暗号、疑似乱数生成器、MAC、デジタル署名、完全性検証機能付きのブロック暗号のモード(CCMやGCMなど)、厳密にモジュール化されたAPI、さらに200ページに及ぶpdfマニュアルまでついてくる。ライセンスはパブリックドメイン。
素晴らしいライブラリなのだが、2007年からパタリと更新が止まってしまったようだし、サイトの移転も中途半端な状態のまま。今は一体どういう状況なんだろう?と思っていろいろ検索したところ、ショッキングな事実が判明した…。
”なりすまし”の嫌がらせを受けて云々、という部分の詳しい事実関係は不明だが、ライブラリの作者Tom St Denis氏が既にメンテナンスするつもりがない、というのは間違いないようだ。2008年12月31日の投稿の中で全ソースツリーを公開して、どのようにでも使って良い、と宣言されている(また1年以上変更していない、とも)。
http://home.libtom.org/lt_tree.tar.bz2
公式にリリースされた最新バージョンはミラーサイトにアーカイブが残っている。
設計の美しさや充実したドキュメントに感動していただけに、かなりショックだ…。このライブラリに対する Ruby や PHP のバインディングが作れたらステキだと思っていたのだが……。
stream_set_blocking関数を使う。
『C/C++セキュアプログラミングクックブック〈VOLUME2〉』に載っている「レシピII-4.3 Unix標準の乱数インフラストラクチャを使用する」をPHPで実装すれば、次のようになると思われる。
(続きを読む…)
暗号ライブラリ内部でどんな処理が行われているのか、Ruby と PHP について調べてみた。調査に使用したバージョンは ruby-1.9.1-p129, php-5.2.9, openssl-0.9.8k
ruby-openssl ライブラリの場合
乱数用のインターフェイスは OpenSSL::Random モジュール。例えば
salt = OpenSSL::Random.random_bytes(8)
とすると8バイトのランダムなバイト列が取得できるのだが、こんな風に初期化処理なしで、いきなり乱数の取得メソッドを使っても大丈夫なのだろうか?
- Ruby のソースを見ると random_bytes の定義は ossl_rand.c にあり、実体は OpenSSL の RAND_bytes() に対するアダプタ
- OpenSSL のソースを見ると RAND_bytes の定義は rand_lib.c にあり、実体は(非FIPSモードでは)md_rand.c の中の ssleay_rand_bytes()
- ssleay_rand_bytes() の中で初期化済みか否か(initialized)を調べて、初期化済みでなかったら RAND_poll() を呼び出す
- RAND_poll の定義はOS/環境ごとに分かれているが、可能な限りのソースからエントロピーを収集するようになっている
- UNIX: rand_unix.c 通常はまず乱数デバイスからエントロピーの読み出しを試みる。試行対象となるデバイスのパスは e_os.h で DEVRANDOM として定義されており、デフォルトでは /dev/urandom が最優先となる(この際、Linux では poll, その他では select を使って非同期で読み出される)。それでも十分なエントロピーが集まらなかった場合、エントロピー収集デーモン(EGD)のソケットファイルから読み出しを試みる(こちらも e_os.h に試行対象となるパスが定義されている)。さらに pid と uid と現在時刻もエントロピーとして追加する。
- Windows: rand_win.c の場合は CryptAPI の CryptGenRandom() 関数から取得した疑似乱数をエントロピーとして追加する。さらにメモリ使用量やプロセスIDといったデータもエントロピーとして追加する。
ということで、ほとんどのケースではいきなり random_bytes メソッドなどを使っても大丈夫そうである。
ちなみに OpenSSL::Random モジュールには十分なエントロピーで初期化されたかどうかを調べる status? メソッドもある(現在はUndocumentedだが)。こちらもソースをたどって行けば初期化コードに繋がっているので、システムに十分なエントロピーが存在する環境ならば、常に true を返すはずである。
OpenSSL::Random.status? #=> true
php-mcrypt ライブラリの場合
直接的な乱数インターフェイスは存在しないが、初期化ベクトルの生成用として mcrypt_create_iv() 関数が用意されている。 この関数の定義は ext/mcrypt/mcrypt.c にあり、中身は指定されたソース(/dev/random または /dev/urandom)からそのままデータを読み出しているだけだった。
fd = open(source == RANDOM ? "/dev/random" : "/dev/urandom", O_RDONLY);
if (fd < 0) {
efree(iv);
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot open source device");
RETURN_FALSE;
}
while (read_bytes < size) {
n = read(fd, iv + read_bytes, size - read_bytes);
if (n < 0) {
break;
}
read_bytes += n;
}
n = read_bytes;
close(fd);
結構ナイーブな実装だな、というのが率直な感想。てっきり mcrypt に対応するインターフェイスがあって、処理を委譲しているのかと思っていたが…(気になって libmcrypt-2.5.8 のソースもざっとチェックしてみたが、やはり乱数の処理は入っていないようだ)。
Non-blocking I/O を使うなどの工夫は無いため、/dev/random では読み出しでブロックする可能性がある。また MCRYPT_RAND は単に rand 関数を必要な回数呼び出すだけなので、暗号用途では使うべきではない(つまりWindows環境では実質使用できない、ということになる)。
Authenticated encryption:機密性と完全性を同時に守るための暗号システム。対称暗号とMACとの組み合わせで実現するか、もしくは専用に設計された対称ブロック暗号のモードを使用する。最近になって活発に研究されている分野らしい。
そのような対称ブロック暗号のモードは Authenticated Encryption Modes といった名称で分類され、中でもNISTが標準化しているのは CCM と GCM モード。CCM モードは IEEE 802.11i 規格の一部として実用化されている。また他にNISTに提案されているものとしては CWC, EAX, OCB モードなどがある。
一般に使用可能な実装としては、Brian Gladman氏のサイトで CCM, GCM, EAX, CWC モードの実装がBSDライクなライセンスで公開されている。またパブリックドメインの暗号化ライブラリ libTomCrypt(工事中の新サイト) では EAX, OCB, CCM, GCM モードが利用できる。
『C/C++セキュアプログラミングクックブック〈VOLUME2〉対称鍵暗号の実装』でイチ押しされているのは CWC モードだが、世間的にはNIST標準である CCM モードや GCM モードの方が広く使われているようだ。
比較的新しい分野のためか、WEB上には日本語の情報は少ない(探し方が悪いのかも)。唯一CRYPTREC が2003年に公開した報告書(pdf)『ブロック暗号を使った秘匿、メッセージ認証、及び認証暗号を目的とした利用モードの技術調査報告』の中で「認証暗号に関するモード」として安全性や効率について詳しい検討を行っているが、ちょっと専門的過ぎる。一方でWikipedia英語版の充実振りは素晴らしい。
GWなので欲しかった本をまとめ買いしてきた。
『WEB+DB PRESS vol.50』
だいたい読み終えた。Gitの記事とkey-value storeの記事が面白かった。Gitは内部の構造まで理解しないと使いこなすのが難しい。Subversion とは対照的。でも総合的な使い勝手ではやっぱりGitの方が上だと思う。
Subversion が提示する世界観は分かりやすい。リポジトリ全体は「ディレクトリツリー」なのだという比喩が中心にあって、そのツリーに対する更新やらコピーやらの操作が、各種バージョン管理の操作に対応するようインターフェイスが工夫されている。オブジェクト指向的とも言えるかも知れない。一方Gitは機能指向的というか、まず自分が何をすべきなのか、どのような操作を実行すべきなのか理解した上で、対応するコマンド(とコマンドラインオプション)を選ばなければいけない。操作対象の抽象度も低く、あくまでもプログラマ向けという匂いが強い。そして実際、プログラマなら(慣れれば)Gitの方が使いやすい。
一見「分かりやすい」比喩を用意したとしても上手くいくとは限らないということか、単に比喩の妥当性の問題なのか。いろいろ考えさせられる。
『C/C++セキュアプログラミングクックブック』
前々から買おうかどうか迷っていた本。熟慮の末volume 2と3を買った。立ち読みした限りではそこまでハードなC言語の知識は要求していないっぽい。ギリギリ「コードが読める」レベルの私でもなんとかなりそう。
まだvolume 2を読み始めたばかりだが、まさに私が求めていた本、かもしれない。暗号化周りは実際にコードを書こうとすると分からなくなることが多すぎる。「IVって公開して良いの?」とか「『鍵』ってパスワードみたいにコード中に直接書いておけば良いの?」とか「ソルトってどうやって作って管理すれば良いの?」とか。自力で一つ一つ理屈を積み重ねて判断していくのは辛過ぎる…。しかもこの分野ほど”素人の直感”が裏目に出やすい分野はない。
本文中では「一般人は生の暗号化操作をするな。信頼できる高水準のインターフェイスを使え」と繰り返されている。Ruby やら PHP やらの階層でごにょごにょやるより、既存のC実装へのインターフェイスを作った方が良いのかも知れない(私にそのスキルがあればの話だが)。
以下はまだほとんど手付かず。

OpenSSL::Cipher::Cipher#padding= でパディング処理の有無を制御できる。では実際にどのようなパディング処理が行われているのだろう?
暗号化時にはパディングを有効にして、復号時に無効にすれば確認できる。
# Ruby 1.8.7 or 1.9
require 'openssl'
def enc_dec_base(operation)
password = 'fixed password'
lambda do | cipher, data, padding |
c = OpenSSL::Cipher::Cipher.new(cipher)
c.send(operation)
c.padding = padding
c.pkcs5_keyivgen(password)
c.update(data) + c.final
end
end
encrypt = enc_dec_base(:encrypt)
decrypt = enc_dec_base(:decrypt)
cipher = 'aes-256-cbc'
msg = 'ABCDEFGHIJKLMNOP' # 16bytes = AES block size
(1..msg.size).each do |len|
plain = msg[0, len]
encrypted = encrypt[cipher, plain, 1]
decrypted = decrypt[cipher, encrypted, 0] # disable padding
puts "original message: #{plain}"
p plain.bytes.to_a
p decrypted.bytes.to_a
puts
end
結果、PKCS#5 の PBES1, PBES2 と同じく RFC 1423 で定義されたパディング処理を行っていることが分かった。このパディングアルゴリズムは一般には PKCS#5 Padding などと呼ばれているらしい。
PHP の mcrypt ライブラリと相互運用する場合は、PHP 側で適切に処理する必要がある。PHP Manual の User Contributed Notes に実装例を見つけた:duerra_NOT_THIS_ at pushitlive dot net。
一応実行結果も載せておく。見事に階段状になっていてちょっと面白い。
(続きを読む…)
PKCS#5 section 4.1 より引用:
For instance, suppose two legitimate parties exchange a encrypted message, where the encryption key is an 80-bit key derived from a shared password with some salt. An opponent could take the salt from that encryption and provide it to one of the parties as though it were for a 40-bit key. If the party reveals the result of decryption with the 40-bit key, the opponent may be able to solve for the 40-bit key. In the case that 40-bit key is the first half of the 80-bit key, the opponent can then readily solve for the remaining 40 bits of the 80-bit key.
何回も読み直してようやく理解できた、と思う。
パスワードを元に鍵を導出する関数:PBKDF は、Nビットの鍵を得るために、パスワードとソルトから作ったビット列を先頭からNビットだけ切り出して鍵とする。つまり同じパスワードとソルトから異なる長さの鍵を生成した場合、短い方は長い方の先頭部分と一致する。これは必ずしも鍵導出関数一般の性質とは言えないのでは無いかと思うが、少なくとも PBKDF1(PKCS#5 v1.5)、PBKDF2(PKCS#5 v2.0)、EVP_BytesToKeys、いずれにも当てはまる。
それを前提として、引用文の例は次のようなものである:今「80ビットの鍵で暗号化された暗号文」と、その鍵を導出するのに使ったソルトとが公開されていたとする。攻撃者は両者を「40ビットの鍵で暗号化した暗号文」およびそのソルトだと偽って正規の復号者に渡す。だまされた復号者はソルトと正規のパスワードから40ビットの鍵を導出して復号処理を行う(この鍵は本来の80ビットの鍵の前半40ビットである)。結果、「元の暗号文を40ビットの鍵で復号処理したバイナリ」が作り出されることになる。もし攻撃者がこのバイナリを手に入れた場合、高々40ビットの鍵なので、ブルートフォース等で鍵を特定することは可能である。すると残る鍵は後半40ビットだけなので、これも同様に破ることができるようになる。つまり「80ビットの鍵」が「40ビットの鍵2つ」に分割されてしまった!しかもこの操作は(鍵長とアルゴリズムの都合さえ合えば)何度も繰り返すことができる。一般に N ビットの鍵に対して M 回繰り返せば、 N / (M+1) + M ビットにまで鍵を弱めることができる。こういった攻撃を防ぐために、PKCS#5 ではソルトの中に非ランダムな構造を持たせ、鍵の長さや用途(暗号化かメッセージ認証か)等を含める方法を提案している。
ただ、常に対策が必要なほどの脅威だとは、PKCS#5 には書いていない。”there is no concern about interactions between multiple uses of the same key” なら、ランダムに選ぶだけで良いとある(ただし長さは8ビット以上)。要するに固定的な用途で、単発的に用いるだけなら問題にしなくて良い、ということになるだろうか。
(実際のところ、上の例自体はかなり特殊な状況設定だと思う。”If the party reveals the result of decryption with the 40-bit key” なんてサラリと書いてあるが、そんなことが起こり得る状況なら元の平文が漏れる危険性の方が高いだろう)。
GitHubにて、PKCS#5 の PBKDF2 を Ruby で実装された方がいたので fork させていただいた。
tkyk’s pbkdf2-ruby at master – GitHub
とりあえず Ruby 1.9 対応を行った。テスト(spec)は主にRFC 3962に基づいているようなので、併せて詳細を調べていく予定。