Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

「PHPで暗号化・復号あれこれ」の続き

ラボブログの続きですが。。。


mcryptのpaddingは 平文の長さがブロックサイズ倍になるように ASCIIZ('\0')を付けるだけなので, base64化することで末尾のASCIIZを正しく保持する効果があると思われます(試してはいません).

今回挙げたサンプルを作る際、復号データの末尾に何か付加されているのは確認していたのですが、base64_encodeするとうまくいったので、今回はそれ以上深く追っていませんでした。また、コメントにある「padding」を見て、パディングのことをすっかり忘れていたことを思い出しました。

ということで、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対象になっていただけなんじゃないか、と思います(今思えば)。