異なる言語間での暗号化と復号
仕事でデータを暗号化して保存する必要が出てきたので色々調べてみた。
メインのシステムはPHPで作っているんだけど、Javaなども絡んでくるので、お互いが処理できる暗号方式でなければいけない。
仕様さえ明確にしてあればオレオレアルゴリズムでもいいんだけど、今回はかなり重要なデータを扱うので世間でそれなりに使われている暗号方式を使うことにした。
暗号の種類
自分も暗号にそんな詳しいわけではないけど、「データをパスワードで暗号化するんでしょ?」ぐらいにしか思ってない人はきっと大変な思いをする。
今の話で登場するのは
- 暗号化したいデータ
- 暗号の種類(アルゴリズム)
- パスワード(暗号処理に使うキー)
の3つだけど、これからやろうとしてるブロック暗号では
- 暗号化したいデータ
- 暗号の種類(アルゴリズム)
- 暗号処理に使うキー(パスワードというかバイト列)
- 暗号利用モード(ブロック処理の種類)
- 初期化ベクトル(IV=Initialization Vector)
- パディング方式
と、6種類もの要素が絡んでくる。なにこれめんどうくさい!
暗号の種類は、DESとかAESとかBlowfishとかいうやつ。
キーは、ブロック暗号処理に使う共通鍵。
暗号利用モードは、そのブロック暗号が扱うブロック長よりも長いデータを暗号化するときにどうやって処理するかという方式。
初期化ベクトルは、ブロック*1を処理する時に同じ平文、同じ鍵でも異なる暗号文にするために必要なデータ。まぁ、乱数初期化時のseedみたいなもん?
パディング方式は、ブロックのサイズに満たないデータを処理する時にブロックサイズまでデータを詰める方式。
PHPのmcrypt
PHPではMCryptがサポートされている*2のでこれを使うことにする。
MCryptを使うとRijndael(AES)とかBlowfishとかDESとか3DESとか……まぁとにかく主要な暗号アルゴリズムで暗号処理をすることができる。
mcryptの一般的な使い方は
mcrypt_module_open()
でモジュールをオープンするmcrypt_generic_init()
で初期化するmcrypt_generic()
で暗号化したり、mdecrypt_generic()
*3で復号したりするmcrypt_deinit()
で後始末するmcrypt_module_close()
でモジュールをクローズする
こんな感じで結構面倒くさい。*4
openとinitとか、deinitとcloseとかひとつにならなかったの?とか思わないでもない。
面倒くさいと感じた方には関数一発で処理できる方法もある。
mcrypt_encrypt()
で暗号化mcrypt_decrypt()
で復号
openやinit時に渡していたパラメータも全て渡すことになるので引数が多くなるが、これなら一発で処理できる。
しかしこの方法だと細かい制御ができなかったりエラーが起きた時に細かい情報は取れない。
やはりきちんと制御したい時には前者の方法を使うことになる。
というわけで、これを使えば世間一般で使われている暗号処理はできるようになる。のだが……
mcryptのパディング方式
mcryptはパディング方式を選べない。
ブロック長に満たない場合はNULLバイトで埋められる(ZeroBytePadding)。
しかも一般的なパディング方式と違ってブロック長ぴったりな場合はパディングされないので困る。
この方式はどうも一般的ではないみたい……というか何も考えずに埋めてるようにしか思えないのでおすすめ出来ない。たぶん復号時に困る。
0x00で埋められたからといって復号したときに末尾の0x00を取り除けばいいわけではないしね。
元々のデータに0x00が含まれていたのかパディングされたのか判別できないので。
世間で暗号処理のサンプルを漁ると、Base64やserializeで一段階くるんでから暗号掛けるようなサンプルがたくさん出てくるけど、これだとサイズがふくれあがるのでおすすめ出来ない。
そんなことするぐらいならデータの先頭か末尾にデータ長を埋めるぐらいのことをしてほしい。
が、そんなことをするぐらいならもう一歩進んでPKCS#5 Paddingというパディング方式を自前で実装したほうがもっとよい。簡単だし。
PKCS#5 Padding
パディング方式とか今回初めて知ったので自分もよくわかってないんだけど、PKCS#5 Paddingはたぶん標準的に使われている方式だと思う。
PerlのCrypt::CBCなんてpadding=standardでPKCS#5 Paddingだしきっとスタンダードな方式なんだよ。ううん、きっとそう!
この方式は切り捨てるべきサイズ(=埋めるサイズ)の値を表すバイト値で足りない分を埋めるという方式。
ブロック長が8バイトでデータサイズが5バイトなら0x03で足りない3バイト分埋める。
戻す時は末尾の1バイトを見れば切り捨てるべきサイズがわかる。
ちょっと注意が必要なのはデータがブロック長にぴったりだった場合。この場合は1ブロック分丸ごとパディングになる。つまり0x08が8つ付く。戻す時のことを考えればわかるよね?
イメージがわかない人はこちらのブログを見てもらうと一発でわかる。
http://wp.serpere.info/archives/432
PHPでこの処理をするコードはMcryptのUser Contributed Notesに載っているのでそれを使えばよい。
簡単だから自前でも実装できるけどね。
Javaでの暗号処理
さて、PHPでの暗号化はできたので次にこれをJavaで復号してみようというわけだが……Java久しぶりすぎてわからんw
Javaでの暗号処理はjava.securityパッケージとjavax.cryptoパッケージあたりが中心になっているようだ。
こちらの流れはだいたいこんな感じ。
java.security.Key
を用意するjavax.security.spec.AlgorithmParameterSpec
を用意するjavax.crypto.Cipher.getInstance()
するCipher.init()
で初期化するCipher#update()
とかCipher#doFinal()
で暗号化・復号する
今回はとりあえずBlowfishで、鍵も普通にパスワードみたいに文字列で試してみたかったのでこんな感じかな。
鍵長とIVのサイズはアルゴリズムによって違うので注意。
java.security.Key key = new javax.crypto.SecretKeySpec("password".getBytes(), "Blowfish"); java.security.spec.AlgorithmParameterSpec iv = new javax.crypto.spec.IvParameterSpec("_initiv_".getBytes());
で、あとはCipherを初期化してupdate()
, update()
, doFinal()
!!
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("Blowfish/CBC/PKCS5Padding"); cipher.init(javax.crypto.Cipher.DECRYPT_MODE, key, iv); byte[] decrypted = cipher.doFinal(encrypted);
まぁ実際にはいきなりdoFinal()
だけで終了したけどw
苦労したところ
その1。
まず何よりも暗号に対する知識が絶対的に不足していたことw
その2。
PHPだとキーとかいってもただのstring(中身はバイナリかもしれないけど)なんだけど、Javaで処理しようとしたらDESKeySpecとかPBEKeySpecとかキーの種類が色々あってどれを使えばいいのかわからなかった。まぁBlowfishなのでDESは違うかなとは思ったけど。
結局MCryptのサイトを見たらKey TypeのところがSecretとなっていたので、SecretKeySpecを使えばいいのかな?と。
その3。
IVの設定も最初はどうやればいいのかわからなかった。
暗号アルゴリズムに対する設定は全てAlgorithmParametersという括りになっていてIVという名前がまったく出てこないので、IvParameterSpecにたどり着くまでだいぶかかった。
その4。
暗号化されたデータは普通バイナリなんだけど、今回は16進数文字列でやりとりをするので、そこのインターフェイスとか作る方が却って苦労した。
あぁぁ、こんな簡単なことも出来ないほどにJava忘れてるのか……と軽く泣きたくなったw
ちなみにPHPの場合はbin2hex($bin)
とpack('H*', $hex)
でおk