S_a_k_Uの日記みたいなDB

~サクゥーと呼ばないで~

PHPで暗号化された情報をJavaで復号する

暗号化の要件で、PHPから送られる暗号化された情報の複合処理をJavaで行うような話。
PHP側から提示された復号の手順を見ると大垣さんの
PHPのOpenSSL関数を利用して暗号化する例 | yohgaki's blog
にあるアルゴリズムっぽかったので、その複合処理をJavaで書いてみた。
というか、初めてPHPのコードを実行したり、書き換えてみたりしたのはナイショだ。

PHP

大垣さんのコードに自分用にコメントやらechoでコンソールに出力しながら確認。

<?php
// 暗号化する文字列
$data = 'cleattext';
// 共通のパスワード
$pass = 'mypass';

$encrypted = encrypt($data, $pass);
echo 'Encrypted :' . $encrypted . PHP_EOL;
echo 'Decrypted :' . decrypt($encrypted, $pass) . PHP_EOL;

 /**
  * decrypt AES 256
  *
  * @param data $edata 暗号化された文字列
  * @param string $password 共通のパスワード
  * @return decrypted data 復号された文字列
  */
function decrypt($edata, $password) {

    // BASE64からデコードする
    $data = base64_decode($edata);

    // SALTを抽出する(1~16バイト目)
    $salt = substr($data, 0, 16);
    // 暗号化された文字列を取得する(17バイト目以降)
    $ct = substr($data, 16);

    // 暗号化キーをハッシュ値変換する回数(暗号化する側に依存)
    $rounds = 3; // depends on key length
    // 共通のパスワードにSALTを結合する
    $data00 = $password.$salt;
    // hash[0]=共通のパスワード+SALTのハッシュ値
    $hash = array();
    $hash[0] = hash('sha256', $data00, true);
    // 変換したハッシュ値を結合して保持する変数
    $result = $hash[0];
    // 共通のパスワード+SALTをハッシュ値変換回数-1回変換する
    for ($i = 1; $i < $rounds; $i++) {
        // 1回前のハッシュ値+共通のパスワード+SALTをハッシュ値変換する
        $hash[$i] = hash('sha256', $hash[$i - 1].$data00, true);
        // 変換したハッシュ値をresultに結合する
        $result .= $hash[$i];
    }

    // 暗号化のパスワード(暗号化する側に依存)
    $key = substr($result, 0, 32);
    echo '[復号]暗号化のパスワード:'.bin2hex($key).PHP_EOL;
    // 初期化ベクター(暗号化する側に依存)
    $iv  = substr($result, 32,16);
    echo '[復号]初期化ベクター    :'.bin2hex($iv).PHP_EOL;

    // 暗号化された文字列を暗号化のパスワードと初期化ベクターで復号する
    return openssl_decrypt($ct, 'AES-256-CBC', $key, true, $iv);

}

/**
 * crypt AES 256
 *
 * @param data $data 暗号化する文字列
 * @param string $password 共通のパスワード
 * @return base64 encrypted data 暗号化された文字列
 */
function encrypt($data, $password) {

    // SALTをランダム文字列で生成する
    // Set a random salt
    $salt = openssl_random_pseudo_bytes(16);

    // 変換したハッシュ値を結合して保持する変数
    $salted = '';
    // 0回目のハッシュ値
    $dx = '';
    // 48バイトを超えるまでハッシュ値を生成する(32バイト×2回)
    // Salt the key(32) and iv(16) = 48
    while (strlen($salted) < 48) {
      // 1回前のハッシュ値+共通のパスワード+SALTをハッシュ値変換する
      $dx = hash('sha256', $dx.$password.$salt, true);
      // 変換したハッシュ値をsaltedに結合する
      $salted .= $dx;
    }

    // 暗号化のパスワード(復号する側と合せる)
    $key = substr($salted, 0, 32);
    echo '[暗号化]暗号化のパスワード:'.bin2hex($key).PHP_EOL;
    // 初期化ベクター(復号する側と合せる)
    $iv  = substr($salted, 32,16);
    echo '[暗号化]初期化ベクター    :'.bin2hex($iv).PHP_EOL;

    // 暗号化する文字列を暗号化のパスワードと初期化ベクターで暗号化する
    $encrypted_data = openssl_encrypt($data, 'AES-256-CBC', $key, true, $iv);

    // BASE64へエンコードする
    return base64_encode($salt . $encrypted_data);

}
?>

Java

PHPの復号処理をそのまま書き換えてみた。
配列の使い方とかでもっとカッコいい書き方があるんかもしれんけど、ごめんなさい。

import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.ArrayUtils;

public class CryptTest {

    // 共通のパスワード
    private static String PASS = "mypass";

    // SHA256ハッシュ値取得アルゴリズムの名前
    private static String HASH_ALGO_NAME = "SHA-256";

    // 復号化アルゴリズムの名前
    private static String CRYPT_ALGO_NAME = "AES/CBC/PKCS5Padding";

    public static void main(String[] args) {
        try {
            String encrypted = "vrRpShah5IrIRmvYuBXA8IQldREfLuSZBpd67hc0z00=";
            System.out.println("Encrypted :" + encrypted);
            String decStr = new CryptTest().decrypt(encrypted);
            System.out.println("Decrypted :" + decStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String decrypt(String src) throws Exception {

        // SHA256のハッシュ値変換アルゴリズム
        MessageDigest md = MessageDigest.getInstance(HASH_ALGO_NAME);

        // BASE64からデコードする
        byte[] data = Base64.decodeBase64(src.getBytes());

        // SALTを抽出する(1~16バイト目)
        byte[] salt = Arrays.copyOfRange(data, 0, 16);
        // 暗号化された文字列を取得する(17バイト目以降)
        byte[] ct = Arrays.copyOfRange(data, 16, data.length);

        // 暗号化キーをハッシュ値変換する回数(暗号化する側に依存)
        int rounds = 3;
        // 共通のパスワードにSALTを結合する
        byte[] data00 = ArrayUtils.addAll(PASS.getBytes(), salt);
        // hash(0)=共通のパスワード+SALTのハッシュ値
        List<byte[]> hash = new ArrayList<byte[]>();
        md.update(data00);
        hash.add(md.digest());
        // 変換したハッシュ値を結合して保持する変数
        byte[] result = hash.get(0);
        // 共通のパスワード+SALTをハッシュ値変換回数-1回変換する
        for ( int i = 1 ; i < rounds ; i++ ) {
            // 1回前のハッシュ値+共通のパスワード+SALTをハッシュ値変換する
            md.update(ArrayUtils.addAll(hash.get(i - 1), data00));
            hash.add(md.digest());
            // 変換したハッシュ値をresultに結合する
            result = ArrayUtils.addAll(result, hash.get(i));
        }

        // 暗号化のパスワード(暗号化する側に依存)
        byte[] key = Arrays.copyOfRange(result, 0, 32);
        System.out.println("[復号]暗号化のパスワード:" + new String(Hex.encodeHex(key)));
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        // 初期化ベクター(暗号化する側に依存)
        byte[] iv = Arrays.copyOfRange(result, 32, 48);
        System.out.println("[復号]初期化ベクター    :" + new String(Hex.encodeHex(iv)));
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

        // 暗号化された文字列を暗号化のパスワードと初期化ベクターで復号する
        Cipher cipher = Cipher.getInstance(CRYPT_ALGO_NAME);
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
        return new String(cipher.doFinal(ct));

    }

}

実行結果

PHP(暗号化→復号)
[暗号化]暗号化のパスワード:1640a2f3bc5f7e5ee0c42827a4b9fc884fd7db9d7cb67aae521ec3bcb9fe7769
[暗号化]初期化ベクター    :e899f868cab4f2c51240c027df61a117
Encrypted :vrRpShah5IrIRmvYuBXA8IQldREfLuSZBpd67hc0z00=
[復号]暗号化のパスワード:1640a2f3bc5f7e5ee0c42827a4b9fc884fd7db9d7cb67aae521ec3bcb9fe7769
[復号]初期化ベクター    :e899f868cab4f2c51240c027df61a117
Decrypted :cleattext
Java(復号のみ)
Encrypted :vrRpShah5IrIRmvYuBXA8IQldREfLuSZBpd67hc0z00=
[復号]暗号化のパスワード:1640a2f3bc5f7e5ee0c42827a4b9fc884fd7db9d7cb67aae521ec3bcb9fe7769
[復号]初期化ベクター    :e899f868cab4f2c51240c027df61a117
Decrypted :cleattext