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

[株式会社オズビジョン](http://www.oz-vision.co.jp/)の@terra_yuccoです。

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

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

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

## はじめに

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

```terminal
$ 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
<?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に

[^1]: [PHP.net - openssl_encrypt](http://php.net/manual/en/function.openssl-encrypt.php) を見ると、`$iv`はPHP5.3.3以降ってことだけど、動くんだよなあ。モジュールだけ新しいのかもしれない。

### 解決できなかったこと

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

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

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

```php
    /**
     * 暗号化方式が指定するブロック長を算出
     * 
     * @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暗号化](http://www.atmarkit.co.jp/ait/articles/1505/21/news030.html)）
なので、今回のDESに限った実装においては以下のようにしても良さそう。

```php

    /**
     * @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にやってみた](https://qiita.com/Uchikoba/items/7784e1f1eb7bae3b1593)** の@Uchikoba様に心からの感謝をいたします。
弊社側で取り込むにあたりかなり改修をしてはいますが、そのままある程度参考にできるコードがあるというのは、非常にありがたいものでした。

## 最後に

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

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

おしまい。