プログラマーになりたい

(まともな)プログラマーになりたいんや!

Javaでopenssl準拠のaes128cbc暗号化

AES128の暗号化って何かと使う機会あるんだけど、InitialVectorの扱いとかどうしようとかいろいろ悩んでいたんだけど、opensslのアルゴリズムパクればいいんじゃね!?と閃いた。

どうせ暗号化するなら、互換性もあったほうが気分的にいいしね。

package sample;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * AES128,CBCモードで暗号化/復号するためのUtil<br>
 * opensslの実装に準拠している
 * <pre>
 * 【暗号化】
 * $ openssl aes-128-cbc -e -in rawtext.txt -out encrypted.txt
 * 【復号】
 * $ openssl aes-128-cbc -d -in encrypted.txt -out decrypted.txt
 * </pre>
 *
 * @author test
 * @version 1.0
 *
 */
public class AESCryptUtil {

    /** saltのバイト長 */
    public static final int SALT_BYTE_LENGTH = 8;

    /** 暗号化Keyのバイト長 */
    public static final int KEY_BYTE_LENGTH = 16;

    /** IVのバイト長 */
    public static final int IV_BYTE_LENGTH = 16;

    /** キャッシュのバイト長 */
    public static final int BUFFER_BYTE_LENGTH = 1024;

    /** 暗号化ファイルの先頭に付与するbyte値 */
    protected static final byte[] HEADER_BYTES = "Salted__".getBytes(StandardCharsets.UTF_8);

    /** 不可視のコンストラクタ */
    private AESCryptUtil() {
        // インスタンス化禁止
    }

    public static void main(String... args) {
        String pass = "password";
        File in = new File(args[0]);
        File encrypt_out = new File(args[1]);
        File decrypt_out = new File(args[2]);

        // 暗号化
        encrypt(in, encrypt_out, pass);
        // 復号
        decrypt(encrypt_out, decrypt_out, pass);
    }

    /**
     * 復号処理
     *
     * @param srcIs
     *            復号対象ファイルストリーム
     * @param password
     *            暗号化パスワード
     * @return 復号ファイルストリーム
     */
    public static InputStream decrypt(FileInputStream srcIs, String password) {
        try {
            // バイナリからヘッダー文字列(Salted__)を取得し、検証する
            byte[] header = new byte[HEADER_BYTES.length];
            srcIs.read(header);
            if (!Arrays.equals(HEADER_BYTES, header)) {
                throw new IllegalArgumentException("復号対象データの形式が不正です");
            }
            // バイナリからsaltを取得する
            byte[] salt = new byte[SALT_BYTE_LENGTH];
            srcIs.read(salt);
            // パスワードとsaltをもとに、暗号化keyとivを生成する
            byte[] key = generateKey(password, salt);
            byte[] iv = generateIV(password, salt, key);

            // 復号処理
            Cipher cipher = getAESCipher(Cipher.DECRYPT_MODE, key, iv);
            return new CipherInputStream(srcIs, cipher);
        } catch (IOException e) {
            throw new IllegalArgumentException("復号処理に失敗しました", e);
        }
    }

    /**
     * 復号処理
     *
     * @param src
     *            復号対象ファイル
     * @param dest
     *            復号後のファイル
     * @param password
     *            暗号化パスワード
     */
    public static void decrypt(File src, File dest, String password) {
        validate(src, dest);
        try (InputStream is = decrypt(new FileInputStream(src), password);
                FileOutputStream fos = new FileOutputStream(dest);) {
            byte[] buf = new byte[BUFFER_BYTE_LENGTH];
            int read;
            while ((read = is.read(buf)) != -1) {
                fos.write(buf, 0, read);
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("復号処理に失敗しました", e);
        }
    }

    /**
     * 暗号化処理
     *
     * @param src
     *            暗号化対象ファイル
     * @param dest
     *            暗号化後のファイル
     * @param password
     *            暗号化パスワード
     */
    public static void encrypt(File src, File dest, String password) {
        validate(src, dest);
        // salt、暗号化Key、IVを生成する
        byte[] salt = generateSalt();
        byte[] key = generateKey(password, salt);
        byte[] iv = generateIV(password, salt, key);

        // 暗号化処理
        Cipher cipher = getAESCipher(Cipher.ENCRYPT_MODE, key, iv);
        try (FileInputStream fis = new FileInputStream(src);
                FileOutputStream fos = new FileOutputStream(dest);
                CipherOutputStream cos = new CipherOutputStream(fos, cipher);) {
            // ヘッダー文字列「Salted__」(8バイト) + Salt値(8バイト)をまず書き込む
            fos.write(concatBytes(HEADER_BYTES, salt));
            // 暗号化されたコンテンツを書き込む
            byte[] buf = new byte[BUFFER_BYTE_LENGTH];
            int read;
            while ((read = fis.read(buf)) != -1) {
                cos.write(buf, 0, read);
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("暗号化処理に失敗しました", e);
        }
    }

    /**
     * バリデート
     *
     * @param src
     *            入力ファイル
     * @param dest
     *            出力ファイル
     */
    private static void validate(File src, File dest) {
        if (!src.exists()) {
            throw new IllegalArgumentException("入力ファイルが存在しません。:" + src);
        }
        if (src.equals(dest)) {
            throw new IllegalArgumentException("入力ファイルと出力ファイルが同一です。:" + src);
        }
        if (dest.exists()) {
            throw new IllegalArgumentException("出力ファイルがすでに存在します。:" + dest);
        }
    }

    /**
     * 暗号器を取得する
     *
     * @param mode
     *            暗号化モード/復号モード
     * @param key
     *            暗号化キー
     * @param iv
     *            IV
     * @return 暗号機
     */
    private static Cipher getAESCipher(int mode, byte[] key, byte[] iv) {
        SecretKey secretKey = new SecretKeySpec(key, "AES");
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(mode, secretKey, new IvParameterSpec(iv));
            return cipher;
        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
            throw new IllegalStateException("暗号処理の初期化に失敗しました", e);
        } catch (NoSuchPaddingException | InvalidKeyException e) {
            throw new IllegalArgumentException("暗号化キーが不正です", e);
        }

    }

    /**
     * saltを生成する<br>
     * saltは実行毎に違う値となる。
     *
     * @return 生成されたsalt
     */
    private static byte[] generateSalt() {
        byte[] salt = new byte[SALT_BYTE_LENGTH];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    /**
     * パスワードとsaltをもとに、暗号化キーを取得する<br>
     * 暗号化キー = パスワード + Salt のMD5
     *
     * @param passwdStr
     *            パスワード
     * @param salt
     *            Salt
     * @return 生成された暗号化キー
     */
    private static byte[] generateKey(String passwdStr, byte[] salt) {
        byte[] passwd = passwdStr.getBytes(StandardCharsets.UTF_8);
        return getMd5(concatBytes(passwd, salt));
    }

    /**
     * 暗号化キーとパスワードとsaltをもとに、IVを取得する<br>
     * IV = 暗号化キー + パスワード + Salt のMD5
     *
     * @param passwdStr
     *            パスワード
     * @param salt
     *            Salt
     * @param key
     *            暗号化キー
     * @return 生成された暗号化iv
     */
    private static byte[] generateIV(String passwdStr, byte[] salt, byte[] key) {
        byte[] passwd = passwdStr.getBytes(StandardCharsets.UTF_8);
        return getMd5(concatBytes(key, passwd, salt));
    }

    /**
     * byte配列の連結
     *
     * @param byteArrays
     *            byte配列
     * @return 連結後byte配列
     */
    private static byte[] concatBytes(byte[]... byteArrays) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            for (byte[] array : byteArrays) {
                outputStream.write(array);
            }
        } catch (IOException e) {
            throw new IllegalStateException("バイト配列の連結処理で予期しないエラーが発生しました。", e);
        }
        return outputStream.toByteArray();
    }

    /**
     * MD5ハッシュの生成
     *
     * @param src
     *            ハッシュ生成対象
     * @return MD5ハッシュ
     */
    private static byte[] getMd5(byte[] src) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return md.digest(src);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("MD5ハッシュの生成処理で予期しないエラーが発生しました。", e);
        }
    }

}

いじょーう。