OpenSSL::Cipher::Cipher#pkcs5_keyivgen の中身

結論から書くと、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 ではこれを取り除かないと動かない。

Leave a Reply