PHPのOpenSSL関数を利用して暗号化する例

(Last Updated On: 2021年2月15日)

色々やることがあってブログを更新できていませんでした。久々のブログはPHPのOpenSSL関数を使ってAES-256-CBCを使って暗号化する例です。今時のハードウェアとソフトウェアならハードウェアAESが利用できるので普通はAES-256-CBCで構わないでしょう。

”パスワード”だけで暗号化する例

暗号を利用する場合のポイントは以下の通りです。

  • IV(Initialization Vector ソースでは$iv)にはランダムな「バイト」を利用する。IVに16進数のハッシュ「テキスト」を使うと折角のIVの空間を半分にしてしまいます。IVには毎回ランダムバイトを設定する。
  • 鍵($key)には「バイト」を利用する。IVと同様にハッシュ「テキスト」などを使うと鍵空間が半分になってしまう。
  • 人に256ビットの鍵を要求するのは非現実的なので、パスワードはどんなパスワードでも受け付けるようにする。(勿論、パスワードは長くて強固な方が良い)
  • 同じパスワードで同じ暗号文が作られては困るので、ランダムなSaltを付け加えて毎回異なる暗号文が生成されるようにする。

暗号化後のデータはバイナリで返されるのでBase64でエンコードして取り扱いやすくして返しています。バイナリのままで構わない場合、Base64でエンコード/デコードするのは無駄なので省いて下さい。

これらの要件を満たしたコードは以下のようになります。

<?php
$data = 'cleattext';
$pass = 'mypass';

$encrypted = encrypt($data, $pass);
echo 'Encrypted :' . $encrypted . PHP_EOL;
echo 'Decrypted :' . decrypt($encrypted, $pass) . PHP_EOL;

 /**
  * decrypt AES 256
  *
  * @param data $edata
  * @param string $password
  * @return decrypted data
  */
function decrypt($edata, $password) {
    $data = base64_decode($edata);
    $salt = substr($data, 0, 16);
    $ct = substr($data, 16);

    $rounds = 3; // depends on key length
    $data00 = $password.$salt;
    $hash = array();
    $hash[0] = hash('sha256', $data00, true);
    $result = $hash[0];
    for ($i = 1; $i < $rounds; $i++) {
        $hash[$i] = hash('sha256', $hash[$i - 1].$data00, true);
        $result .= $hash[$i];
    }
    $key = substr($result, 0, 32);
    $iv  = substr($result, 32,16);

    return openssl_decrypt($ct, 'AES-256-CBC', $key, 0, $iv);
  }

/**
 * crypt AES 256
 *
 * @param data $data
 * @param string $password
 * @return base64 encrypted data
 */
function encrypt($data, $password) {
    // Set a random salt
    $salt = openssl_random_pseudo_bytes(16);

    $salted = '';
    $dx = '';
    // Salt the key(32) and iv(16) = 48
    while (strlen($salted) < 48) {
      $dx = hash('sha256', $dx.$password.$salt, true);
      $salted .= $dx;
    }

    $key = substr($salted, 0, 32);
    $iv  = substr($salted, 32,16);

    $encrypted_data = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
    return base64_encode($salt . $encrypted_data);
}

実行例

これを複数回実行すると、毎回異なる暗号文が生成され、正しく復号されていることが分かります。

[yohgaki@dev test-php-script]$ php openssl_crypt.php 
Encrypted :H+VerU584c96QoegR/5zg3S7VHLDpEcHTHK1BbnMAUU=
Decrpyted :cleattext
[yohgaki@dev test-php-script]$ php openssl_crypt.php 
Encrypted :2z+1YCouttQq8Sy9kRmn5VFRX/WO6TnUqTp7PS5j1Io=
Decrpyted :cleattext

IV、鍵、Saltについて

検索してみると、IVや鍵にランダム「テキスト」を利用している例が多かったようです。ランダム「バイト」を使わないと暗号強度を不必要に落としてしまうので注意して下さい。

人が設定するパスワードは脆弱なので、ランダムバイトの秘密のデータと連結して使うと鍵がより強固になります。

$saltを秘密にしておける場合は、$saltを秘密にするようにして利用してください。例えば、特定の暗号データに対する$saltをサーバー側で保存し、復号時に設定できるような場合には$saltを秘密にするとより安全になります。

この例では、鍵($key)を生成する方法にSHA-256を利用していますが、この鍵生成アルゴリズムを秘密にすることでより安全になります。鍵生成のアルゴリズムを知らなければ、攻撃者はパスワードのみでなく、鍵生成のアルゴリズムの推測も行わなければなりません。複数の種類のハッシュ関数を利用する、ハッシュ関数を複数回適用する、IVと同じようにパスワード+秘密のランダムバイトを使ってハッシュ関数を適用するなどの方法が考えられます。AES-256は鍵サイズが256ビットなので鍵空間が小さくならないよう注意してください。例えば、128ビットのハッシュであるMD5で単純にMD5ハッシュを生成しSHA-256ハッシュを生成するなど、とすると鍵空間を小さくしてしまいます。

弱い鍵(ユーザーが入力したパスワードは非常に弱い鍵)を強い鍵にする方法も紹介しました。弱い鍵を使う必要がある場合、こちらを参考にしてください。

鍵の管理にはFS/PFSの概念も欠かせません。

IVやSaltを保存/共有できる場合の例

上の例はAESのIVや鍵導出のsaltがない場合かつ簡単に共有できない場合の暗号化例です。次の例はAESの$iv、鍵導出の$saltを保存/共有できる場合の例です。前の物よりコードがシンプルになります。

HKDFは鍵情報にタイムスタンプや鍵バージョンの情報を付与する$info引数があります。この例では$infoも指定できるようにしています。

前の例はバイナリの結果となることを避ける為に、暗号化関数内でBase64エンコードをしていました。今回はバイナリのままにします。

<?php
$data = 'cleattext';
$pass = 'mypass';
$iv   = random_bytes(16);
$salt = random_bytes(32);

$encrypted = encrypt($data, $pass, $iv, $salt);
echo 'Encrypted :' . base64_encode($encrypted) . PHP_EOL;
echo 'Decrypted :' . decrypt($encrypted, $pass, $iv, $salt) . PHP_EOL;

 /**
  * decrypt AES 256
  *
  * @param data $edata
  * @param string $password
  * @param string $iv 16 bytes binary
  * @param string $salt strong random value
  * @param string $info any informational data
  * @return decrypted data
  */
function decrypt($edata, $password, $iv, $salt, $info = '') {
    $key = hash_hkdf('sha256', $password, 0, $info, $salt);    
    return openssl_decrypt($edata, 'AES-256-CBC', $key, 0, $iv);
}

/**
 * crypt AES 256
 *
 * @param data $data
 * @param string $password
 * @param string $iv 16 bytes binary
 * @param string $salt strong random value
 * @param string $info any informational data
 * @return binary encrypted data
 */
function encrypt($data, $password, $iv, $salt, $info = '') {
    $key = hash_hkdf('sha256', $password, 0, $info, $salt);
    $encrypted_data = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
    return $encrypted_data;
}

$passwordが十分に強い場合、$iv、$salt、$infoは公開情報であっても構いません。ただし、$ivと$saltはユーザーが提供したモノを使わないようにします。$infoも重要ですが、セキュリティ的な意味では重要度は低いです。仕様としてユーザーが設定できる、としても構いません。例:解読を許可する時間を設定する等。ただし、この仕様の場合は暗号化データ自体をサーバー側で厳重に保存し、設定した時間をバリデーションしてから平文化して渡さないと意味がないです、念の為。

弱い$passwordの場合、強い$saltであっても簡単に解析されてしまいます。例えば、123456やp@a$$wordといった最悪のパスワードだと即解読されます。入力$passwordが弱いケースを想定する場合、$saltは機密情報です。(導出鍵の$keyを渡して他人に解読を許可する、といった場合)

基本的には$passwordにはrandom_bytes(32)で得られるレベルのエントロピーを持った秘密の値を使うべきです。

例えば、弱い$passwordが想定される場合は次のようにhash_hmac()で秘密の強いsaltを使い強い鍵に変えてからhash_hkdf()で導出鍵を作ると$keyを渡しても、$passwordを解析できなくなります。

function encrypt($data, $password, $iv, $salt, $info = '') {
    $secured = hash_hmac('sha256', $password, 'Very Strong Secret Static Random Salt', true);
    $key = hash_hkdf('sha256', $secured, 0, $info, $salt);
    $encrypted_data = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
    return $encrypted_data;
}

使い方や値によって、どの情報を渡しても構わないのか変わります。よく考えてから使ってください。

実際の使用例 – PHPのCookieセーブハンドラ

Cookieはブラウザに送信されるので、機密情報であるセッション情報は暗号化しなければなりません。以下の例はOpenSSLによる暗号化とHKDFによる鍵管理、HMACによるデータ改竄の検出を行っています。

まとめ

暗号化したデータの安全性を維持するには、鍵が重要です。人間が作るパスワードは非常に弱い鍵なので、できる限り強い鍵にして使う方がよいです。

使う鍵が毎回同じだとリスクが増えます。秘密鍵を直接利用せず、HKDFを利用し導出鍵を利用する方が安全です。HKDFのinfo引数を利用し、FS/PFSを確保すると更に安全性が増します。saltは機密情報である必要がある場合、公開しても構わない場合があります。saltは基本的にramdom_bytes()を利用して生成します。

AESのIVは暗号化するデータが同じデータと同じ鍵であっても異なる結果となるようにします。IVは基本、random_bytes()を用いて生成します。

PHPのhash_hkdf()は必須のsaltが最後のオプションパラメータになっている、良くないAPIになっています。saltは必須かつ強い物であるべきです。注意しましょう。

参考: 素のopenssl関数を使って暗号化している例

投稿者: yohgaki