CRYPT_BLOWFISHのアルゴリズム$2a$, $2x$, $2y$について英語のドキュメントを読んでみた

CRYPT_BLOWFISH – Blowfish ハッシュ。salt の形式は、 "$2a$" か "$2x$" あるいは "$2y$"、2 桁のコストパラメータ、"$"、そして文字 "./0-9A-Za-z" からなる 22 文字となります。

[snip]

5.3.7 までのバージョンの PHP では、salt のプレフィックスとして "$2a$" だけしか使えませんでした。PHP 5.3.7 で新たなプレフィックスが導入され、 Blowfish の実装にあったセキュリティ上の弱点に対応しました。 セキュリティ修正の対応の詳細については » この文書 を参照ください。 簡単にまとめると、PHP 5.3.7 以降しか使わないのなら "$2a$" ではなく "$2y$" を使うべきだということです。
http://www.php.net/manual/ja/function.crypt.php

crypt()に使えるハッシュ方式のCRYPT_BLOWFISHには、saltに$2a$, $2x$, $2y$の3つのアルゴリズムを指定するプレフィックスが指定出来ます。
引用した通り$2a$より$2y$を使うべきらしいけど、じゃぁ$2x$は何の為にあるのか良くわからないという事で、ちょっと深追いしてみました。

まとめ

上記引用で「» この文書」とあったhttp://www.php.net/security/crypt_blowfish.phpを読んでのまとめです。
※ 英語だと何となく分かっても細かいところが分からないので、実はまだ完全に把握しきれてません。

  • 非ASCII文字をハッシュ化するとほぼ毎回(not always)、脆弱性のある形で処理される。
  • 一部の環境において$2a$に問題があったので、$2a$での処理を修正し、互換性の為に修正前と同じ処理をする$2x$を追加。
  • $2y$は修正済みの$2a$と同じ処理をする。
  • 問題の出るの環境は、*BSDとSolaris以外のOS、もしくはPowerPCとARM以外のCPU。

良くわかってないところ

$2x$は分かりやすかったけど、$2y$が何の為に作られたのか、ちょっと良くわかってません。
多分だけど、5.3.7未満でハッシュ化されたパスワードを持つシステムで、5.3.7以上にアップグレードした後に、徐々にパスワードを脆弱性の無いものにハッシュ化し直す為に用意されてる、的な事が書いてある様な気がします。

  1. PHP 5.3.7以上にアップグレードすると$2a$は修正済なので、処理が違いハッシュの検証が出来ない。
  2. なので、DBに保存されたパスワードが$2a$だった場合、アルゴリズムを$2x$に置き換える事で、以前と同じ処理になりハッシュの検証が出来る。
  3. 5.3.7未満で作られたパスワードは、ハッシュ化をしなおしてDBに保存したいけど$2a$だと、まだ再ハッシュ化してないパスワードと見分けがつかないので、修正済み$2a$と同じ処理をする$2y$を使ってハッシュ化をして保存する。
  4. これで、DBに保存されたハッシュで$2a$は問題のある状態、$2y$は修正済みと見分けがつく。

多分こういう事何じゃないかなと思っています。

補足

Pythonの方の話だけど、2a2yは等価と書いてあります。そして2aの方がデフォルトとも。
crypt()のとこには、とりあえず$2y$を使うべきと書いてあるけど、等価であれば$2a$でも問題は無いはずだし、$2y$が移行用という理解が正しければ、最初から5.3.7以上のPHPで作るなら$2a$で良いんじゃないかって気がします。

コードで確認

public function test_ASCIIをハッシュ化する場合$2a、$2x、$2yは同じ結果になる(){
    if( version_compare(PHP_VERSION, '5.3.7') < 0 ) return;
    $password = "foo";
    $salt = array(
        'algorithm' => '$2a$',
        'cost' =>      '04$',
        'string' =>    '0123456789012345678901',
    );
    $hashed_2a = crypt($password, implode('', $salt));

    $salt['algorithm'] = '$2x$';
    $hashed_2x = crypt($password, implode('', $salt));

    $salt['algorithm'] = '$2y$';
    $hashed_2y = crypt($password, implode('', $salt));

    //$2a$04$012345678901234567890u8k59IBiNFaMMG.gK0GD0jzhrbQAcpgi
    //    04$012345678901234567890u8k59IBiNFaMMG.gK0GD0jzhrbQAcpgi
    $hashed_2a = substr($hashed_2a, 4);
    $hashed_2x = substr($hashed_2x, 4);
    $hashed_2y = substr($hashed_2y, 4);

    $this->assertEquals($hashed_2a, $hashed_2x);
    $this->assertEquals($hashed_2a, $hashed_2y);
}

public function test_非ASCII文字を含む場合、algorithmによって結果が変わる(){
    if( version_compare(PHP_VERSION, '5.3.7') < 0 ) return;
    $password = "f\x80oo";
    $salt = array(
        'algorithm' => '$2a$',
        'cost' =>      '04$',
        'string' =>    '0123456789012345678901',
    );
    $hashed_2a = crypt($password, implode('', $salt));

    $salt['algorithm'] = '$2x$';
    $hashed_2x = crypt($password, implode('', $salt));

    $salt['algorithm'] = '$2y$';
    $hashed_2y = crypt($password, implode('', $salt));

    //$2a$04$012345678901234567890u8k59IBiNFaMMG.gK0GD0jzhrbQAcpgi
    //    04$012345678901234567890u8k59IBiNFaMMG.gK0GD0jzhrbQAcpgi
    $hashed_2a = substr($hashed_2a, 4);
    $hashed_2x = substr($hashed_2x, 4);
    $hashed_2y = substr($hashed_2y, 4);

    $this->assertNotEquals($hashed_2a, $hashed_2x);
    $this->assertEquals($hashed_2a, $hashed_2y);
}

https://github.com/kanonji/php_testing/blob/2c660e3ac675d5092ebde2f5298c83d3712d3fb5/tests/cryptTest.php#L20-L74

ASCII文字であれば$2a$, $2x$, $2y$のどれでも同じハッシュが生成される事と、非ASCII文字を含んでたら、問題のあった頃の動きをする$2x$だけ違うハッシュを生成する事を確認しました。

password_hash()

CRYPT_BLOWFISHのアルゴリズムとは直接関係ないけど、PHP 5.6からはcrypt()よりpassword_hash()が推奨されるらしいです。

PHPのhash_password関数を利用している場合、タイミング攻撃に注意する必要はありません。パスワードは普通、ハッシュ化されたパスワードを比較するのでタイミング攻撃は行えません。APIキーなどを文字列として保存し、文字列として比較している場合には注意が必要です。上記のフレームワークが利用しているタイミングセーフな文字列比較を行うか、直接比較せずハッシュ値を比較するようにして下さい。
http://blog.ohgaki.net/php-5-6-become-timing-safe

PHPのドキュメントは、あまり具体的な理由を書いてくれないから、なんでpassword_hash()が推奨されるのかちゃんとは分からないけど、検索して調べる感じ、crypt()と違ってpassword_hash()はタイミング攻撃に対策済みとなるようです。

参考

書いた日

2014年7月7日頃

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>