🎉

PHP OpenSSL によるシェルコマンドベースの暗号化

2022/08/25に公開

目的

PHPには、暗号化や複合化を容易に行うことができる組み込みの関数が存在するが、PHPのバージョンによってサポートしているOpenSSLのバージョンは異なり、サポートしていないバージョンを使用すると、Webサーバーへの攻撃に対しての脆弱性をかかえてしまうことが、公式ドキュメントに警告として上がっている。
https://www.php.net/manual/ja/openssl.requirements.php
しかし、別のアプリケーションのバージョンアップの影響やサーバー移転に伴うバージョンアップなどで、PHPやOpenSSLなどのバージョンが急に変わってしまった場合、組み込みの関数(openssl_encryptやopenssl_decrypt)ではエラーが発生してしまうことがある。
そこで、シェルベースの暗号化を手段として持っておくことで、急な状況にも対応する手段の1つとして参考にしてもらえると良い。

環境

PHP7.4で試しましたが、PHP5.6以上であれば問題ないかとおもいます。

PHP組み込みのOpenSSL関数を使った例

初期化ベクトル($iv)は、疑似乱数を使っているので、暗号化の結果は実行するたびに異なりますが、複合結果は同じです。

あと、KEYの部分は、デモのものなら公開しても良いが、運用で使うものなら、設定ファイルとかに記述して、中身を隠しておいた方が良いと思います。

<?php

define('KEY','Lk5Uz3slx3BrAghS1aaW5AYgWZRV0tIX5eI0yPchFz4=');

class Encrypt {
    public $rawdata = "";
    public $output = "";
    public $iv = "";
    public $method = "aes-256-cbc";
    
    function secured_encrypt($data)
    {
        $key = base64_decode(KEY);

        $iv_length = openssl_cipher_iv_length($this->method);
        $this->iv = openssl_random_pseudo_bytes($iv_length);
       
        $encrypted = openssl_encrypt($data,$this->method,$key, OPENSSL_RAW_DATA ,$this->iv);
           
        $output = base64_encode($encrypted); 
        
        $this->output = $output;
    }
    
    function secured_decrypt($input)
    {
        $key = base64_decode(KEY);
        $encrypted = base64_decode($input);
         
        $iv_length = openssl_cipher_iv_length($this->method);
           
        $data = openssl_decrypt($encrypted,$this->method,$key,OPENSSL_RAW_DATA,$this->iv);

        $this->raw_data = $data;
    }
}

$input = "Please Encrype Me!!!!";

$encryptHandler = new Encrypt();

$encryptHandler->secured_encrypt($input);
$output = $encryptHandler->output;


$encryptHandler->secured_decrypt($output);
$raw_data = $encryptHandler->raw_data;

print("入力: $input").PHP_EOL;
print("暗号 $output").PHP_EOL;
print("複合 $raw_data").PHP_EOL;

出力

入力: Please Encrype Me!!!!
暗号 85Lsu+ah7sEtE4dSaw9Rokbh5I9D301A9ZFnXM3jN1w=
複合 Please Encrype Me!!!!

シェルコマンドを呼び出して暗号化するパターン

<?php

define('KEY','Lk5Uz3slx3BrAghS1aaW5AYgWZRV0tIX5eI0yPchFz4=');

class ShellEncrypt {
    public $rawdata = "";
    public $output = "";
    public $iv = "";
    public $method = "aes-256-cbc";
    
    function secured_encrypt($data)
    {
        $key = base64_decode(KEY);
        $key = bin2hex($key);

        $iv_length = openssl_cipher_iv_length($this->method);
        $this->iv = openssl_random_pseudo_bytes($iv_length);
        $this->iv = bin2hex($this->iv);

        // echoコマンドを実行前に、ダブルクオテーションを\でエスケープしておく
        $data = str_replace('"', '\"', $data);
        $command = sprintf('echo -n "%s" | openssl enc "-%s" -K "%s" -iv "%s" -base64 -A', $data, $this->method, $key, $this->iv);

        exec($command, $output);
           
        $output = base64_encode($output[0]);
        
        $this->output = $output;
    }
    
    function secured_decrypt($input)
    {
        $key = base64_decode(KEY);
        $key = bin2hex($key);
        $input = base64_decode($input);
         
        $iv_length = openssl_cipher_iv_length($this->method);

        // echoコマンドを実行前に、ダブルクオテーションを\でエスケープしておく
        $data = str_replace('"', '\"', $input);
        $command = sprintf('echo -n "%s" | openssl enc "-%s" -d -K "%s" -iv "%s" -base64 -A', $data, $this->method, $key, $this->iv);

        exec($command, $output);
        $raw_data = $output[0];

        $this->raw_data = $raw_data;
    }
}

$input = "Please Encrype Me!!!!";

$encryptHandler = new ShellEncrypt();

$encryptHandler->secured_encrypt($input);
$output = $encryptHandler->output;


$encryptHandler->secured_decrypt($output);
$raw_data = $encryptHandler->raw_data;

print("入力: $input").PHP_EOL;
print("暗号 $output").PHP_EOL;
print("複合 $raw_data").PHP_EOL;

出力

入力: Please Encrype Me!!!!
暗号 ZEcwOXVDT2dlbWlDRVlVNmM1S0xQSmptZ1BUd1REcnJqZHVGdEExRi9jWT0=
複合 Please Encrype Me!!!!

3系でのレガシーアルゴリズムの暗号化

OpenSSL 3系では、レガシーアルゴリズムとして、MD2, MD4, MDC2, RMD160, CAST5, BF (Blowfish), IDEA, SEED, RC2, RC4, RC5 および DES (3DES は除く) が定義されており、1系と3系でコマンドが少し異なる。
https://sehermitage.web.fc2.com/devel/openssl_v30.html

レガシーアルゴリズムの例

[1系]

$ php -r 'var_dump(openssl_encrypt("test is test", "bf-cbc","a88e2d710bee460c", 0,"11111111"));'

string(24) "s8ok3w2MO+HsisE4OglLog=="

echo -n "test is test" | openssl enc -bf-cbc -K "61383865326437313062656534363063" -iv "3131313131313131" -base64 -A

s8ok3w2MO+HsisE4OglLog==

[3系]
レガシーアルゴリズムを使うと警告が出て暗号化できない

$ php -r 'var_dump(openssl_encrypt("test is test", "bf-cbc","a88e2d710bee460c", 0,"11111111"),openssl_error_string ());'

bool(false)
string(53) "error:0308010C:digital envelope routines::unsupported"

echo -n "test is test" | openssl enc -bf-cbc -K "61383865326437313062656534363063" -iv "3131313131313131" -base64 -A

Error setting cipher BF-CBC
40F7B56E8C7F0000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:crypto/evp/evp_fetch.c:349:Global default library context, Algorithm (BF-CBC : 14), Properties ()

-provider legacy をつけると動作する

$ echo -n "test is test" | openssl enc -bf-cbc -K "61383865326437313062656534363063" -iv "3131313131313131" -base64 -A -provider legacy

レガシーアルゴリズムの対応

そもそもレガシーアルゴリズムを避けるべきであるが、やむを得ない場合は、バージョンによる条件分岐で、後はexec()からopensslコマンドを実行するべし。

exec(
      "openssl version | grep -o -E '([0-9]+\.){1}[0-9]+(\.[0-9]+)?' | head -n 1",
      $openssl_version
    );
    $openssl_version = substr($openssl_version[0], 0, 1);
    
    if ($openssl_version < 3) {
        // 1系の暗号化コマンド
    } else {
        // 3系の暗号化コマンド
    }

まとめ

PHPのバージョンにあうOpenSSLのバージョンを使いながらopenssl_encryptやopenssl_decryptを使うのがベストですが、バージョンを簡単に変更できない環境においては、シェルベースの暗号化もできるということで一例を紹介いたしました。

Discussion