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

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

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

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

```php: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;
    }
}
```

```php: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);
```