Perlの暗号化モジュール Crypt::CBC とPHPの暗号化モジュール mcrypt_encrypt の仕様の違いを吸収する実装方法

たまにまテックなネタでも。
仕事がらみで必要に迫られたのでちょっと調査してみました。
Perlの Crypt::CBC と PHPの mcrypt_encrypt では多少の処理ロジックに差異があります。
その解決方法を探ってみましょう。
Perl Crypt::CBC で暗号化してみる

$ cat 1.pl
use Crypt::CBC;
use MIME::Base64;

my $key = 'be497e4272bae86d1c729e5c2a85fe65';
my $iv = '460e2f651ca61161';
my $text = 'hogehoge';

my $cipher = Crypt::CBC->new({
		key => $key,
		cipher => 'Crypt::Rijndael',
		iv => $iv,
		header => 'none',
	});

my $crypted = $cipher->encrypt($text);
print encode_base64($crypted);
$
$ perl 1.pl
fazwxd6I4GS/UnKUsG7kYg==
PHP mcrypt_encrypt で暗号化してみる

$ cat 2.php 
<?
$key  = 'be497e4272bae86d1c729e5c2a85fe65';
$iv   = '460e2f651ca61161';
$text = 'hogehoge';

$crypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $text, MCRYPT_MODE_CBC, $iv);
echo base64_encode($crypted)."\n";
?>
$
$ php 2.php 
1bwpbx18QSv9TpHi5cZFXg==
暗号化キーや初期ベクトルの指定は同じですが、暗号化したデータの結果が違います。

これは、Crypt::CBC の内部処理では、mcrypt_encrypt では行われない処理が2つあるためです。
  1. 指定している暗号化モードのブロックサイズに暗号化対象のテキストをPKCS#5パディングする
  2. 指定している暗号化モードのキーサイズに暗号化キーをmd5ハッシュ化する
暗号化モードが Rijndael(128bit)の場合のブロックサイズとキーサイズは下記で把握できます。

$ cat 1.pl 
use Crypt::CBC;
use MIME::Base64;

my $key = 'be497e4272bae86d1c729e5c2a85fe65';
my $iv = '460e2f651ca61161';
my $text = 'hogehoge';

my $cipher = Crypt::CBC->new({
		key => $key,
		cipher => 'Crypt::Rijndael',
		iv => $iv,
		header => 'none',
	});

print "keysize: ".$cipher->keysize()."\n";
print "blocksize: ".$cipher->blocksize()."\n";
$
$ perl 1.pl 
keysize: 32
blocksize: 16

この1.と2.の処理をPHPでは自分で実装する必要があります。

1.テキストの パディング

function pkcs5_pad( $text, $blocksize ) {
	$pad = $blocksize - ( strlen( $text ) % $blocksize );
	return $text . str_repeat( chr( $pad ), $pad );
}

2.暗号化キーのハッシュ化

function key_hash( $key, $keysize )
{
	$key = md5( $key, true);
	while( strlen( $key ) < $keysize )  {
		$key .= md5( $key, true);
	}
	return substr( $key, 0, $keysize);
}

で、これができあがったPHPの実装コードサンプルです。

$ cat 2.php
<?
$key  = 'be497e4272bae86d1c729e5c2a85fe65';
$iv   = '460e2f651ca61161';
$text = 'hogehoge';
$keysize = 32;
$blocksize = 16;

$padding_text = pkcs5_pad( $text, $blocksize );
$hashed_key = key_hash( $key, $keysize );

$crypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $hashed_key, $padding_text, MCRYPT_MODE_CBC, $iv);
echo base64_encode($crypted)."\n";

function pkcs5_pad( $text, $blocksize ) {
	$pad = $blocksize - ( strlen( $text ) % $blocksize );
	return $text . str_repeat( chr( $pad ), $pad );
}

function key_hash( $key, $keysize )
{
	$key = md5( $key, true);
	while( strlen( $key ) < $keysize )  {
		$key .= md5( $key, true);
	}
	return substr( $key, 0, $keysize);
}
?>
$
$ php 2.php 
fazwxd6I4GS/UnKUsG7kYg==
perl の暗号化結果と同じになりましたね。
これで、Perlの Crypt::CBC と PHPの mcrypt_encrypt で暗号化・復号化のやりとが可能になります。

ちなみに、Perlの Crypt::CBC側で、literal_key => 1 の設定を追加すれば、PHPでは、2.の暗号化キーのハッシュ化の実装が不要になります。

$ cat 1.pl 
use Crypt::CBC;
use MIME::Base64;

my $key = 'be497e4272bae86d1c729e5c2a85fe65';
my $iv = '460e2f651ca61161';
my $text = 'hogehoge';

my $cipher = Crypt::CBC->new({
		key => $key,
		cipher => 'Crypt::Rijndael',
		iv => $iv,
		header => 'none',
		literal_key => 1,
	});

my $crypted = $cipher->encrypt($text);
print encode_base64($crypted);
$
$ perl 1.pl 
4yb1njYe2Ftxt2P+08tNzA==


$ cat 2.php 
<?
$key  = 'be497e4272bae86d1c729e5c2a85fe65';
$iv   = '460e2f651ca61161';
$text = 'hogehoge';
$keysize = 32;
$blocksize = 16;

$padding_text = pkcs5_pad( $text, $blocksize );
// $hashed_key = key_hash( $key, $keysize );

$crypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $padding_text, MCRYPT_MODE_CBC, $iv);
echo base64_encode($crypted)."\n";

function pkcs5_pad( $text, $blocksize ) {
	$pad = $blocksize - ( strlen( $text ) % $blocksize );
	return $text . str_repeat( chr( $pad ), $pad );
}

function key_hash( $key, $keysize )
{
	$key = md5( $key, true);
	while( strlen( $key ) < $keysize )  {
		$key .= md5( $key, true);
	}
	return substr( $key, 0, $keysize);
}
?>
$
$ php 2.php 
4yb1njYe2Ftxt2P+08tNzA==

おしまい。