LaravelのEncrypterクラスを解析してみた
Laravelの暗号化(encrypt)・複合化(decrypt)メソッドの中身を解析してみました。
ことの始まりは、別のシステム(PHPだけで出来ている)で暗号化・複合化する必要があったため、同じようにしたくてソースを解析しました。
下記のソースの中身の解説と、使われているPHP関数の解説を少ししています。
標準関数は公式ドキュメントにリンクしています。
vendor/laravel/framework/src/Illuminate/Encryption/Encrypter.php
暗号化(encrypt)メソッド
encryptは値を渡したら暗号化してくれるメソッドです、ソースを開いて流れを追ってみました。
/**
* Encrypt the given value.
*
* @param mixed $value
* @param bool $serialize
* @return string
*
* @throws \Illuminate\Contracts\Encryption\EncryptException
*/
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);
}
解説
メソッドの引数
メソッドに渡されるのは$valueと$serialize(trueを初期値で設定)です。
public function encrypt($value, $serialize = true)
暗号化のためにランダムバイト列を作成する
最初にrandom_bytesを呼んでいる箇所です。
$this->cipherには暗号化方式が設定されています。(AES-128-CBC
など)
これをopenssl_cipher_iv_lengthに渡して、暗号初期化ベクトルの長さを返しています。
暗号化方式によって固定の長さが返ってきます。 openssl_cipher_iv_length(‘AES-128-CBC’); だと16になります。
そして取得した長さを使用して、random_bytesでランダムなバイト列を取得しています。
取得したものを下記のように$ivにセットしていますね。
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
random_bytesメソッドはphp7のみでしか使えないメソッドです。
暗号化する
次に実際に渡した値を暗号化している箇所になります。
$value = \openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher, $this->key, 0, $iv
);
openssl_encryptを使用して、メソッドに渡された$valueを暗号化します。
渡すパラメータは下記になります。
2. 暗号化方式(暗号化方式の文字列)
3. 暗号化キー(暗号化キーの文字列、ここではenvのAPP_KEYが渡される。)
4. 0 (ドキュメントをみるとOPENSSL_RAW_DATA と OPENSSL_ZERO_PADDING のらしいですが… よくわからないです…😫)
5. 初期化ベクター(上記で作成した$iv(ランダムバイト列)を渡しています。)
暗号化した値を$valueにセットしています。
暗号化が失敗していたら、falseが返ってきます。失敗していた場合は下記のように例外を投げています。
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
ハッシュ値を取得する
そして、次に作成したランダム文字列をbase64_encodeしたものと、$valueを同じクラス内のhashメソッドに渡します。
$mac = $this->hash($iv = base64_encode($iv), $value);
hashメソッドはこちらです。
hash_hmac関数を使って、sha256アルゴリズムを指定して、渡した値と、キーを使ってハッシュ値を作成しています。
hashメソッド
protected function hash($iv, $value)
{
return hash_hmac('sha256', $iv.$value, $this->key);
}
作成したハッシュ値を返して、$macにセットします。
返り値を作成して、返す
最後に作成した$iv(ランダムなバイト列), $mac(作成されたhash値)と引数の$valueをjson_encodeでjson形式にして、$jsonにセットしています。
その後にbase64_encodeして、呼び出し元に返しています。
$json = json_encode(compact('iv', 'value', 'mac'));
return base64_encode($json);
つまり最終的にはbase64_encodeした文字列が返ってくるんですね。。
複合化(decrypt)メソッド
暗号化の流れが掴めれば、複合化の流れもなんとなくわかります。
/**
* Decrypt the given value.
*
* @param string $payload
* @param bool $unserialize
* @return mixed
*
* @throws \Illuminate\Contracts\Encryption\DecryptException
*/
public function decrypt($payload, $unserialize = true)
{
$payload = $this->getJsonPayload($payload);
$iv = base64_decode($payload['iv']);
// Here we will decrypt the value. If we are able to successfully decrypt it
// we will then unserialize it and return it out to the caller. If we are
// unable to decrypt this value we will throw out an exception message.
$decrypted = \openssl_decrypt(
$payload['value'], $this->cipher, $this->key, 0, $iv
);
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
return $unserialize ? unserialize($decrypted) : $decrypted;
}
解説
メソッドの引数
メソッドには$payload(暗号化文字列:base64_encodeしたもの)と、$unserialize(trueを初期値で設定)が渡されます。
public function decrypt($payload, $unserialize = true)
暗号化したjsonを配列に戻す
まずはgetJsonPayLoadを呼び出して、暗号化文字列を渡します。
getJsonPayLoadの中身はこんな感じになっています。
getJsonPayLoadメソッド
protected function getJsonPayload($payload)
{
$payload = json_decode(base64_decode($payload), true);
// If the payload is not valid JSON or does not have the proper keys set we will
// assume it is invalid and bail out of the routine since we will not be able
// to decrypt the given value. We'll also check the MAC for this encryption.
if (! $this->validPayload($payload)) {
throw new DecryptException('The payload is invalid.');
}
if (! $this->validMac($payload)) {
throw new DecryptException('The MAC is invalid.');
}
return $payload;
}
base64_decodeをした後にjson_decodeをして、その結果を返しています。
base64_decodeでbase64の値からjson形式に戻します。
その後にjson_decodeでjson形式から配列に戻します。
暗号化した内容を少しずつ複合化してますね。
$payloadという変数で、配列まで複合化した内容を受け取って返しています。
詳細のメソッドは割愛しますが、$payloadが有効なのかをvalidPayloadメソッドで判定して、$payloadの中のmacが有効なのかをvalidMacで判定していますね。
無効だった場合は例外を投げています。
配列のivをバイトデータに戻す
その後に下記のように$payload[‘iv’]で初期化ベクター(ランダムバイト列)を取り出して、base64_decodeでバイトデータに戻して、$ivにセットしています。
$iv = base64_decode($payload['iv']);
暗号化した値の複合化
そのあとは、下記で暗号化した値を複合化しています。
暗号化の値は$payload[‘value’]に入っています。
$decrypted = \openssl_decrypt(
$payload['value'], $this->cipher, $this->key, 0, $iv
);
openssl_decryptで複合化していてパラメータは下記になります。
2. 暗号化方式(暗号化方式の文字列)
3. 暗号化キー(暗号化キーの文字列、ここではenvのAPP_KEYが渡される。)
4. 0 (ドキュメントではOPENSSL_RAW_DATAまたはOPENSSL_ZERO_PADDING… なんのオプションかよくわからない…)
5. 暗号化時のランダムバイトデータを複合化して渡している
複合化できなかった場合は、falseが返ってくるのでその場合は、例外を投げていますね。
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
成功した場合は複合化した値を$decryptedにセットして、
return $unserialize ? unserialize($decrypted) : $decrypted;
$unserializeがtrueだったらunserializeして、そうでなかったら複合化した値を返しています。
基本的にメソッドには$valueだけ渡して使うので、毎回unserializeされていることになりますね。
終わりに
Encrypterクラスのencryptメソッドとdecryptメソッドの中身を解析してみました。
簡単にまとめると、暗号化は暗号化したい値を渡したら、暗号化のために$iv(ランダムバイトデータ)を作成し、openssl_encryptで暗号化しました。
その後に$mac(ハッシュ値)を作成して、$iv(バイトデータ)、$value(暗号化した値)、$mac(ハッシュ値)をjson形式にした後にさらに、base64にして返していました。
複合化は逆に、渡された値をbase64から複合化して、json形式から配列に戻します。
その後に、暗号化した時のiv(バイトデータ)と$value(暗号化した値)などを使って、openssl_decryptを使って複合化しました。
流れはつかめたものの、hmacやopenssl_decryptの暗号化方式などについて理解が浅いので、なんだか理解できていない感が残ります。
暗号化方式についても、わかるように学習していきたいところです。
コメント