最近業務で暗号化を取り扱う機会があったのですが、開発用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のバージョンを上げたい。
ローカル環境と本番環境のモジュールが違うのは本当にツライ。
おしまい。
-
PHP.net - openssl_encrypt を見ると、
$iv
はPHP5.3.3以降ってことだけど、動くんだよなあ。モジュールだけ新しいのかもしれない。 ↩