LoginSignup
8
10

More than 5 years have passed since last update.

PHP7.2で非推奨になったmcryptをopensslで実装してみた [PHP5.3版]

Last updated at Posted at 2018-09-06

株式会社オズビジョン@terra_yuccoです。

最近業務で暗号化を取り扱う機会があったのですが、開発用Vagrantにはmcryptがインストールされていたものの、本番用EC2にはmcryptがインストールされていないという事象に直面しました。

mcryptを入れるという選択肢もあるのですが、mcryptの有無で処理を切り替えるコードがコア部分に入っていたので、選択できませんでした。(しかも、納期はすぐそこに迫っている。/(^o^)\)

そのため、休日に調べながらOpenSSLで再実装をしたのですが、自分たちの環境の制約もあり、若干違う学びがあったので、別記事としてあげさせていただきます。
参考にさせていただいた記事は PHP7.2で非推奨になったmcryptの代わりにopensslにやってみた です。ソースコードからリンクさせていただくくらいお世話になりました。

はじめに

PHPは5.3.17(そろそろ7にしたい)、mcryptモジュールは入っていません。

$ php --version
PHP 5.3.17 (cli) (built: Sep 17 2012 20:19:13)
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2012 Zend Technologies
$
$ php -m | grep mcrypt
$

実装コード

まずはコードで語ります。

<?php

/**
 * Class Openssl_cryptor
 *
 * mcryptで実装していたDES/CBC/PKCS5をOpenSSL利用で再現
 * 元の実装ではバイナリ文字列は16進数文字列に変換しているが
 * この実装ではバイナリのまま取り扱っている
 * PHP5.3.17ではhex2bin関数が使えないため
 *
 * @link   https://qiita.com/Uchikoba/items/7784e1f1eb7bae3b1593
 */
class Openssl_cryptor
{
    /**
     * Crypt method
     * 暗号化方式
     *
     * @var string
     */
    private $cryptMethod;

    /**
     * Crypt key
     * 暗号化キー
     *
     * @var string
     */
    public $cryptKey = null;

    /**
     * Initial vector
     * 初期化ベクトル
     *
     * @var string
     */
    private $initialVector = null;

    /**
     * @param $cryptMethod
     */
    public function setCryptMethod($cryptMethod)
    {
        $this->cryptMethod = $cryptMethod;
    }

    /**
     * @param $cryptKey
     */
    public function setCryptKey($cryptKey)
    {
        $this->cryptKey = $cryptKey;
    }

    /**
     * @param $initialVector
     */
    public function setInitialVector($initialVector)
    {
        $this->initialVector = $initialVector;
    }

    /**
     * @param $plainText
     *
     * @return string
     */
    public function encrypt($plainText)
    {
        return base64_encode(
            openssl_encrypt(
                $this->pkcs5Padding($plainText),
                $this->cryptMethod,
                $this->cryptKey,
                true,
                $this->initialVector
            )
        );
    }
    /**
     * @param string $text
     *
     * @return string
     */
    private function pkcs5Padding($text)
    {
        $blockSize = $this->getOpensslCipherBlockLength($this->cryptMethod);
        $pad = $blockSize - (strlen($text) % $blockSize);
        return $text . str_repeat(chr($pad), $pad);
    }

    /**
     * @param $cipher
     *
     * @return bool|int
     */
    private function getOpensslCipherBlockLength($cipher)
    {
        /**
         * 現状必要なのはDES/CBC/PKCS5
         * DESは64bit=8byte
         *
         * @todo 他の暗号化方式を使う場合には修正が必要
         */
        $desBlockLength = 8;

        return $desBlockLength;
    }
}

変更点

  • 全体に可能な範囲でPSR-2準拠
  • 現状は復号処理が不要のため、復号関係は一律削除
  • 暗号化メソッド、暗号化キー、初期化ベクトルを一律外部から設定可能に変更 1
  • 今の要件では初期化ベクトルは固定のため、初期化ベクトルの生成メソッドを削除
  • 暗号化済文字列を、base64 encode/decodeした文字列で引き回し、
    • コメント記載の通り hex2bin が無い(´・ω・`)ので
    • コード上でわかりやすいようにopenssl_encryptで行うのではなく外でbase64関数コール
  • ブロックサイズ取得を固定値returnに

解決できなかったこと

  • ブロックサイズ取得を固定値returnに

これだけがどうしても解決できなかったことで、最終的には今回の要件がDESであることから固定で8を返却するようにしています。

参照元のコードを引用させていただくと以下のようになるのですが、
$ivSizeを取るところまでは'des-cbc'を渡すとちゃんと動きます。(8になる)
その後の1byteから1,024byteまでブロック長を算出するところが、1の時点でreturnされてしまうので、Paddingなどが正しく行えずに暗号化が失敗しました。

    /**
     * 暗号化方式が指定するブロック長を算出
     * 
     * @param string $cipher 暗号化方式
     * @return int 暗号化方式が指定しているブロック長
     */
    function openssl_cipher_block_length($cipher) {
        $ivSize = @openssl_cipher_iv_length($cipher);

        // サポートしていない暗号化方式だった
        if ($ivSize === false) {
            return false;
        }

        $iv = str_repeat("a", $ivSize);

        // 1バイトから1024バイトまで順に暗号化可能なブロック長を試していく
        for ($size = 1; $size < 1024; $size++) {
            $output = openssl_encrypt(
                str_repeat("a", $size),
                $cipher,
                "a",
                OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
                $iv
            );

            if ($output !== false) {
                return $size;
            }
        }

        return false;
    }

わかっていないこと

openssl_encryptコール時、DESにて8byte以外の$ivを指定するとwarningが出るという事実がありました。
$ivのサイズと$blockSizeは等しいのだろうか?(等しいならもっと簡単に実装できそうだけれども、そこについて明示的に触れている場所を探せていません)

上記について、少なくともDESにおいては、$iv$blockSizeは等しそうということがわかりました。(マスターIT/暗号技術:第2回 DES暗号化
なので、今回のDESに限った実装においては以下のようにしても良さそう。


    /**
     * @param $cipher
     *
     * @return bool|int
     */
    private function getOpensslCipherBlockLength($cipher)
    {
        $allowedCiphers = array('des-cbc');

        /**
         * DES-CBC
         * $iv の長さとブロックサイズは等しい
         */
        if (!in_array($cipher, $allowedCiphers)) {
            return false;
        }
        return openssl_cipher_iv_length($cipher);
    }

mcrypt_get_block_sizeの実装ってどうなっているんだろう。
いつかコード読もう。

謝辞

再三の引用になりますが PHP7.2で非推奨になったmcryptの代わりにopensslにやってみた@Uchikoba様に心からの感謝をいたします。
弊社側で取り込むにあたりかなり改修をしてはいますが、そのままある程度参考にできるコードがあるというのは、非常にありがたいものでした。

最後に

mcryptの代替はopensslで可能ですが、全く同じように対応する関数があるわけではないので、今回DES/CBC/PKCS5Paddingのケースに限って実装をしましたが、他の暗号化方式に耐えられるものにはなっていません。
このあたり、ある程度スタンダードな代替手段が定着するといいなあと思っています。PHP7.2でcoreからPECLになったので、今後ある程度出てくるのかな、と期待しています。

とりあえず早いことPHPのバージョンを上げたい。
ローカル環境と本番環境のモジュールが違うのは本当にツライ。

おしまい。


  1. PHP.net - openssl_encrypt を見ると、$ivはPHP5.3.3以降ってことだけど、動くんだよなあ。モジュールだけ新しいのかもしれない。 

8
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10