Java BouncyCastle と PHP mcrypt のパディング処理の違い

OpenPNE の DB を Java から叩こうとしたときにはまったメモ。

先に結論を書くと、

  • PHP mcrypt の zero padding は元データが8の倍数長の場合は padding しない。
  • Java BouncyCastle の zero padding は元データが8の倍数長でも padding ( 8 バイトの 0x00 を追加 )する。

という挙動の違いのおかげで、元データのサイズ(バイト町)が 8 の倍数長である場合は、Java 側で得られた暗号化ストリームの末尾 8 バイトを削除/無視してやなければ PHP 側で得られる暗号化ストリームと合致しない。


やろうとしたことは、外部から受け取ったメールアドレスを元に、Java から OpenPNE の DB に直接つないで、対応するメンバID c_member_id の取得。

DB 内では各ユーザのメールアドレスは regist_address に暗号化された形で格納されているので、外部から受け取ったメールアドレスを OpenPNE と同じ方法で暗号化し、それを where 節に渡して突き合わせるオーソドックスなアプローチでやろうとした。(他に思いつかなかった)

OpenPNE ではメールアドレスなどの情報は Blowfish の ECB モードで暗号化した後、base64 エンコードして DB に保存されている。
Blowfish はブロック長 64bit のブロック暗号なので、暗号化の際にブロック長を8バイト(64bit)の倍数にするための padding 処理が入る。
OpenPNE では不足分を 0x00 で埋める zero padinng (null paddingとも言うらしい)が利用されている。

したがって、Java側でも同様に zero padding → Blowfish で暗号化 → base64 エンコード をすれば同じ暗号化ストリームが得られるので、それをこんな感じの SQL にして DB を直接叩けば無事に c_member_id が得られるはずだった。

select c_member_id from c_member_secure where regist_address=(base64エンコード結果)

が、実際にやってみると、メールアドレスの長さが 8 の倍数の時に限って結果が帰ってこない。

8 の倍数というところで暗号化周りが臭いことはすぐにわかったので、Java + BouncyCastle と PHP + mcrypt に適当な8の倍数長の文字列を放り込んで base64 エンコード結果をそれぞれ出してみたら次のように異なる結果になっていた。

 Java + BouncyCastle 生成(例) 
 qOl0xfznpGr2agvLHOOOYzva9E2wQm5/yWsuc6qZQJE=
 PHP mcrypt (例)
 qOl0xfznpGr2agvLHOOOYzva9E2wQm5/

見ての通り、Java で生成した暗号化ストリームには、PHP mcrypt で生成した暗号化ストリームの末尾に12文字、base64デコードすれば 8 バイトの不要なバイト列が追加される形になっている。
先に書いた目的を達するには、元データが 8 の倍数長のバイト数であった場合、暗号化後のストリームの末尾8バイトを削除する操作が必要になる。

なので以下のコードを増築して padding の差異を吸収することにしましたとさ。

String uid = "(OpnePNEのメールアドレス)";
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

SecretKeySpec sksSpec = new SecretKeySpec(key.getBytes(), "Blowfish");
Cipher cipher = Cipher.getInstance("Blowfish/ECB/ZeroBytePadding");
cipher.init(Cipher.ENCRYPT_MODE, sksSpec);

byte[] in_data = uid.getBytes();
byte[] encdata = null;
byte[] outdata = null;
if (in_data.length != 0 && in_data.length % 8 == 0) {
              // padding 調整が必要
              encdata = cipher.doFinal(in_data);
              byte[] nopadding_data = new byte[encdata.length - 8];
              for (int i=0; i < nopadding_data.length; i++) {
                  nopadding_data[i] = encdata[i];
              }
              outdata = Base64.encodeBase64(nopadding_data);
} else {
              encdata = cipher.doFinal(in_data);
              outdata = Base64.encodeBase64(encdata);
}

(Base64 エンコードには Jakarta Commons Codec の org.apache.commons.codec.binary.Base64 を利用)