PHP
CakePHP
Hasher

CakePHPのソースコードを読んでいたら、FallbackPasswordHasherが面白かった

More than 1 year has passed since last update.

AbstractPasswordHasherを継承するクラスに、FallbackPasswordHasher、WeakPasswordHasher、DefaultPasswordHasherがあります。

https://github.com/cakephp/cakephp/blob/master/src/Auth/AbstractPasswordHasher.php

https://github.com/cakephp/cakephp/blob/master/src/Auth/DefaultPasswordHasher.php

https://github.com/cakephp/cakephp/blob/master/src/Auth/FallbackPasswordHasher.php

https://github.com/cakephp/cakephp/blob/master/src/Auth/WeakPasswordHasher.php

なので、この継承関係から推察できることは、ハッシュアルゴリズムを、DefaltとWeak、Fallbackにそれぞれ入れ替えられるということです。
では、AbstractPasswordHasherはどういう責任を持っているか、それに想いを馳せてみると、protected $_defaultConfig = [];のプロパティと、コンストラクタ、abstract public function hash($password)の抽象関数と、abstract public function check($password, $hashedPassword);の抽象関数、public function needsRehash($password)の抽象関数になっています。
"$_defaultConfig"で、ユーザー提供のconfigを保存しているのと、コンストラクタで、configをセットしているのを除けば、実質的にインターフェースの役割になっています。

では、DefaultPasswordHasher、WeakPasswordHasherの責任について想いを馳せて見ます。

両者には、hashのアルゴリズムに相違があると考えられるので、"public function hash($password)"の実装がどう違うのかを比較して見ましょう。

DefaultPasswordHasherの実装は以下の通り。

    public function hash($password)
    {
        return password_hash(
            $password,
            $this->_config['hashType'],
            $this->_config['hashOptions']
        );
    }

WeakPasswordHasherの実装は以下の通り

    public function hash($password)
    {
        return Security::hash($password, $this->_config['hashType'], true);
    }

FallbackPasswordHasherの実装は以下の通り

    public function hash($password)
    {
        return $this->_hashers[0]->hash($password);
    }

DefaultPasswordHasherを使ってhashを生成する場合は、PHPの組み込み関数password_hashを使います。WeakPasswordHasherの場合は、Securityで定義されたハッシュを利用しています。
FallbackPasswordHasherは、「設定された」ハッシュアルゴリズムの、最初のものを使っている。これは、複数個設定可能です。
DefaultPasswordHasherで使用されるhash関数の方が新しいようで、WeakPasswordHasherの解説文には以下のような記述があります。

/**
 * Password hashing class that use weak hashing algorithms. This class is
 * intended only to be used with legacy databases where passwords have
 * not been migrated to a stronger algorithm yet.
 */

(訳)弱いハッシュアルゴリズムを使用するパスワードハッシュクラスである。このクラスは、パスワードがより強いアルゴリズムにマイグレートされていないデータベースのみに使われます。

では、ハッシュが、古いアルゴリズムでできたものか、新しいアルゴリズムでできたものかが区別がつかない場合はどうすればいいでしょうか。そのためにあるのは、FallbackPasswordHasherです。
では、ハッシュをパスワードでチェックするときは、どのようなメソッドを使うだろうか。当然、public function check($password, $hashedPassword)である。では、実装を見てみましょう。

    public function check($password, $hashedPassword)
    {
        foreach ($this->_hashers as $hasher) {
            if ($hasher->check($password, $hashedPassword)) {
                return true;
            }
        }

        return false;
    }

$this->_hashersには、古い方のハッシュアルゴリズムと、新しい方のアルゴリズムが両方格納されていると考えられます。で、両方によりチェックして、どちらかが適合すれば、trueを返してくれます。
両方ともに適合しなかったら、falseを返します。
これでもわかりにくかっただろうか、では、FallbackPasswordHasherTestを見てみれば、わかりやすいかもしれないです。
PHPには、仕様が厳格に守られているかどうかをチェックする自動テストが実装されており、それは、TestCaseを継承することによって使うことができます。
では、テストメソッドを一つ取り上げてみましょう。

    public function testCheck()
    {
        $hasher = new FallbackPasswordHasher(['hashers' => ['Weak', 'Default']]);
        $weak = new WeakPasswordHasher();
        $simple = new DefaultPasswordHasher();

        $hash = $simple->hash('foo');
        $otherHash = $weak->hash('foo');
        $this->assertTrue($hasher->check('foo', $hash));
        $this->assertTrue($hasher->check('foo', $otherHash));
    }

これは、FallbackPasswordHasherで生成したハッシュオブジェクトが、WeakPasswordHasherと、
DefaultPasswordHasherのハッシュアルゴリズム両方で生成したハッシュに対して、checkすると、同じようにtrueを返すことをチェックしていることがわかります。
$this->assertTrue()関数は、$this->assertTrue(true)の時テストを正常にパスし、$this->assertTrue(false)の時、異常を知らせます。
ということで、これからもソースコード読んでいこうかと思います。