Javaで暗号化したデータをPerlで復号化しようとしたら大変だった件
JavaでRijndael(AES)で暗号化されたデータをPerlで復号化しようと思います。
「暗号方式と秘密鍵だけ聞いておけば簡単にデコードできるっしょ、余裕っしょ」とタカをくくっていたら、思いっきり天罰がくだりました。久しぶりにハマったのであります。
ちゃんと確認しておくべきだった情報
まずは暗号方式と秘密鍵だけでなく、以下の情報をしっかりと確認しておく必要アリでした。
暗号のことちゃんと勉強した事がないので、なんだかよくわからんけど、必要らしい。
せめて事前にここらへんを読んで勉強しておけばよかった。
ぱせらんメモ
http://d.hatena.ne.jp/pasela/20100612/crypto
DESに代わる次世代暗号「AES」の最終候補が「Rijndael」に決定
http://itpro.nikkeibp.co.jp/members/ITPro/ITARTICLE/20001003/1/
ブロック暗号化モード
http://www.triplefalcon.com/Lexicon/Encryption-Block-Mode-1.htm
今回はこれらの情報を全然把握せず、よその開発チームからJavaのソースコード(ドキュメントなし)と断片的な情報(「暗号鍵はたぶんこれ」といった感じの不確かな情報)だけを入手して、「まぁ適当に復号化できるだろう」とナメた態度で臨んだら、思いのほかハマってしまったわけです。
結局、いろいろ調べるのにJavaのソースを読んだり書いたりして、えらい手間がかかりました。。。
Javaで暗号化
Javaでの暗号化/復号化の簡単なサンプルです。
条件は以下のとおり。
- 暗号アルゴリズム => AES(Rijndael)
- 秘密鍵 => 0123456789ABCDEF
- 秘密鍵の長さ => 128bit
- ブロック暗号化モード => CBC
- IV => 0000000000000000
- padding方式 => PKCS5Padding
「Hello World!」という文字列を暗号化してみます。
以下おソースです。
import java.security.*; import javax.crypto.*; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class Encrypt { public static void main(String[] args) { try { // 元データはHello World! String plainText = "Hello World!"; System.out.println( "PLAIN TEXT: " + plainText ); // 秘密鍵とIV byte[] key = "0123456789ABCDEF".getBytes(); byte[] iv = "0000000000000000".getBytes(); SecretKey cipherKey = new SecretKeySpec(key, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv); // 条件を指定してCipherを暗号モードで初期化 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec); // 暗号化してみる。結果のバイト配列を16進文字列で表示 byte[] cipherText = cipher.doFinal(plainText.getBytes()); System.out.println( "ENCRYPTED : " + asHex(cipherText) ); // 今度は複合化。 cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec); byte[] output = cipher.doFinal(cipherText); System.out.println("DECRYPTED : " + new String(output)); } catch(Exception e) { e.printStackTrace(); } } // byte配列を16進で返す関数。 private static String asHex(byte bytes[]) { StringBuffer strbuf = new StringBuffer(bytes.length * 2); for (int index = 0; index < bytes.length; index++) { int bt = bytes[index] & 0xff; if (bt < 0x10) { strbuf.append("0"); } strbuf.append(Integer.toHexString(bt)); } return strbuf.toString(); } }
実行すると以下のようになります。
PLAIN TEXT: Hello World!
ENCRYPTED : d481e7bc55b0ef8b74221d497d6bc259
DECRYPTED : Hello World!
「Hello World!」という文字列を暗号化して16進にした文字が「d481e7bc55b0ef8b74221d497d6bc259」として取得できました。
そいつをさらに復号すると元の「Hello World!」になるのも確認できました。
Perlで復号化
さて、こいつをPerlで復号化します。Crypt::CBCを使います。
use strict; use warnings; use Crypt::CBC; # Javaで暗号化した文字列 my $encrypted = "d481e7bc55b0ef8b74221d497d6bc259"; # Crypt::CBCのコンストラクタ。パラメータが重要。 my $cipher = Crypt::CBC->new( -key => '0123456789ABCDEF', -keysize => 16, -literal_key => 1, -cipher => "Crypt::Rijndael", -iv => '0000000000000000', -header => 'none', ); # 16進文字列をバイトに変換 $encrypted = pack( "H*", $encrypted ); # 復号化して表示 my $decrypted = $cipher->decrypt($encrypted); print "decrypted: ", $decrypted, "\n";
このスクリプトによってPerl側で「Hello World!」と復元できました。それにしてもここに至るまで長かった。。。
Crypt::CBCのコンストラクタに渡す全てのパラメータが重要で、少しでも組み合わせが違うとまともにデコードできません。
さらに言うとこの中で「-literarl_key => 1」がかなり盲点です。
当初このパラメータの存在に気づかずだいぶ悩んでいたら、ようやくこんな書き込みを発見。
Java Solution会議室 > perl java暗号化復号化ロジックに関して
http://ap.atmarkit.co.jp/bbs/core/fjava/22485
言い忘れましたが、javaで暗号化しても、perlで暗号化しても、こちらでは同じ暗号文が出力されました。ただしliteral_key => trueを指定した場合です。
これを指定しないと、Crypt::CBCのほうは、デフォルトでは指定したキーをパスワードとみなし、
その文字列を複数回ハッシュした結果からキーを生成するようです(標準的なPBEではなく、おそらく独自方式ではないかと思います)。
OH! そんなの知らないってば。
これがわかるまで1日かかってしまったあるyo!