AES-256-CBCを利用したPHPによる暗号化処理の実装

はじめまして、naoyaです。
全く別の業種からブリスウェルに入社して4年目になり、普段はお客様との打ち合わせや設計などを担当させていただいております。

新型コロナも中々収束の兆しが見えず、まだ外出がしづらい日々が続いておりますが皆様いかがお過ごしでしょうか?
私はリモートワークで数少ない運動であった毎日の通勤がなくなり当然のように体重が増量しましたが、家で運動が出来るゲームを購入しなんとか減量に成功しました。

今回はそんなリモートワークの中で起きた、とあるプロジェクトに途中から参画して暗号化処理のテストでハマってしまったお話になります。

AES-256-CBC

保存処理の中で、一部の値を暗号化して格納する処理を発見
暗号化処理自体は該当の項目を平文に復号し利用する別の処理が実行できていたため問題なさそうだけど、暗号化自体の確認も兼ねて別のテストで既に保存されたデータ(開発者がテスト用に作ったデータ、平文は不明)を復号して使用出来るようにもしておきたいので、再現可能かテストを続行

DBに格納された値をブラウザ上でチェック出来るツールなんかでさっと見比べて終わりかな、などと思いつつ既存の暗号化処理を利用しているとのことだったので、まずは開発者にチャットで暗号化形式について質問してみた所「SHA-256で実装しています」との回答
SHA-256はハッシュ化で、項目は平文に戻せないとダメなはずなので再度確認するも「戻せます」との返答、いまいち納得がいかないがとりあえずSHA-256でハッシュ化してみる

やはりDBに格納された文字列とは異なっている
これ以上はソースを見たほうが早いな、と思いソースを漁ってみると暗号化処理の部分には"openssl_encrypt"の関数と形式として"AES-256-CBC"が使用されていた。 なんだ、チャットの打ち間違いか。AES-256-CBCは初めて聞いたけど、ここまでわかればすぐ終わるだろう

前提
1. 入力値をAES-256-CBCで暗号化している
2. 暗号化はPHPのOpenSSL関数で実装済
3. 最終的な目的は暗号化された文字列の復号まで

opensslコマンド

AES-256-CBCがどんな形式かはわからないが、詳細は後にしてまずは実装された処理が合っているか確認したいので、再現できるようなツールがないか検索

ツールなどは見つからず、PHPやNode.jsでの実装方法などが出てくる
ローカル環境建てたりは面倒なのでなんとか出来ないか探していたらopensslコマンドの存在を発見
PHPの関数と同じ名前だしこれなら再現できそう、と思いサイトに載っていたコマンドを参考に実行

$echo "test" | openssl enc -aes-256-cbc -e -base64

で"test"をAES-256-CBCで暗号化(-eを-dにすると復号)
-base64オプションを付けてbase64エンコードして表示
これでいけるかな、と思ったらパスワードを求められつまづく
適当に入れても当然結果は一致しない
よく考えると、ソースに書いてある処理の"key"と"iv"について何もしていない
ここまで来てAES-256-CBCについて調べ始める

....
なるほど、opensslコマンドのデフォルトでは入力したパスワードと自動生成されるsaltから共通鍵である"key"と初期化ベクトルの"iv"を生成し、この2つを利用して暗号化を行うのか
それならソースの処理と同じ形でkeyとivを直接指定すればいけそう(-K "共通鍵" / -iv "初期化ベクトル" で指定が可能)

ここで改めてソース内の処理を確認して気づいた
opensslコマンドで指定できるkeyとivはhex(16進数)での指定
一方、ソースのkeyとivは文字列で送っている

あれ、これソース側で送っている文字列を変換したとしてもkeyが変わる = 暗号化文字列が変わるから無理じゃないか。。。
ということで、これ以上は自分の理解では進めなさそうなのでopensslコマンドでの再現は諦める

OpenSSL関数

当初の目的である暗号化のチェックからは逸れてしまうが、こうなるともうソースの処理を逆にしてデコードする方が早そうなのでPHPの処理を再度解読する

$ciphertext = openssl_encrypt($str, $method, $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($ciphertext);

str = 暗号化したい平文
method = aes-256-cbc
key / iv = ソースで処理された値
をそれぞれ持ってきて暗号化→base64エンコードを行っている

ここまで避けてきたけどもう観念してMAMPを起動し、ローカルサーバーを建てる。 当然ソースの入力画面と処理は別のファイル、ただPHPで外部ファイルから処理読み込んだりといったソースを書いたことはないので、1ファイルで処理が実行できるように簡単にフォームを作成

<form method="post">
  <table border="1">
    <tr>
      <td>暗号化したい文字列</td>
      <td><input type="text" name="str" value=""></td>
    </tr>
    <tr>
      <td>key</td>
      <td><input type="text" name="key" value=""></td>
    </tr>
    <tr>
      <td>iv</td>
      <td><input type="text" name="iv" value=""></td>
    </tr>
  </table>
  <br>
  <input type="submit" value="実行">
</form>

とりあえずkey/ivも直接入力する形にして、暗号化処理はソースからツギハギして作成

<?php
  //constant value
  $method = 'AES-256-CBC';

  //screen input
  $str = $_POST['str'];
  $key = $_POST['key'];
  $iv = $_POST['iv'];

  // encrypt
  $ciphertext = openssl_encrypt($str, $method, $key, OPENSSL_RAW_DATA, $iv);
  $encryptresult = base64_encode($ciphertext);

  //screen output
  print ("暗号化したい文字列: $str <br />");
  print ("暗号化結果: $encryptresult <br />");
  print ("key: $key <br />");
  print ("iv: $iv <br />");
?>

これでようやく暗号化された結果(encryptresult)がDBの値と一致した
続けて、復号処理を追加

フォーム

    <tr>
      <td>復号したい文字列</td>
      <td><input type="text" name="encryptstr" value=""></td>
    </tr>

復号処理

  $encryptstr = $_POST['encryptstr'];


暗号化と逆にopenssl_decryptを使って

  $decryptresult = openssl_decrypt($encryptstr, $method, $key, OPENSSL_ZERO_PADDING, $iv);

表示部分

  print ("復号したい文字列: $encryptstr <br />");
  print ("復号結果: $decryptresult <br />");

をそれぞれ追加
注)このソースは「とりあえず動く」レベルの物なので、PHPの作法など色々おかしいかもしれませんが私は開発者ではないのでご容赦ください。

試しに"test_text"という文字列をkey = "testkey" / iv = "testiv"※ で暗号化
※実際は複雑な値が使用されていますが簡略化します

暗号化された文字列は"eivIkeGzth5HH8VgV6Biew=="、これを「復号したい文字列」に入力して同じkey/ivで再度実行すると無事"test_text"が返ってくる
やっと出来た...
f:id:bw_naoya:20200803164148p:plain


おわりに

当然といえば当然ですが、やはり暗号化は複雑なので手軽に再現出来るものでもないですね。
openssslコマンドにしてももしかしたら再現方法があるかもしれないので、ご存知の方がいらっしゃればご教授いただければ幸いです。