Help us understand the problem. What is going on with this article?

[メモ] パスワードのハッシュを強力にする

More than 5 years have passed since last update.

PHP 5.5からパスワードのハッシュ化用の関数が追加されています。

http://www.php.net/manual/ja/function.password-hash.php
http://www.php.net/manual/ja/function.password-verify.php

これにより、単一のハッシュ関数より強力なハッシュ化が行えるようになります。

ただ、ディストリビューションの関係でPHP5.3系の環境が多いと思います。
そのような環境ではpassword_compatというライブラリで上記関数をエミュレートすることが出来ます。

https://github.com/ircmaxell/password_compat
(5.3.7以降が必要)

RHEL6.4系列のyumでは5.3.3ですが、上記ライブラリで必要な修正がバックポートされているようなので、試してみるのも良いかと思います。

追記

5.3.0以降であれば、crypt関数を使うのも良さそうです。
http://www.php.net/manual/en/function.crypt.php

saltに「\$2a\$<2桁コスト数値>\$<21桁の「./0-9A-Za-z」の文字列>」を指定する事によって上記のpassword-hashより劣るが、Blowfishハッシュが作成出来る。
5.3.7以降であれば「\$2y\$」も使えるので、強度的にpassword-hashと同等のBlowfishハッシュが作成出来る。

ただし、パスワードとハッシュの比較ロジックを書くのが少し面倒です。
ハッシュは「<saltの値>.<hash値>」になるので、saltをハッシュから取得し、パスワードを取得したsaltでハッシュ化した文字列を比較する必要があります。

例:いま作っているアプリケーションのクラス

Password.php
<?php
/**
 * パスワードハッシュの機能を提供するクラス
 */
class Password
{
    const MODE_INTERNAL = 'internal';
    const MODE_HIGH = '$2y$';
    const MODE_NORMAL = '$2a$';
    const MODE_LOW = '$1$';

    private $mode;
    private $cost = 10;

    public function __construct()
    {
        if (function_exists('password_hash')) {
            $this->mode = self::MODE_INTERNAL;
        } else if (defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH === 1) {
            if (version_compare(PHP_VERSION, '5.3.7', '>=')) {
                $this->mode = self::MODE_HIGH;
            } else {
                $this->mode = self::MODE_NORMAL;
            }
        } else {
            $this->mode = self::MODE_LOW;
        }
    }

    public function setCost($cost)
    {
        if (is_int($cost)) {
            $this->cost = $cost;
        } else {
            throw new \InvalidArgumentException(__METHOD__ . 'only accepts integers. Input was: ' . $cost);
        }
    }

    public function setMode($mode)
    {
        switch (strtolower($mode)) {
            case self::MODE_INTERNAL:
                if (function_exists('password_hash')) {
                    $this->mode = $mode;
                } else {
                    throw new \RuntimeException('Not declared function "password_hash"');
                }
                break;
            case self::MODE_HIGH:
            case self::MODE_NORMAL:
            case self::MODE_LOW:
                $this->mode = $mode;
                break;
            default:
                throw new \InvalidArgumentException('Not found crypt mode "' . $mode . '"');
        }
    }

    private function random($length)
    {
        return substr(strtr(base64_encode(hash('sha256', mt_rand())), '+', '.'), 0, $length);
    }

    public function hash($password)
    {
        switch ($this->mode) {
            case self::MODE_INTERNAL:
                return password_hash($password, PASSWORD_DEFAULT, array('cost' => $this->cost));
                break;
            case self::MODE_HIGH:
            case self::MODE_NORMAL:
                $cost = substr(str_pad($this->cost, 2, '0', STR_PAD_LEFT), -2);
                $salt = $this->mode . $cost . '$' . $this->random(21);
                return crypt($password, $salt);
                break;
            case self::MODE_LOW:
                $salt = $this->mode . $this->random(8);
                return crypt($password, $salt);
                break;
        }
    }

    public function verify($password, $hash)
    {
        switch ($this->mode) {
            case self::MODE_INTERNAL:
                return password_verify($password, $hash);
                break;
            case self::MODE_HIGH:
            case self::MODE_NORMAL:
                if (preg_match('/^(\$2[ay]\$)(\d+)\$(.{21})/', $hash, $matches)) {
                    $mode = $matches[1];
                    $cost = substr(str_pad(intval($matches[2]), 2, '0', STR_PAD_LEFT), -2);
                    $salt = $matches[3];
                    $checkSalt = $mode . $cost . '$' . $salt;
                    $checkHash = crypt($password, $checkSalt);

                    return ($hash === $checkHash);
                }
                return false;
                break;
            case self::MODE_LOW:
                if (preg_match('/^(\$1\$)(.{8})/', $hash, $matches)) {
                    $mode = $matches[1];
                    $salt = $matches[2];
                    $checkSalt = $mode . $salt;
                    $checkHash = crypt($password, $checkSalt);

                    return ($hash === $checkHash);
                }
                return false;
                break;
        }
    }

    public function isNeedRehash($hash)
    {
        // チェック(internalの場合)
        if ($this->mode === self::MODE_INTERNAL) {
            return password_needs_rehash($hash, PASSWORD_DEFAULT, array('cost' => $this->cost));
        }

        // アルゴリズムのチェック
        if ($this->mode === self::MODE_LOW && substr($hash,0, 3) !== self::MODE_LOW) {
            // MD5ハッシュだが、他のハッシュ方式になっている。
            return true;
        }
        if ($this->mode === self::MODE_NORMAL && substr($hash,0, 4) !== self::MODE_NORMAL) {
            // Blowfishハッシュだが、他のハッシュ方式になっている。
            return true;
        }
        if ($this->mode === self::MODE_HIGH && substr($hash,0, 4) !== self::MODE_HIGH) {
            // Blowfishハッシュだが、他のハッシュ方式になっている。
            return true;
        }

        // コストのチェック
        switch ($this->mode) {
            case self::MODE_NORMAL:
            case self::MODE_HIGH:
                $cost = intval(substr($hash, 4, 2));
                return ($this->cost !== $cost);
                break;
        }

        return false;
    }
}
sample.php
<?php

$p = new Password();
// PHP 5.3.0~5.3.6では$2a$、5.3.7以降では$2y$でハッシュ化
$hash = $p->hash('password');

// ハッシュの比較
$p->verify('password', $hash);
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした