Edited at
LaravelDay 22

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