「PHPで暗号化・復号あれこれ」の続き
ラボブログの続きですが。。。
今回挙げたサンプルを作る際、復号データの末尾に何か付加されているのは確認していたのですが、base64_encodeするとうまくいったので、今回はそれ以上深く追っていませんでした。また、コメントにある「padding」を見て、パディングのことをすっかり忘れていたことを思い出しました。
mcryptのpaddingは 平文の長さがブロックサイズ倍になるように ASCIIZ('\0')を付けるだけなので, base64化することで末尾のASCIIZを正しく保持する効果があると思われます(試してはいません).
ということで、PEAR::Crypt_Blowfishを使ったサンプルを元に見てみました。以下のコードはラボブログに書いたサンプルからbase64_xxxxxxを抜いて、文字列長やコード出力(面倒なのでurlencodeで代用)を追加したものです。サンプルのファイルエンコーディングはutf-8、実行環境のLANGはja_JP.UTF-8です。
<?php require_once 'Crypt/Blowfish.php'; echo "mcrypt module is " . (extension_loaded("mcrypt") ? "" : "not ") . "loaded\n"; // 暗号化するデータ $data = '小池さんはラーメン大好き'; echo "data : " . $data . "\n"; echo "data len : " . strlen($data) . "\n"; // 暗号化キー $key = 'the key value for crypting'; // CBCモードで暗号化するため、初期化ベクトルを用意する $iv = substr(md5(uniqid(rand(), 1)), 0, 8); // 暗号化処理 $blowfish = Crypt_Blowfish::factory('cbc', $key, $iv); $encrypted_data = $blowfish->encrypt($data); echo "encrypted data : " . base64_encode($encrypted_data) . "\n"; /** * [BK]mcrypt拡張モジュールがロードされている場合、再度インスタンスを * 取得する必要がある(mcrypt_generic_deinitしていないため) */ if (extension_loaded("mcrypt")) { $blowfish = Crypt_Blowfish::factory('cbc', $key, $iv); } // 復号処理 $decrypted_data = $blowfish->decrypt($encrypted_data); if (PEAR::isError($decrypted_data)) { die($decrypted->getMessage() . "\n"); } echo "decrypted data : " . $decrypted_data . "\n"; echo "decrypted data len : " . strlen($decrypted_data) . "\n"; echo "decrypted data urlencode : " . urlencode($decrypted_data) . "\n"; echo 'validate : ' . ($data == $decrypted_data ? 'true' : 'false') . "\n";
実行結果は次のようになります。
$ php crypt_blowfish2.php mcrypt module is not loaded data : 小池さんはラーメン大好き data len : 36 encrypted data : dwZOdVGPQn42y42/gPA7HP9zcYvnzzWpiCahtr7q7JRMQsw3OFxOJw== decrypted data : 小池さんはラーメン大好き decrypted data len : 40 decrypted data urlencode : %E5%B0%8F%E6%B1%A0%E3%81%95%E3%82%93%E3%81%AF%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3%E5%A4%A7%E5%A5%BD%E3%81%8D%00%00%00%00 validate : false $
mcrypt拡張モジュールを使った場合は以下の通りです。
$ php crypt_blowfish2.php mcrypt module is loaded data : 小池さんはラーメン大好き data len : 36 encrypted data : dHDiWn/UOnWd3q7eoofKYwDdSj8hCF8SiNzRl7f7YVJHwKkOmDX6wQ== decrypted data : 小池さんはラーメン大好き decrypted data len : 40 decrypted data urlencode : %E5%B0%8F%E6%B1%A0%E3%81%95%E3%82%93%E3%81%AF%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3%E5%A4%A7%E5%A5%BD%E3%81%8D%00%00%00%00 validate : false $
urlencodeされたデータの最後を見ると分かりますが、確かに\0が追加されています。CBCモードの場合、ブロックサイズは8なので、\0が4つ追加されています。PEAR::Crypt_Blowfishでは、$PEAR/Crypt/Blowfish/CBC.phpのencryptメソッドにある
<?php class Crypt_Blowfish_CBC extends Crypt_Blowfish_PHP { : function encrypt($plainText) { : $plainText .= str_repeat(chr(0), (8 - ($len % 8)) % 8);
の部分がその処理を行っているようです。
ということで、元の文字列と復号した文字列をそのまま「===」で比較するとfalseになり得ますね。。。ということで、
復号したデータの最後に連続している「\0」を削除する
必要があります(文字列に限って言うと)。
いや、元々PHPマニュアルに掲載されたサンプルでは、復号した文字列をtrimしていたので、「何でかなぁ〜」と思っていたんですが、どうやらこのサンプルが正解。。。じゃないですね。元データの先頭・末尾が半角スペースの場合、trimだとそれらの半角スペースも削除されてしまいます。なので、rtrimを使い、第2引数で「\0」を指定してやる必要があります。
修正したサンプルは以下のようになります。
<?php require_once 'Crypt/Blowfish.php'; echo "mcrypt module is " . (extension_loaded("mcrypt") ? "" : "not ") . "loaded\n"; // 暗号化するデータ // 前後に半角スペースを3つずつ付加してみた $data = ' 小池さんはラーメン大好き '; echo "data : " . $data . "\n"; echo "data len : " . strlen($data) . "\n"; // 暗号化キー $key = 'the key value for crypting'; // CBCモードで暗号化するため、初期化ベクトルを用意する $iv = substr(md5(uniqid(rand(), 1)), 0, 8); // 暗号化処理 $blowfish = Crypt_Blowfish::factory('cbc', $key, $iv); $encrypted_data = $blowfish->encrypt($data); echo "encrypted data : " . base64_encode($encrypted_data) . "\n"; /** * [BK]mcrypt拡張モジュールがロードされている場合、再度インスタンスを * 取得する必要がある(mcrypt_generic_deinitしていないため) */ if (extension_loaded("mcrypt")) { $blowfish = Crypt_Blowfish::factory('cbc', $key, $iv); } // 復号処理 $decrypted_data = $blowfish->decrypt($encrypted_data); if (PEAR::isError($decrypted_data)) { die($decrypted->getMessage() . "\n"); } echo "decrypted data : " . $decrypted_data . "\n"; echo "decrypted data len : " . strlen($decrypted_data) . "\n"; echo "decrypted data urlencode : " . urlencode($decrypted_data) . "\n"; // エラーでなければrtrimを使って末尾の「\0」を削除 $decrypted_data = rtrim($decrypted_data, "\0"); echo "decrypted data : " . $decrypted_data . "\n"; echo "decrypted data len : " . strlen($decrypted_data) . "\n"; echo "decrypted data urlencode : " . urlencode($decrypted_data) . "\n"; echo 'validate : ' . ($data == $decrypted_data ? 'true' : 'false') . "\n";
出力は以下の通りです。
$ php crypt_blowfish2.php mcrypt module is not loaded data : 小池さんはラーメン大好き data len : 42 encrypted data : Uhh/AyjF9hkuwPeN7twC90D9BaLWqePBEE8dgAnBhquWxJPSLRI6MS7IcFZp7dvn decrypted data : 小池さんはラーメン大好き decrypted data len : 48 decrypted data urlencode : +++%E5%B0%8F%E6%B1%A0%E3%81%95%E3%82%93%E3%81%AF%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3%E5%A4%A7%E5%A5%BD%E3%81%8D+++%00%00%00%00%00%00 decrypted data : 小池さんはラーメン大好き decrypted data len : 42 decrypted data urlencode : +++%E5%B0%8F%E6%B1%A0%E3%81%95%E3%82%93%E3%81%AF%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3%E5%A4%A7%E5%A5%BD%E3%81%8D+++ validate : true $
mcrypt拡張モジュールがロードされている場合の結果です。
$ php crypt_blowfish2.php mcrypt module is loaded data : 小池さんはラーメン大好き data len : 42 encrypted data : D0rMN47UFq/YXZw8v0w2WJpITVad2iMxS5+3/9KteMcQUa0LdEn7XB6iAXcbIPd5 decrypted data : 小池さんはラーメン大好き decrypted data len : 48 decrypted data urlencode : +++%E5%B0%8F%E6%B1%A0%E3%81%95%E3%82%93%E3%81%AF%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3%E5%A4%A7%E5%A5%BD%E3%81%8D+++%00%00%00%00%00%00 decrypted data : 小池さんはラーメン大好き decrypted data len : 42 decrypted data urlencode : +++%E5%B0%8F%E6%B1%A0%E3%81%95%E3%82%93%E3%81%AF%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%B3%E5%A4%A7%E5%A5%BD%E3%81%8D+++ validate : true $
おお。これで良い感じ。
ついでに、「なぜbase64_xxxxxxでうまくいったか」ですが、復号した際に得られるのは
base64_encodeした文字列の末尾に連続する「\0」が付く(付かない場合もある)
です。で、base64_decodeする際、「\0」付きの場合もそうでない場合も文字列の終端(\0)として認識され、*結果的に*base64_encodeした文字列の部分だけがdecode対象になっていただけなんじゃないか、と思います(今思えば)。