結論から書くと、pkcs5_keyivgen に相当する処理を pure Ruby で実装すれば次のようになる。
def evp_bytes_to_key(digest, key_len, iv_len, pass, salt="", count=2048) total_len = key_len + iv_len return_buf = "" digest_buf = "" until return_buf.size >= total_len digest_buf << pass << salt count.times { digest_buf = digest.digest(digest_buf) } return_buf << digest_buf end return return_buf[0,key_len], return_buf[key_len,iv_len] end
まず Ruby のソースを find & grep したところ、pkcs5_keyivgen の実装は ext/openssl/ossl_cipher.c にあることが分かった。そのコードを注意深く読むと、実際の処理は openssl の EVP_BytesToKey という関数で行われていることが分かった。そこで次に openssl のソースを同様に調査したところ、crypto/evp/evp_key.c に実装があることが分かった。また OpenSSL のサイトで概要を記したドキュメントも見つけた。これら手がかりから同等の処理を組み立てたのが上記の evp_bytes_to_key である。
その調査の過程で、pkcs5_keyivgen という名前は PKCS#5 という、「パスワードに基づく暗号化」を規定した標準規格に由来するということが分かった。しかし現在の Ruby の実装(つまり EVP_BytesToKey に基づく実装)は PKCS#5 の古いバージョン(v1.5)に、限定的に適合しているだけだということも分かった。PKCS#5 の最新のバージョンは2.0であり、Ruby 1.9 のソースコードにも、前出のドキュメントにも、今後は v2.0 を使うように、と注意書きがしてある。
その点から言うと先日のエントリの実行例は、PKCS#5 v2.0 は勿論のこと、v1.5 にさえ適合していない。なぜなら v1.5 ではハッシュ関数の出力長を超える長さの鍵を導出する方法は規定されていないからだ。その部分は EVP_BytesToKey の独自拡張という扱いになる。
この妥協によって、どのような用途でどのような脅威が生じ得るのかは、今の私にはちょっと計り難い。少なくとも、ビットが反転できるとか、暗号文が毎回同じになるとか、そういう分かりやすい現象は出てこない。しかし、それでも推奨されている方法があるのなら、できるだけそちらを使いたい。
ということで今度は PKCS#5 v2.0、すなわち RPC 2898 について調べていくことにする。
参考:
- EVP_BytesToKeyをRubyで実装する – lambda {|diary| lambda { diary.succ! } }.call(hatena)
- 私と同じく Ruby で EVP_BytesToKey を実装している。こちらの方がC言語からのベタ移植に近い。
- RFC 2898 – PKCS #5: Password-Based Cryptography Specification Version 2.0
- PKCS#5 はもとは RSA 社の規格だったらしいが、現在は RFC にもなっている。和訳は、今のところは存在しないようだ。あと v1.5 の仕様も探したのだが、見つけられなかった。どうやら v2.0 の中に含まれているらしい。
最後に Ruby 実装版 EVP_BytesToKey の動作確認の方法を載せておく。オリジナルの pkcs5_keyivgen が生成した鍵と初期化ベクトルは見ることができないので、暗号文が全く同じになることで間接的に確認する。
# Ruby 1.9.1 def encrypt_proc(cipher, msg) enc = OpenSSL::Cipher::Cipher.new(cipher) enc.encrypt yield enc enc.update(msg) + enc.final end def run_test cipher = 'aes-256-cbc' digest = OpenSSL::Digest::MD5 msg = "arcadia inferno" pass = "password" salt = "saltsalt" count= 2048 original = encrypt_proc(cipher, msg) do |enc| enc.pkcs5_keyivgen(pass, salt, count, digest.new) end my_impl = encrypt_proc(cipher, msg) do |enc| k, i = evp_bytes_to_key(digest, enc.key_len, enc.iv_len, pass.dup.force_encoding('ASCII-8BIT'), salt, count) enc.key, enc.iv = k, i end original == my_impl end p run_test #=> true
一応、バイナリの処理であることを意識して force_encoding による変換を入れてある。パスワードに非 ASCII 文字を使う場合はこの変換がないと動かない。もちろん Ruby 1.8 ではこれを取り除かないと動かない。