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

# はじめに
[Laravel Advent Calendar 2017](https://qiita.com/advent-calendar/2017/laravel) 22日目の記事です。

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

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

# まずはドキュメント

何事もまずはドキュメントを確認してみましょう。
Laravelerにはおなじみ [Readouble](https://readouble.com/laravel/) には `key:generate` に関して主に "インストール"と、"暗号化"項目にて触れられています。
#### インストール
> 次にインストール後に行うべきなのは、アプリケーションキーにランダムな文字列を設定することです。ComposerかLaravelインストーラを使ってインストールしていれば、php artisan key:generateコマンドにより、既に設定されています。
通常、この文字列は３２文字にすべきです。キーは.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:generate` は `Illuminate\Foundation\Console\KeyGenerateCommand` というartisan command用のクラスが実行されます。 (ディレクトリでいうと `verdor/laravel/framework/src/illuminate/Foundation/Console/` 内にあります。)

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

```php:KeyGenerateCommand.php
protected function generateRandomKey()
{
    return 'base64:'.base64_encode(
        Encrypter::generateKey($this->laravel['config']['app.cipher'])
    );
}
```
`Encrypter` にてKeyをを生成、[base64](https://ja.wikipedia.org/wiki/Base64)方式でエンコードしています。configの`app.cipher`にはデフォルトでは`AES-256-CBC`(暗号化アルゴリズムの種類) が設定されています。 `Encrypter`では

```php:Encrypter.php
public static function generateKey($cipher)
{
    return random_bytes($cipher == 'AES-128-CBC' ? 16 : 32);
}
```
という風に生成されています。 `random_bytes` はphp7から追加された、ランダムバイトを生成する関数です。([技術自体](https://github.com/paragonie/random_compat)は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, という感じでコンテナに登録されています。)

```php: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` のコンストラクタ第四引数として設定されていますね。

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

```php:DatabaseTokenRepository.php
public function createNewToken()
{
    return hash_hmac('sha256', Str::random(40), $this->hashKey);
}
```
[`hash_hmac`](http://php.net/manual/ja/function.hash-hmac.php)は `HMAC方式を使用してハッシュ値を生成する組み込み関数` です。 第三引数は秘密鍵にあたるので、 token 生成の際の秘密鍵として利用されていたのですね。

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

# APP_KEY の使用され方 (Encrypter編)

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

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

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

```php: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を見て見ましょう。

```php: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`を注入された `Encrypter` で `hash` や `encrypt` メソッドにて (もちろん`decrypt`でも) 利用されています。

```php: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編)` の追加と構成の一部を修正しました。