Tech blog Produced by FOURIER

[Blowfish] McryptからOpenSSLへの移管

Sena Sena 2022.12.08

概要

Redhat系のOSを使っている方は、そろそろPHPのバージョンが5系からアップデートする時期が迫っていると思います。 PHPの5.Xから7.2以上に上げるときに困るのがMcrypt関数の廃止です。

移行先として推奨されているOpenSSLへの単純な移行だと復号が出来なくなってしまいます。 そこでMcryptからOpenSSLに代替した場合でも、復号可能なコードの実装例を紹介します。

参考

移行の結果

既存のコード

暗号化

function reversible_encrypt($key, $iv, $data)
{
    $base64_data = base64_encode($data);
    $resource    = mcrypt_module_open(MCRYPT_BLOWFISH, '', MCRYPT_MODE_CBC, '');
    mcrypt_generic_init($resource, $key, $iv);
    $encrypted_data = mcrypt_generic($resource, $base64_data);
    mcrypt_generic_deinit($resource);
    //後始末
    mcrypt_module_close($resource);
    $encrypted_data_base64 = base64_encode($encrypted_data);
    return $encrypted_data_base64;
}

複合化

function reversible_decrypt($key, $iv, $encrypted_data_base64)
{
    $encrypted_data = base64_decode($encrypted_data_base64);
    $resource       = mcrypt_module_open(MCRYPT_BLOWFISH, '', MCRYPT_MODE_CBC, '');
    mcrypt_generic_init($resource, $key, $iv);
    $base64_decrypted_data = mdecrypt_generic($resource, $encrypted_data);
    mcrypt_generic_deinit($resource);
    $decrypted_data = base64_decode($base64_decrypted_data);
    //後始末
    mcrypt_module_close($resource);
    return $decrypted_data;
}

修正コード

共通処理

/**
 * 暗号化キーの長さが足りない場合、16byte以上の鍵長まで拡張する
 *
 * @see https://github.com/LancersDevTeam/PHP_versionup/blob/master/PHP5.6toPHP7.3/1.1_mcrypt%E5%AF%BE%E5%BF%9C.md
 *
 * @param string $key
 *
 * @return string
 *
 * @throws LengthException
 */
function extension16BytesOrMoreKey(string $key): string
{
    $length = strlen($key);
    if ($length === 0) throw new LengthException('暗号化キーが空です');
    if ($length < 16) $key = str_repeat($key, ceil(16 / $length));
    return $key;
}

/**
 * 0x00パディングを行う
 *
 * @see https://github.com/LancersDevTeam/PHP_versionup/blob/master/PHP5.6toPHP7.3/1.1_mcrypt%E5%AF%BE%E5%BF%9C.md
 *
 * @param string $data
 *
 * @return string
 */
function paddingZero(string $data): string
{
    $pad_length = strlen($data) % 8;
    if ($pad_length !== 0) $data .= str_repeat("\x00", 8 - $pad_length);
    return $data;
}

暗号化

function reversible_encrypt(string $key, string $iv, string $data): string
{
    $base64_data = base64_encode($data);
    return openssl_encrypt(paddingZero($base64_data), 'BF-CBC', extension16BytesOrMoreKey($key), OPENSSL_ZERO_PADDING, $iv);
}

復号化

function reversible_decrypt(string $key, string $iv, ?string $encrypted_data_base64): string
{
    $base64_decrypted_data = openssl_decrypt($encrypted_data_base64, 'BF-CBC', extension16BytesOrMoreKey($key), OPENSSL_ZERO_PADDING, $iv);
    return base64_decode($base64_decrypted_data);
}

BlowfishにおけるMcryptとOpenSSLの違い

動作を確認したところ、以下のようになっていました。

暗号鍵が短い場合の処理の違い

暗号化キーがabcだとします。

Mcrypt

16bit以上の長さになるまで循環されて拡張される。

abcabcabcabcabcabc

OpenSSL

16bitの長さになるまで0で埋めて拡張される。

abc\0\0\0\0\0\0\0\0\0\0\0\0\0

対策

OpenSSLの関数を呼び出す前に、こちらで暗号鍵を長くします。

function extension16BytesOrMoreKey(string $key): string
{
    $length = strlen($key);
    if ($length === 0) throw new LengthException('暗号化キーが空です');
    if ($length < 16) $key = str_repeat($key, ceil(16 / $length));
    return $key;
}

暗号化されたデータの違い

暗号化する際の文字列を固定ブロック長に分割したときの動作が違うようでした。 暗号化キーがabcだとします。

Mcrypt

固定ブロック長(1octet)のサイズになるように、NULL(ゼロ)パディングされます。

abc\x00\x00\x00\x00\x00

OpenSSL

PKCS#7パディングが行われます。

abc\x05\x05\x05\x05\x05
💡
上記の場合、5ビットパディングされているので\x05になります。

対策

暗号化する前に、暗号化キーに対して固定ブロック長のNULL(ゼロ)パディングを自分で実装して適用します。

function paddingZero(string $data): string
{
    $pad_length = strlen($data) % 8;
    if ($pad_length !== 0) $data .= str_repeat("\x00", 8 - $pad_length);
    return $data;
}

最後に

Mcryptは既に廃止されたライブラリということもあり、既に他の言語でもOpenSSLが使われています。 今回のようにMcryptでした暗号が全く同じ方式だったとしても、ちょっとした仕様の違いでOpenSSLで復号が出来ない場合があります。

Mcryptで暗号化したデータを復号しなければならない案件の都合上、Mcryptを使い続けるか書き直すかのどちらかでしたが、今回はMcryptで暗号している部分が少なかった事もありOpenSSLで書き直しました。

参考サイト様は大変参考になりました。ありがとうございました。

Sena

Sena / Engineer

生涯に亘り技術を極めていきたい。