[Laravel]Encrypterクラス(encrypt・decryptメソッド)を解析してみた

Laravel

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を暗号化します。

渡すパラメータは下記になります。

1. 暗号化する値(メソッドのserializeがtrueだった場合はserializeされた値)
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_decodebase64の値からjson形式に戻します。
その後にjson_decodejson形式から配列に戻します。

暗号化した内容を少しずつ複合化してますね。

$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で複合化していてパラメータは下記になります。

1. 暗号化した値
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の暗号化方式などについて理解が浅いので、なんだか理解できていない感が残ります。
暗号化方式についても、わかるように学習していきたいところです。

コメント

タイトルとURLをコピーしました