CubicLouve

Spring_MTの技術ブログ

bcryptの仕組み(とbcrypt-rubyの中身を見る)

Ruby(主にRails)でパスワードを扱う際のハッシュ化には bcrypt-ruby を使うことがほとんどだと思います。

github.com

bcryptについて勉強しつつ、さらに実装を確認してみようと思います。

bcrypt-rubyv3.1.15 を利用しています。

bcrypt

説明はwikipediaにあります。

ja.wikipedia.org

https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf

Blowfishの鍵セットアップ関数に Eksblowfish を使っていて、このセットアップにcostとソルトとパスワードを利用しています。

セットアップ後、 OrpheanBeholderScryDoubt という24 byteの文字列を64回繰り返し暗号化しバイト列を生成します。

bcrypt-ruby ではここで定義されています。

https://github.com/codahale/bcrypt-ruby/blob/4279fa01f083b48de9a18c54423453059074ba77/ext/mri/crypt_blowfish.c#L80-L87

そして、costとソルトと暗号化して生成されてバイト列を結合したものがbcryptがハッシュ値として生成する文字列になります。

bcrypt-ruby でハッシュ化された文字列

bcrypt-ruby でハッシュ化に使われるバージョンは 2a のみぽいです。

生成されるハッシュは接頭辞は $2a$ になっています。

irb(main):002:0> my_password = BCrypt::Password.create("my password")
=> "$2a$12$zafAQ3jKUpR7TXwYRm8nmeO.3p/1iZN9a09F7cFs7M6kNZxnbvf9C"
irb(main):003:0> my_password.version 
=> "2a"

ただし、他のバージョンでハッシュ化されたものも読み込めそうです。

irb(main):006:0> new_password = BCrypt::Password.new("$2y$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e")
=> "$2y$05$/OK.fbVrR/bpIqNJ5ianF.CE5elHaaO4EbggVDjb8P19RukzXSM3e"
irb(main):007:0> new_password.version
=> "2y"

https://github.com/codahale/bcrypt-ruby/blob/2121626ba841b4c1f0f3947d2f703cfe71fb977f/ext/mri/wrapper.c#L216-L234

ハッシュ化されている文字列はいくつかの区分に分けられます。

まず 最初の $ $ で 囲われている部分はバージョンです。

次に $ $で囲われているのは、コストの部分です。

これは、2を基数とした指数となり、この数の2の累乗がEksblowfishでのセットアップ中での繰り返し回数になります。(ストレッチング)

この回数が多ければ、ブルートフォース攻撃になどに対する対抗が強くなりますが、その分計算量が増えて計算に時間がかかるようになります。

bcrypt-ruby では BCrypt::Engine.calibrate() といいメソッドが用意されており、どれくらいのコストになるかをざっくりと計算できます。

irb(main):002:0> BCrypt::Engine.calibrate(5000) # この場合は5000ms以内で終わる最大のコストを算出してくる
=> 16

bcrypt-ruby ではハッシュ化のcostを指定できます。

irb(main):004:0> my_password = BCrypt::Password.create("my password", cost: 4)
=> "$2a$04$KV38A7SDgPKjmVi7ir59muRmquFxR/KmX/HtgWY97qC1tBryajHp."

ただし、4以下の場合は全て4に置き換えられます。

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/engine.rb#L67-L69

irb(main):005:0> my_password = BCrypt::Password.create("my password", cost: 1)
=> "$2a$04$vLCmqS2n9tnYpnKT.G9wJuhBPjtKakH2/9zOQuU1IIv.HIDj6q27e"  # コストが04になっている

また、costが31より大きいと ArgumentError の例外が発生します。

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L45

コスト以降の部分はソルトとチェックサムになります。

128ビットのソルトになるのですが、Radix-64エンコードされて22バイトになっています。

チェックサムも184ビットなのですが、Radix-64エンコードされて31バイトになっています。

パスワードの確認する際は、 bcrypt-ruby ではハッシュ化された文字列のバージョンやコストも含む先頭から29バイトをソルトとして渡して処理をしていきます。

ハッシュ化

bcrypt-ruby ではソルトの生成におけるランダムな16バイトを生成するために OpenSSL::Random.random_bytes() を利用しています。

docs.ruby-lang.org

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/engine.rb#L74

生成されたソルトを利用してハッシュ化をします。

https://github.com/codahale/bcrypt-ruby/blob/master/lib/bcrypt/password.rb#L46

同じソルトを使えば、同じ文字列からは同じハッシュ値が生成されます。

irb(main):032:0> my_password = BCrypt::Password.create("my password")
=> "$2a$12$VoGypMhauXeq4LZcLMbk6uGo/moLdWs/OAGUNbq9gLe9p.YZyOqXm"
irb(main):033:0> BCrypt::Engine.hash_secret("my password", "$2a$12$VoGypMhauXeq4LZcLMbk6u")
=> "$2a$12$VoGypMhauXeq4LZcLMbk6uGo/moLdWs/OAGUNbq9gLe9p.YZyOqXm"

bcrypt-ruby の利用で気をつけること

Nullバイトの対応

bcrypt-ruby は Nullバイトの対応ができてないです。

irb(main):039:0> password = BCrypt::Password.create("foo\0bar")
=> "$2a$12$YPoekx0N24xH50Z0y9JfUeBpEBvYLNISVaSaH4oWJKAWnoTHa46RO"
irb(main):040:0> password == "foo"
=> true

github.com

PRはあるのですが、mergeされていませんね、、

なので、下記2つのブログで言及されているようなことで問題になります。

blog.tokumaru.org

qiita.com

パスワードの文字数は72文字まで

72文字以降は切り捨てられます。

irb(main):043:0> password = BCrypt::Password.create("a" * 73)
=> "$2a$12$CnOALTas/7Yj0JuzBW3GuOE1rKm3DZfN7N6OLwEGH8lnELV7H3iEC"
irb(main):044:0> password == "a" * 72
=> true

これは他の言語の実装でも同じような挙動になることが多いとのことです。

SecretSalt

bcryptでは OrpheanBeholderScryDoubt という文字列を暗号化することですが、この固定の文字列も変えてハッシュ値を取るのが SecretSalt になります。

qiita.com