PHPのpassword_hashを使用してパスワードをハッシュおよび検証する方法


94

最近、インターネットで見つけたログインスクリプトに独自のセキュリティを実装しようとしています。ユーザーごとにソルトを生成するための独自のスクリプトを作成する方法を学ぶのに苦労した後、私はに遭遇しましたpassword_hash

私が理解していることから(このページの読みに基づいて)、を使用すると、ソルトはすでに行に生成されていますpassword_hash。これは本当ですか?

私が持っていたもう一つの質問は、2つの塩を持っているのは賢明ではないかということでした。1つはファイルに直接、もう1つはDBにありますか?そうすれば、誰かがDB内のソルトを危険にさらした場合でも、ファイルに直接ソルトがありますか?私はここで塩を保存することは決して賢い考えではないことを読みました、しかしそれは人々がそれによって何を意味するかをいつも私に混乱させました。


8
いいえ。関数に塩の処理を任せてください。二重塩漬けはトラブルの原因になりますので、必要ありません。
ファンクフォーティナイナー2015年

回答:


182

password_hashパスワードを保存するには、を使用することをお勧めします。それらをDBとファイルに分けないでください。

次の入力があるとしましょう。

$password = $_POST['password'];

まず、次のようにしてパスワードをハッシュします。

$hashed_password = password_hash($password, PASSWORD_DEFAULT);

次に、出力を確認します。

var_dump($hashed_password);

ご覧のとおり、ハッシュ化されています。(私はあなたがそれらのステップをしたと思います)。

次に、このハッシュ化されたパスワードをデータベースに保存し、パスワード列がハッシュ化された値(少なくとも60文字以上)を保持するのに十分な大きさであることを確認します。ユーザーがログインを要求した場合、次のようにして、データベース内のこのハッシュ値で入力されたパスワードを確認します。

// Query the database for username and password
// ...

if(password_verify($password, $hashed_password)) {
    // If the password inputs matched the hashed password in the database
    // Do something, you know... log them in.
} 

// Else, Redirect them back to the login page.

公式リファレンス


2
わかりました、これを試してみましたが、うまくいきました。簡単すぎるように思えたので、機能を疑った。varcharの長さをどのくらい長くすることをお勧めしますか?225?
Josh Potter

4
これは、マニュアルの中にすでにあるphp.net/manual/en/function.password-hash.php --- php.net/manual/en/function.password-verify.php OPはおそらく読んだり理解していなかったということ。この質問は、誰よりも頻繁に尋ねられます。
ファンクフォーティナイナー2015年

それは別のページです。
Josh Potter 2015年

@JoshPotterは何と違うの?さらに、彼らがあなたの2番目の質問に答えていないことに気づきました。彼らはおそらくあなたがあなた自身を見つけることを期待しているか、彼らは知りません。
ファンクフォーティナイナー2015年

8
@ FunkFortyNiner、b / c Joshが質問をしました、2年後にそれを見つけました、そしてそれは私を助けました。それがSOのポイントです。そのマニュアルは泥と同じくらい明確です。
toddmo 2018

23

はい、正しく理解しました。関数password_hash()はそれ自体でソルトを生成し、結果のハッシュ値にそれを含めます。ソルトをデータベースに保存することは絶対に正しいです、それは知られていてもその仕事をします。

// Hash a new password for storing in the database.
// The function automatically generates a cryptographically safe salt.
$hashToStoreInDb = password_hash($_POST['password'], PASSWORD_DEFAULT);

// Check if the hash of the entered login password, matches the stored hash.
// The salt and the cost factor will be extracted from $existingHashFromDb.
$isPasswordCorrect = password_verify($_POST['password'], $existingHashFromDb);

あなたが言及した2番目のソルト(ファイルに保存されているもの)は、実際にはペッパーまたはサーバー側のキーです。(塩のように)ハッシュする前にそれを追加する場合は、コショウを追加します。ただし、より良い方法があります。最初にハッシュを計算し、その後、サーバー側のキーを使用してハッシュを暗号化(双方向)することができます。これにより、必要に応じてキーを変更できます。

ソルトとは対照的に、このキーは秘密にしておく必要があります。人々はしばしばそれを混ぜ合わせて塩を隠そうとしますが、塩にその仕事をさせて、鍵で秘密を追加する方が良いです。


8

はい、それは本当だ。関数に関するphpのよくある質問を疑うのはなぜですか?:)

実行の結果にpassword_hash()は4つの部分があります。

  1. 使用されるアルゴリズム
  2. パラメーター
  3. 実際のパスワードハッシュ

ご覧のとおり、ハッシュはその一部です。

確かに、セキュリティの層を追加するために追加のソルトを用意することもできますが、正直なところ、通常のphpアプリケーションではそれはやり過ぎだと思います。デフォルトのbcryptアルゴリズムは優れており、オプションのblowfishアルゴリズムは間違いなくさらに優れています。


2
BCryptはハッシュ関数であり、Blowfishは暗号化のアルゴリズムです。ただし、BCryptはBlowfishアルゴリズムに由来します。
martinstoeckli 2015年

7

パスワードを保護するためにmd5()を使用しないでください。ソルトを使用しても、常に危険です。

以下のような最新のハッシュアルゴリズムでパスワードを保護します。

<?php

// Your original Password
$password = '121@121';

//PASSWORD_BCRYPT or PASSWORD_DEFAULT use any in the 2nd parameter
/*
PASSWORD_BCRYPT always results 60 characters long string.
PASSWORD_DEFAULT capacity is beyond 60 characters
*/
$password_encrypted = password_hash($password, PASSWORD_BCRYPT);

データベースの暗号化されたパスワードおよびユーザーが入力したパスワードと照合するには、以下の機能を使用します。

<?php 

if (password_verify($password_inputted_by_user, $password_encrypted)) {
    // Success!
    echo 'Password Matches';
}else {
    // Invalid credentials
    echo 'Password Mismatch';
}

独自のソルトを使用する場合は、カスタムで生成された関数を使用してください。以下に従ってください。ただし、最新バージョンのPHPでは非推奨であるため、これはお勧めしません。

以下のコードを使用する前に、password_hash()についてお読みください。

<?php

$options = [
    'salt' => your_custom_function_for_salt(), 
    //write your own code to generate a suitable & secured salt
    'cost' => 12 // the default cost is 10
];

$hash = password_hash($your_password, PASSWORD_DEFAULT, $options);

4
この関数は暗号的に安全なソルトを生成するために最善を尽くし、それを改善することはほぼ不可能であるため、ソルトオプションは適切な理由で非推奨になりました。
martinstoeckli 2017年

@martinstoeckli、はい、あなたは正しいです、私はちょうど私の答えを更新しました、ありがとう!
Mahesh Yadav 2017年

if(isset($ _ POST ['btn-signup'])){$ uname = mysql_real_escape_string($ _ POST ['uname']); $ email = mysql_real_escape_string($ _ POST ['email']); $ upass = md5(mysql_real_escape_string($ _ POST ['pass'])); これはlogin.phpで使用されるコードです。escapeとmd5を使用せずに実行したいと思います。私は..パスワードハッシュを使用したい
のRashmi SM

PASSWORD_DEFAULT-bcryptアルゴリズムを使用します(PHP 5.5.0が必要です)。この定数は、PHPに新しく強力なアルゴリズムが追加されると、時間の経過とともに変化するように設計されていることに注意してください。そのため、この識別子を使用した結果の長さは時間の経過とともに変化する可能性があります。
エイドリアン

5

PHPのパスワード関数に組み込まれている下位互換性と上位互換性については、明らかに議論が不足しています。特に:

  1. 下位互換性:パスワード関数は、基本的には適切に記述されたラッパーでありcrypt()crypt()廃止されたハッシュアルゴリズムや安全でないハッシュアルゴリズムを使用している場合でも、本質的に-formatハッシュと下位互換性があります。
  2. Forwards Compatibilty:password_needs_rehash()認証ワークフローに少しロジックを挿入することで、ワークフローに将来の変更がない可能性がある現在および将来のアルゴリズムでハッシュを最新の状態に保つことができます。注:指定されたアルゴリズムに一致しない文字列は、暗号化と互換性のないハッシュを含め、再ハッシュが必要であるというフラグが立てられます。

例えば:

class FakeDB {
    public function __call($name, $args) {
        printf("%s::%s(%s)\n", __CLASS__, $name, json_encode($args));
        return $this;
    }
}

class MyAuth {
    protected $dbh;
    protected $fakeUsers = [
        // old crypt-md5 format
        1 => ['password' => '$1$AVbfJOzY$oIHHCHlD76Aw1xmjfTpm5.'],
        // old salted md5 format
        2 => ['password' => '3858f62230ac3c915f300c664312c63f', 'salt' => 'bar'],
        // current bcrypt format
        3 => ['password' => '$2y$10$3eUn9Rnf04DR.aj8R3WbHuBO9EdoceH9uKf6vMiD7tz766rMNOyTO']
    ];

    public function __construct($dbh) {
        $this->dbh = $dbh;
    }

    protected function getuser($id) {
        // just pretend these are coming from the DB
        return $this->fakeUsers[$id];
    }

    public function authUser($id, $password) {
        $userInfo = $this->getUser($id);

        // Do you have old, turbo-legacy, non-crypt hashes?
        if( strpos( $userInfo['password'], '$' ) !== 0 ) {
            printf("%s::legacy_hash\n", __METHOD__);
            $res = $userInfo['password'] === md5($password . $userInfo['salt']);
        } else {
            printf("%s::password_verify\n", __METHOD__);
            $res = password_verify($password, $userInfo['password']);
        }

        // once we've passed validation we can check if the hash needs updating.
        if( $res && password_needs_rehash($userInfo['password'], PASSWORD_DEFAULT) ) {
            printf("%s::rehash\n", __METHOD__);
            $stmt = $this->dbh->prepare('UPDATE users SET pass = ? WHERE user_id = ?');
            $stmt->execute([password_hash($password, PASSWORD_DEFAULT), $id]);
        }

        return $res;
    }
}

$auth = new MyAuth(new FakeDB());

for( $i=1; $i<=3; $i++) {
    var_dump($auth->authuser($i, 'foo'));
    echo PHP_EOL;
}

出力:

MyAuth::authUser::password_verify
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$zNjPwqQX\/RxjHiwkeUEzwOpkucNw49yN4jjiRY70viZpAx5x69kv.",1]])
bool(true)

MyAuth::authUser::legacy_hash
MyAuth::authUser::rehash
FakeDB::prepare(["UPDATE users SET pass = ? WHERE user_id = ?"])
FakeDB::execute([["$2y$10$VRTu4pgIkGUvilTDRTXYeOQSEYqe2GjsPoWvDUeYdV2x\/\/StjZYHu",2]])
bool(true)

MyAuth::authUser::password_verify
bool(true)

最後に、ログイン時にのみユーザーのパスワードを再ハッシュできることを考えると、ユーザーを保護するために、安全でないレガシーハッシュを「サンセット」することを検討する必要があります。これは、特定の猶予期間が経過した後、安全でない[例:裸のMD5 / SHA /その他の弱い]ハッシュをすべて削除し、ユーザーにアプリケーションのパスワードリセットメカニズムに依存させることを意味します。


0

クラスパスワードの完全なコード:

Class Password {

    public function __construct() {}


    /**
     * Hash the password using the specified algorithm
     *
     * @param string $password The password to hash
     * @param int    $algo     The algorithm to use (Defined by PASSWORD_* constants)
     * @param array  $options  The options for the algorithm to use
     *
     * @return string|false The hashed password, or false on error.
     */
    function password_hash($password, $algo, array $options = array()) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_hash to function", E_USER_WARNING);
            return null;
        }
        if (!is_string($password)) {
            trigger_error("password_hash(): Password must be a string", E_USER_WARNING);
            return null;
        }
        if (!is_int($algo)) {
            trigger_error("password_hash() expects parameter 2 to be long, " . gettype($algo) . " given", E_USER_WARNING);
            return null;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                // Note that this is a C constant, but not exposed to PHP, so we don't define it here.
                $cost = 10;
                if (isset($options['cost'])) {
                    $cost = $options['cost'];
                    if ($cost < 4 || $cost > 31) {
                        trigger_error(sprintf("password_hash(): Invalid bcrypt cost parameter specified: %d", $cost), 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("$2y$%02d$", $cost);
                break;
            default :
                trigger_error(sprintf("password_hash(): Unknown password hashing algorithm: %s", $algo), E_USER_WARNING);
                return null;
        }
        if (isset($options['salt'])) {
            switch (gettype($options['salt'])) {
                case 'NULL' :
                case 'boolean' :
                case 'integer' :
                case 'double' :
                case 'string' :
                    $salt = (string)$options['salt'];
                    break;
                case 'object' :
                    if (method_exists($options['salt'], '__tostring')) {
                        $salt = (string)$options['salt'];
                        break;
                    }
                case 'array' :
                case 'resource' :
                default :
                    trigger_error('password_hash(): Non-string salt parameter supplied', E_USER_WARNING);
                    return null;
            }
            if (strlen($salt) < $required_salt_len) {
                trigger_error(sprintf("password_hash(): Provided salt is too short: %d expecting %d", strlen($salt), $required_salt_len), E_USER_WARNING);
                return null;
            } elseif (0 == preg_match('#^[a-zA-Z0-9./]+$#D', $salt)) {
                $salt = str_replace('+', '.', base64_encode($salt));
            }
        } else {
            $salt = str_replace('+', '.', base64_encode($this->generate_entropy($required_salt_len)));
        }
        $salt = substr($salt, 0, $required_salt_len);

        $hash = $hash_format . $salt;

        $ret = crypt($password, $hash);

        if (!is_string($ret) || strlen($ret) <= 13) {
            return false;
        }

        return $ret;
    }


    /**
     * Generates Entropy using the safest available method, falling back to less preferred methods depending on support
     *
     * @param int $bytes
     *
     * @return string Returns raw bytes
     */
    function generate_entropy($bytes){
        $buffer = '';
        $buffer_valid = false;
        if (function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
            $buffer = openssl_random_pseudo_bytes($bytes);
            if ($buffer) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid && is_readable('/dev/urandom')) {
            $f = fopen('/dev/urandom', 'r');
            $read = strlen($buffer);
            while ($read < $bytes) {
                $buffer .= fread($f, $bytes - $read);
                $read = strlen($buffer);
            }
            fclose($f);
            if ($read >= $bytes) {
                $buffer_valid = true;
            }
        }
        if (!$buffer_valid || strlen($buffer) < $bytes) {
            $bl = strlen($buffer);
            for ($i = 0; $i < $bytes; $i++) {
                if ($i < $bl) {
                    $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
                } else {
                    $buffer .= chr(mt_rand(0, 255));
                }
            }
        }
        return $buffer;
    }

    /**
     * Get information about the password hash. Returns an array of the information
     * that was used to generate the password hash.
     *
     * array(
     *    'algo' => 1,
     *    'algoName' => 'bcrypt',
     *    'options' => array(
     *        'cost' => 10,
     *    ),
     * )
     *
     * @param string $hash The password hash to extract info from
     *
     * @return array The array of information about the hash.
     */
    function password_get_info($hash) {
        $return = array('algo' => 0, 'algoName' => 'unknown', 'options' => array(), );
        if (substr($hash, 0, 4) == '$2y$' && strlen($hash) == 60) {
            $return['algo'] = PASSWORD_BCRYPT;
            $return['algoName'] = 'bcrypt';
            list($cost) = sscanf($hash, "$2y$%d$");
            $return['options']['cost'] = $cost;
        }
        return $return;
    }

    /**
     * Determine if the password hash needs to be rehashed according to the options provided
     *
     * If the answer is true, after validating the password using password_verify, rehash it.
     *
     * @param string $hash    The hash to test
     * @param int    $algo    The algorithm used for new password hashes
     * @param array  $options The options array passed to password_hash
     *
     * @return boolean True if the password needs to be rehashed.
     */
    function password_needs_rehash($hash, $algo, array $options = array()) {
        $info = password_get_info($hash);
        if ($info['algo'] != $algo) {
            return true;
        }
        switch ($algo) {
            case PASSWORD_BCRYPT :
                $cost = isset($options['cost']) ? $options['cost'] : 10;
                if ($cost != $info['options']['cost']) {
                    return true;
                }
                break;
        }
        return false;
    }

    /**
     * Verify a password against a hash using a timing attack resistant approach
     *
     * @param string $password The password to verify
     * @param string $hash     The hash to verify against
     *
     * @return boolean If the password matches the hash
     */
    public function password_verify($password, $hash) {
        if (!function_exists('crypt')) {
            trigger_error("Crypt must be loaded for password_verify to function", E_USER_WARNING);
            return false;
        }
        $ret = crypt($password, $hash);
        if (!is_string($ret) || strlen($ret) != strlen($hash) || strlen($ret) <= 13) {
            return false;
        }

        $status = 0;
        for ($i = 0; $i < strlen($ret); $i++) {
            $status |= (ord($ret[$i]) ^ ord($hash[$i]));
        }

        return $status === 0;
    }

}

0

パスワードの検証やパスワードの作成、たとえばMySQLデータベースへの保存に常に使用する関数を作成しました。ランダムに生成されたソルトを使用します。これは、静的ソルトを使用するよりもはるかに安全です。

function secure_password($user_pwd, $multi) {

/*
    secure_password ( string $user_pwd, boolean/string $multi ) 

    *** Description: 
        This function verifies a password against a (database-) stored password's hash or
        returns $hash for a given password if $multi is set to either true or false

    *** Examples:
        // To check a password against its hash
        if(secure_password($user_password, $row['user_password'])) {
            login_function();
        } 
        // To create a password-hash
        $my_password = 'uber_sEcUrE_pass';
        $hash = secure_password($my_password, true);
        echo $hash;
*/

// Set options for encryption and build unique random hash
$crypt_options = ['cost' => 11, 'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM)];
$hash = password_hash($user_pwd, PASSWORD_BCRYPT, $crypt_options);

// If $multi is not boolean check password and return validation state true/false
if($multi!==true && $multi!==false) {
    if (password_verify($user_pwd, $table_pwd = $multi)) {
        return true; // valid password
    } else {
        return false; // invalid password
    }
// If $multi is boolean return $hash
} else return $hash;

}

6
saltパラメータを省略することをお勧めします。ベストプラクティスに従って、password_hash()関数によって自動的に生成されます。代わりに、将来のプルーフコードを書くPASSWORD_BCRYPTために使用できPASSWORD_DEFAULTます。
martinstoeckli 2017

そのアドバイスをありがとう。私はドキュメントでそれを監督したに違いありません。長い夜でした。
Gerrit Fries

1
secure.php.net/manual/en/function.password-hash.phpによると、「saltオプションはPHP 7.0.0で非推奨になりました。現在、デフォルトで生成されるsaltを使用することが推奨されています。」
jmng 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.