はじめに
Laravel Advent Calendar 2017 22日目の記事です。
Laravelerにおなじみ?、最初にやらされる php artisan key:generate
。それで .env
に APP_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:generate
は Illuminate\Foundation\Console\KeyGenerateCommand
というartisan command用のクラスが実行されます。 (ディレクトリでいうと verdor/laravel/framework/src/illuminate/Foundation/Console/
内にあります。)
このクラスにて、 APP_KEY
の値が生成され、 .env
ファイルに追加されているようです。基本的にファイルへの書き込み用のロジックがかかれていますが、生成部分は下記の通り。
protected function generateRandomKey()
{
return 'base64:'.base64_encode(
Encrypter::generateKey($this->laravel['config']['app.cipher'])
);
}
Encrypter
にてKeyをを生成、base64方式でエンコードしています。configのapp.cipher
にはデフォルトではAES-256-CBC
(暗号化アルゴリズムの種類) が設定されています。 Encrypter
では
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, という感じでコンテナに登録されています。)
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
というプロパティで登録されます。そして実際にはこのように使用されます。
public function createNewToken()
{
return hash_hmac('sha256', Str::random(40), $this->hashKey);
}
hash_hmac
は HMAC方式を使用してハッシュ値を生成する組み込み関数
です。 第三引数は秘密鍵にあたるので、 token 生成の際の秘密鍵として利用されていたのですね。
このtokenは PasswordBroker
にてパスワードをリセットする時に発行される Tokenなどとして利用されるようです。
APP_KEY の使用され方 (Encrypter編)
しかし他にもReadoubleには
アプリケーションキーが設定されていなければ、ユーザーセッションや他の暗号化済みデーターは安全ではありません!
と書かれています。さすがに上記箇所だけではないはず。
そこで実際、暗号化周りのクラスを見てみましょう。
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を見て見ましょう。
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
でも) 利用されています。
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編)
の追加と構成の一部を修正しました。