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

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

More than 1 year has passed since last update.

はじめに

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

yk2220s
PHP, Laravel, React, Swift, AWS, Terraform, Golang, Python, Rubyなど色々やってます
coubic
人々の生活から、「めんどくさい」をなくす。
https://coubic.com
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
ユーザーは見つかりませんでした