7183


アプリケーションでログイン機構を作るために、パスワードハッシュをDBに保存します。

ハッシュを生成する際、 salt を組み合わせたり、ストレッチングをするといいと聞きましたが、自分で実装するのは不安なので、
`password_hash()` を使おうかと考えています。

  * [PHP: パスワードのハッシュ - Manual](http://php.net/manual/ja/faq.passwords.php)
  * [いまさら聞けないパスワードの取り扱い方](http://www.slideshare.net/ockeghem/ss-25447896/25)

ところが、[この関数のマニュアル](http://php.net/manual/ja/function.password-
hash.php)にはこう書かれていました。

> PASSWORD_BCRYPT を algo に指定すると、 password が最大 72 文字までに切り詰められます。

調べてみると、この制約を回避するためにパスワードをあらかじめ別のハッシュ関数にかけておくとよい、という話が見つかりました。

[password_hash()の重要な制限 | yohgaki's blog](http://blog.ohgaki.net/password_hash-
important-limitation)

追記:このあたりを書き直しました

ただこの記事を見たとき、この記事は `password_hash` に限定しているからこういう方法をとっているのではないかと思いました。

72文字を超えるパスワードを許容し、尚かつほかの実装方法もいとわないのであれば、これは使わない方がいいのでしょうか。あるいはそういった前処理を加えたとしても、依然
`password_hash` が推奨されるのでしょうか。

// Openwall の PHPass がいいという話も見かけましたが、これも内部的に `$2a$` で `crypt()`
関数を使っているようなので、同じ問題があると思っています。


CC BY-SA 3.0
2015-02-26T07:42:12.467
7133
2019-02-27T02:26:34.160
2015-02-27T04:00:20.193
8000
8000
question
php
72文字を超えるパスワードを許容したい場合、password_hashを使うべきではない？


「何を使うべきか」という質問であれば要件次第と言う答えにしかならないと思います

  * 73文字目以降が無視されるのは不可、複数のハッシュ関数を使うのは気持ち悪い→`crypt()`を使ってBROWFISH以外で自前実装。もしくはもっと低レイヤのハッシュ関数で自前実装
  * (この項について追記を読んでください)73文字目以降が無視されるのは不可だが複数のハッシュ関数という点は気にしない→ハッシュ関数を通してから`password_hash()`
  * 73文字目以降が無視されてもかまわない→そのまま`password_hash()`

ほとんどのアプリケーションでは72文字もあればパスワード強度としては十分でしょうから73文字以降が無視されたとしても現実的には問題ないと思います。

* * *

更新された質問に合わせて追記。

`password_hash()`が依然としてベストプラクティスなのか、もしくはよりよい代替手段があるのか、ということですね。

`password_hash()`を直接置き換える代替手段は提供されていない一方、`password_hash()`の文字数問題は前記の通り現実のアプリケーションに深刻な影響を与えるものとは言えず`password_hash()`を忌避する積極的な理由はないないので、結局は上に書いたとおりになると思います。

いかなる場合もベストプラクティス、から、条件付きではあるがベストプラクティス、にニュアンスは変わっていますが、現実には深刻に捉えなければならない条件とは言えないので依然としてベストプラクティスには違いない、と言うことでいいのではないでしょうか。

* * *

余談。ASCIIの印字可能文字95種を72文字使うと約2.5e+142でbit数でいうと473bitになるので、SHA384とSHA512の間ぐらいになります。逆に、512bitを文字数に直すと78文字です。

* * *

(追記)PHPのbcrypt実装はバイナリセーフではないため、生のハッシュ値をpassword_hashに渡すと意図しない切り詰めが生じる場合があります。

<https://blog.tokumaru.org/2019/02/caution-bcrypt-with-sha512.html>

ハッシュ値をpassword_hashに渡すのは避けてください。


CC BY-SA 4.0
2015-02-27T03:20:46.550
7183
2019-02-27T02:26:34.160
2019-02-27T02:26:34.160
5793
5793
7133
answer


質問文にあるマニュアルのリンクに [ユーザーランドでの実装](https://github.com/ircmaxell/password_compat)
があったので、その近所を眺めていると、fork
元がちょっと古いですが、[ここら](https://github.com/fredericve/password_compat) に crypt の
SHA512 で真面目に実装してそうなものがあったりしました。

最新のものにマージしても動きそうなので、独自実装をメンテする覚悟があるなら工夫してみるのも一つの手かもしれません。

  * 捨てコード
[code]     <?php

    require('lib/password.php');
    
    $pass = '0123456789012345678901234567890123456789012345678901234567890123456789012';
    
    $out = password_hash($pass, PASSWORD_DEFAULT);
    echo $out."\n";
    echo (password_verify($pass . "test", $out) ? "true" : "false") ."\n";
    
    $out = password_hash($pass, PASSWORD_SHA512, array('rounds' => 8192));
    echo $out."\n";
    echo (password_verify($pass . "test", $out) ? "true" : "false") ."\n";
    
[/code]

  * 実行
[code]     $ php test.php

    $2y$10$xvbzRgk6P43krmuuV2bHE.4SYMHkiznz1eM628S1dknRRzQqIAVRm
    true
    $6$rounds=8192$32eHmy/jGehb/LS4$ICeAgh81FHnDEPSaUr8f4O9tk4l6EyDg0uif1r2XCSwcbpvLYYNltXB6QcyY6NARBLb6NzxBPKfbzw2DRyxrL/
    false
    $
    
[/code]

以下は `git clone https://github.com/ircmaxell/password_compat` して
[ここ](https://github.com/fredericve/password_compat/commit/9504ebcd1f0350b7e4f2c12bbe43f716bfaac8d9)
とか
[ここ](https://github.com/fredericve/password_compat/commit/11f8d5de164d96fe121a21ecc46f5ee6add2d910)
を参考にしてお手当したものの `git diff`

[code]

    diff --git a/lib/password.php b/lib/password.php
    index 805caa5..d2588a1 100644
    --- a/lib/password.php
    +++ b/lib/password.php
    @@ -17,6 +17,7 @@ namespace {
              * code.
              */
             define('PASSWORD_BCRYPT', 1);
    +        define('PASSWORD_SHA512', 2);
             define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
             define('PASSWORD_BCRYPT_DEFAULT_COST', 10);
         }
    @@ -67,6 +68,24 @@ namespace {
                         // The expected length of the final crypt() output
                         $resultLength = 60;
                         break;
    +                case PASSWORD_SHA512:
    +                    // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
    +                    $rounds = 5000;
    +                    if (isset($options['rounds'])) {
    +                        $rounds = $options['rounds'];
    +                        if ($rounds < 1000 || $rounds > 999999999) {
    +                            trigger_error(sprintf("password_hash(): Invalid sha512 rounds parameter specified: %d", $rounds), E_USER_WARNING);
    +                            return null;
    +                        }
    +                    }
    +                    // The length of salt to generate
    +                    $raw_salt_len = 16;
    +                    // The length required in the final serialization
    +                    $required_salt_len = 22;
    +                    $hash_format = sprintf('$6$rounds=%d$', $rounds);
    +                    // The expected length of the final crypt() output
    +                    $resultLength = strlen($hash_format) + $raw_salt_len + 87;
    +                    break;
                     default:
                         trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
                         return null;
    @@ -188,6 +207,11 @@ namespace {
                     $return['algoName'] = 'bcrypt';
                     list($cost) = sscanf($hash, "$2y$%d$");
                     $return['options']['cost'] = $cost;
    +            } else if (PasswordCompat\binary\_substr($hash, 0, 3) == '$6$') {
    +                $return['algo'] = PASSWORD_SHA512;
    +                $return['algoName'] = 'sha512';
    +                list($rounds) = sscanf($hash, '$6$rounds=%d$');
    +                $return['options']['rounds'] = $rounds;
                 }
                 return $return;
             }
    @@ -215,6 +239,12 @@ namespace {
                             return true;
                         }
                         break;
    +                case PASSWORD_SHA512:
    +                    $rounds = isset($options['rounds']) ? $options['rounds'] : 5000;
    +                    if ($rounds != $info['options']['rounds']) {
    +                        return true;
    +                    }
    +                    break;
                 }
                 return false;
             }
    
[/code]


CC BY-SA 3.0
2015-02-27T08:19:14.527
7210
2015-02-27T08:19:14.527
2992
7133
answer