PHP
laravel
laravel5.5
LaravelDay 22

[Laravel5.5] APP_KEY の行方を追う

はじめに

Laravel Advent Calendar 2017 22日目の記事です。

Laravelerにおなじみ?、最初にやらされる php artisan key:generate 。それで .envAPP_KEY=base64:xxxxxxx が埋まりますよね。自分はLaravel5くらいから使っているんですが、あれって具体的に何に使われているんだろうと思って、今回調べてみました!

[注] composer でインストールすれば自動的に実行されてます。php artisan key:generate

まずはドキュメント

何事もまずはドキュメントを確認してみましょう。
Laravelerにはおなじみ Readouble には key:generate に関して主に "インストール"と、"暗号化"項目にて触れられています。

インストール

次にインストール後に行うべきなのは、アプリケーションキーにランダムな文字列を設定することです。ComposerかLaravelインストーラを使ってインストールしていれば、php artisan key:generateコマンドにより、既に設定されています。
通常、この文字列は32文字にすべきです。キーは.env環境ファイルに設定されます。もし、.env.exampleファイルをまだ.envにリネームしていなければ、今すぐ行ってください。アプリケーションキーが設定されていなければ、ユーザーセッションや他の暗号化済みデーターは安全ではありません!

暗号化

Laravelのエンクリプタを使用する準備として、config/app.php設定ファイルのkeyオプションをセットしてください。php artisan key:generateコマンドを使用し、このキーを生成すべきです。このArtisanコマンドはPHPの安全なランダムバイトジェネレータを使用し、キーを作成します。この値が確実に指定されていないと、Laravelにより暗号化された値は、すべて安全ではありません。

ふむふむ、どうやら暗号化の際に使われているようですね。LaravelのSessionやAuth機能などで使われているのでしょうか。

APP_KEY の生成

次は php artisan key:generate を中身を見て見ましょう。

APP_KEY でgrepすればわかりますが、 key:generateIlluminate\Foundation\Console\KeyGenerateCommand というartisan command用のクラスが実行されます。 (ディレクトリでいうと verdor/laravel/framework/src/illuminate/Foundation/Console/ 内にあります。)

このクラスにて、 APP_KEY の値が生成され、 .env ファイルに追加されているようです。基本的にファイルへの書き込み用のロジックがかかれていますが、生成部分は下記の通り。

KeyGenerateCommand.php
protected function generateRandomKey()
{
    return 'base64:'.base64_encode(
        Encrypter::generateKey($this->laravel['config']['app.cipher'])
    );
}

Encrypter にてKeyをを生成、base64方式でエンコードしています。configのapp.cipherにはデフォルトではAES-256-CBC(暗号化アルゴリズムの種類) が設定されています。 Encrypterでは

Encrypter.php
public static function generateKey($cipher)
{
    return random_bytes($cipher == 'AES-128-CBC' ? 16 : 32);
}

という風に生成されています。 random_bytes はphp7から追加された、ランダムバイトを生成する関数です。(技術自体はphp5.xからでも使えたみたいです。)
つまり実際は base64_encode(random_bytes(32)) が実行されています。
実際に実行してみたら nnxsf4WwqhHKhwgx7sLjwhEl0DqsBUMpffCmTlvh+CE= このように無事出力されました。

APP_KEY の使用され方 (PasswordBroker編)

そして、実際に生成されたAPP_KEYがどのように使われているかを確認してみます。
基本 config/app.php 内で 'key' => env('APP_KEY') と格納されているので、 config('app.key') もしくは ['config']['app.key'] などと検索してみて使われてない場所がないかなーと探してみます。

そして引っかかるのが、 Illuminate\Auth\Passwords\PasswordBrokerManager.php 。このクラスはPasswordBroker を生成するFactoryクラスのようです。

(ちなみに上記のKeyGenerateCommand.php以外にはここしか引っかかりませんでした。)
(また 'auth.password' => PasswordBrokerManager, 'auth.password.broker' => PasswordBroker, という感じでコンテナに登録されています。)

PasswordBrokerManager.php
protected function resolve($name)
{
    $config = $this->getConfig($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Password resetter [{$name}] is not defined.");
    }

    // The password broker uses a token repository to validate tokens and send user
    // password e-mails, as well as validating that password reset process as an
    // aggregate service of sorts providing a convenient interface for resets.
    return new PasswordBroker(
        $this->createTokenRepository($config),
        $this->app['auth']->createUserProvider($config['provider'] ?? null)
    );
}

// doc略
protected function createTokenRepository(array $config)
{
    $key = $this->app['config']['app.key'];

    if (Str::startsWith($key, 'base64:')) {
        $key = base64_decode(substr($key, 7));
    }

    $connection = $config['connection'] ?? null;

    return new DatabaseTokenRepository(
        $this->app['db']->connection($connection),
        $this->app['hash'],
        $config['table'],
        $key,
        $config['expire']
    );
}

APP_KEYは デコードされ Illuminate\Auth\Passwords\DatabaseTokenRepository のコンストラクタ第四引数として設定されていますね。

DatabaseTokenRepositoryAPP_KEY$hashKeyというプロパティで登録されます。そして実際にはこのように使用されます。

DatabaseTokenRepository.php
public function createNewToken()
{
    return hash_hmac('sha256', Str::random(40), $this->hashKey);
}

hash_hmacHMAC方式を使用してハッシュ値を生成する組み込み関数 です。 第三引数は秘密鍵にあたるので、 token 生成の際の秘密鍵として利用されていたのですね。

このtokenは PasswordBroker にてパスワードをリセットする時に発行される Tokenなどとして利用されるようです。

APP_KEY の使用され方 (Encrypter編)

しかし他にもReadoubleには

アプリケーションキーが設定されていなければ、ユーザーセッションや他の暗号化済みデーターは安全ではありません!

と書かれています。さすがに上記箇所だけではないはず。

そこで実際、暗号化周りのクラスを見てみましょう。

Illuminate/Encryption/Encrypter.php
class Encrypter implements EncrypterContract
{
    // doc 略
    protected $key;

    protected $cipher;

    public function __construct($key, $cipher = 'AES-128-CBC')
    {
        $key = (string) $key;

        if (static::supported($key, $cipher)) {
            $this->key = $key;
            $this->cipher = $cipher;
        } else {
            throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
        }
    }

    // 実装略
}

このEncypterクラスは、コンテナにencypterというエイリアスでbindされていて、主にSession、Cookie (csrf tokenなど)などで利用されています。helper encrypt() や Facade Crypt でも利用されています。つまりLaravelの主要な機能で用いられる暗号化を担うクラスです。

プロパティ $key があやしいですね。bindしているProviderを見て見ましょう。

Illuminate/Encryption/EncryptionServiceProvider.php
class EncryptionServiceProvider extends ServiceProvider
{
    // doc 略
    public function register()
    {
        $this->app->singleton('encrypter', function ($app) {
            $config = $app->make('config')->get('app');

            // If the key starts with "base64:", we will need to decode the key before handing
            // it off to the encrypter. Keys may be base-64 encoded for presentation and we
            // want to make sure to convert them back to the raw bytes before encrypting.
            if (Str::startsWith($key = $this->key($config), 'base64:')) {
                $key = base64_decode(substr($key, 7));
            }

            return new Encrypter($key, $config['cipher']);
        });
    }

    protected function key(array $config)
    {
        return tap($config['key'], function ($key) {
            if (empty($key)) {
                throw new RuntimeException(
                    'No application encryption key has been specified.'
                );
            }
        });
    }
}

見つけました。 $app['config']['app.key'] のように取得されていないので、検索では引っかかりませんでしたが、
$config = $app->make('config')->get('app');config/app.php の内容を取得し、 $this->key($config);$config['key'] = APP_KEY の内容を取得して Encrypter に入れていますね。

脇にそれますが、 tap($value, $callback) は5.5から追加されたhelperで、$valueに対して$callbackを実行するというものです。

このようにAPP_KEYを注入された Encrypterhashencrypt メソッドにて (もちろんdecryptでも) 利用されています。

Illuminate/Encryption/Encrypter.php
class Encrypter implements EncrypterContract
{
    // doc / 一部実装略
    protected $key;

    protected $cipher;

    public function encrypt($value, $serialize = true)
    {
        $iv = random_bytes(openssl_cipher_iv_length($this->cipher));
        // First we will encrypt the value using OpenSSL. After this is encrypted we
        // will proceed to calculating a MAC for the encrypted value so that this
        // value can be verified later as not having been changed by the users.
        $value = \openssl_encrypt(
            $serialize ? serialize($value) : $value,
            $this->cipher, $this->key, 0, $iv
        );
        if ($value === false) {
            throw new EncryptException('Could not encrypt the data.');
        }
        // Once we get the encrypted value we'll go ahead and base64_encode the input
        // vector and create the MAC for the encrypted value so we can then verify
        // its authenticity. Then, we'll JSON the data into the "payload" array.
        $mac = $this->hash($iv = base64_encode($iv), $value);
        $json = json_encode(compact('iv', 'value', 'mac'));
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new EncryptException('Could not encrypt the data.');
        }
        return base64_encode($json);
    }

    protected function hash($iv, $value)
    {
        return hash_hmac('sha256', $iv.$value, $this->key);
    }
}

終わりに

このように APP_KEY は暗号化やパスワードリセットといった、セキュリティ的に最重要な箇所で利用されています。アプリケーションのリリース前にはセットし忘れていないか一度確認してみてください。

18/5/10
# APP_KEY の使用され方 (Encrypter編) の追加と構成の一部を修正しました。