PHP 5.3で共通鍵暗号(openssl)を使用する

5.2以前でも使用できるmcryptについてはこちら

PHP 5.3でopensslモジュールの機能が拡張され、共通鍵暗号による暗号化が利用可能になった。同時に待望の疑似乱数生成器も用意された。これで今までmcryptが使用できなかったWindowsでも、暗号学的強度を持った乱数が生成できる(実際に試していないが、OpenSSLはWindowsのCryptAPIから乱数を得ている)。

まだマニュアルにも詳しい情報が載っていないので、ソースを読みながら使い方を探ってみた。以下はphp-5.3.3のソースに基づく。ただしコミットログを見る限り仕様も機能もまだ安定しているとは言いがたいので、実際に使用するにはもう少し様子を見た方が良いかもしれない。

暗号化する際に決めておくべきこと:

  • 鍵(以下 KEY)… マニュアルでは $password と表記されているが、いわゆる「パスワード」ではない。暗号化に使用する鍵であり、十分にランダムでなければならない。
  • 暗号アルゴリズム+モード(以下 CIPHER)… 利用可能な一覧は openssl_get_cipher_methods から得られる。

暗号化/復号を行う関数 encrypt/decrypt は次のようになる。

//実際の暗号化/復号を行う補助関数
function _callOpenSSL($func, $msg, $iv) {
    $opensslFunc = "openssl_{$func}";
    return $opensslFunc($msg, CIPHER, KEY, true, $iv);
};
 
/**
 * @param string  暗号化したいメッセージ
 * @return array  (暗号文, 初期化ベクトル) の配列
 */
function encrypt($msg)
{
    //初期化ベクトルを生成
    $ivSize = openssl_cipher_iv_length(CIPHER);
    $iv = openssl_random_pseudo_bytes($ivSize, $cryptStrong);
    if($iv === false || !$cryptStrong) {
        // 乱数生成失敗
        return false;
    }
 
    //ダミーの初期化ベクトル
    $dummyIV = str_repeat("x", $ivSize);
 
    //メッセージの暗号化
    $cryptMsg = _callOpenSSL('encrypt', $msg, $iv);
 
    //初期化ベクトルの暗号化
    $cryptIV = _callOpenSSL('encrypt', $iv, $dummyIV);
 
    return array($cryptMsg, $cryptIV);
}
 
/**
 * @param string  暗号文
 * @param string  初期化ベクトル
 * @return string  平文
 */
function decrypt($cryptMsg, $cryptIV)
{
    //ダミーの初期化ベクトルを生成
    $ivSize = openssl_cipher_iv_length(CIPHER);
    $dummyIV = str_repeat("x", $ivSize);
 
    //初期化ベクトルの復号
    $iv = _callOpenSSL('decrypt', $cryptIV, $dummyIV);
 
    //メッセージの復号
    $msg = _callOpenSSL('decrypt', $cryptMsg, $iv);
 
    return $msg;
}
実行:
define('CIPHER', 'aes-128-cbc');
define('KEY', '...暗号化のキー...');
 
$msg = "めっせーじ";
 
// 暗号化
$crypt = encrypt($msg);
var_dump(bin2hex($crypt[0]));
 
// 復号
$plain = decrypt($crypt[0], $crypt[1]);
var_dump($plain);

mcrytと違ってブロック長に合わせたパディング(PKCS#5パディング)はopensslが自動でやってくれるので、自前でbase64エンコードしたりする必要はない。

openssl_random_pseudo_bytes の戻り値は、乱数の生成に失敗した場合は真偽値の false になる。また十分なエントロピーが得られなかった場合は第二引数に指定した変数が false になる(ただ滅多に起こらないはずである:OpenSSL FAQ参照)。

ちなみに鍵の生成も openssl_random_pseudo_bytes で行えばいい。

#コマンドラインにて16バイト=128ビットの乱数を生成
$ php -r 'echo base64_encode(openssl_random_pseudo_bytes(16));
bONq0KiSNIO5ww1ggwdFdQ==

//定数KEYの定義
define('KEY', base64_decode('bONq0KiSNIO5ww1ggwdFdQ=='));

パスワードから鍵を生成する必要がある場合は、自分でPBKDFを実装する必要がある。『PHP PBKDF2』で検索するといくらか実装例が見つかるようだ。またRubyの pkcs5_keyivgen(openssl の EVP_BytesToKey)互換の機能が必要な場合はOpenSSL::Cipher::Cipher#pkcs5_keyivgen の中身などを参照のこと。

openssl_random_pseudo_bytes の中身

実体はOpenSSLの RAND_pseudo_bytes(rand_lib.c) -> ssleay_rand_pseudo_bytes(md_rand.c) -> ssleay_rand_bytes(md_rand.c) で、そこから先は『暗号乱数インフラの初期化処理』を参照のこと。

ssleay_rand_pseudo_bytes と ssleay_rand_bytes の違いは RAND_R_PRNG_NOT_SEEDED を見逃すか否か。エラーがあったこと自体は戻り値で判定できるので、$crypto_strong ではこれを利用している。

Leave a Reply