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!

JavaPerl」のような異なる言語間での暗号/復号処理は、ナメてかかると意外と苦労するよ、というお話でした。