LoginSignup
0
2

More than 5 years have passed since last update.

一部の値が暗号化されたプロパティファイルのBeanを丸ごと復号する

Last updated at Posted at 2019-03-21

状況

最新には程遠いJavaVM(6以降)で開発をしている。
・プロパティファイル内で、「パスワード」などの一部の項目値がOpenSSL(Base64)で暗号化されている。
・復号されるべき対象項目は増減するかもしれない。
・復号対象の項目を一つ一つちまちま復号するのが嫌だ。

目的

復号に手間をかけたくない。
あたかも平文で最初から定義されていたかのようにBeanを操作したい。
つまり、Beanごとドーンとやったらパーンと使いたい。(語彙)

参考にさせて頂いた記事

OpenSSL互換の暗号化をJava/PHPで実現する

Javaのバージョン

6以降。
本稿ではとりあえず6に準拠します。

使用ライブラリ

Base64を使用するのでApacheCommonsCodecを使用します。
※今回はJava6で使用可能な「1.11」を使用します。
※なおJava8以降であれば「java.util.Base64」で代替できるので、当該ライブラリは不要です。

登場人物(用意するもの)

プロパティファイル

まずはJAXBで変換したいのでXML形式のプロパティファイルです。

Encryptedアノテーション

Bean用のカスタムアノテーションです。

PropertyBeanクラス

プロパティファイルの値たちが入るBeanです。

EncDecConverterクラス

掲題の通り、ドーンとやったらパーンとしてくれるクラスです。

Javaで使用するリソースは以上です。
別途、暗号化・復号に使用するマスターパスワードを脳内で準備しておいてください。
また、暗号化方式は「128bit鍵AES CBCモード」とします。

実践

1.プロパティファイルを作ります。

xxUserxxPassを復号対象とします。
クラスパスが通っているディレクトリに置いておきます。

property.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
    <!--復号対象「xxUser」-->
    <!--平文「hogeyama」をパスワード「yakisoba」で暗号化した文字列です-->
    <xxUser>U2FsdGVkX1/k+6dPcXrci4AbyQ0TNtytubkVFCxzcF4=</xxUser>
    <!--復号対象「xxPass」-->
    <!--平文「hogeyamanopassword」をパスワード「yakisoba」で暗号化した文字列です-->
    <xxPass>U2FsdGVkX19XLVe01kx2ahoKVKnSXLhBQ2aiRrdUlbjgtKu1IXD3EuYDSADab5vA</xxPass>
    <!--平文-->
    <miscItem1>hoge</miscItem1>
    <!--平文-->
    <miscItem2>fuga</miscItem2>
</root>

実際の暗号化文字列はopensslコマンド(下記)、もしくは上述した参考URLで作成しておいてください。

bash
$echo 【平文】 | openssl enc -e -aes-128-cbc -base64 -k 【マスターパスワード】
2.Encryptedアノテーションを作ります。
Encryptedアノテーション
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypted {}
3.PropertyBeanを作ります。

この時、暗号化値が入るフィールドにEncryptedアノテーションをつけておきます。

PropertyBean

public class PropertyBean {
    @Encrypted
    private String xxUser = null;
    @Encrypted
    private String xxPass= null;
    private String miscItem1 = null;
    private String miscItem2 = null;

    public String getXxUser() {
        return xxUser;
    }
    public void setXxUser(String xxUser) {
        this.xxUser = xxUser;
    }
    public String getXxPass() {
        return xxPass;
    }
    public void setXxPass(String xxPass) {
        this.xxPass = xxPass;
    }
    public String getMiscItem1() {
        return miscItem1;
    }
    public void setMiscItem1(String miscItem1) {
        this.miscItem1 = miscItem1;
    }
    public String getMiscItem2() {
        return miscItem2;
    }
    public void setMiscItem2(String miscItem2) {
        this.miscItem2 = miscItem2;
    }

}
4.EncDecConverterクラスは以下の通りです。
EncDecConverterクラス
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;


public class EncDecConverter<T> {
    private T t = null;
    private String password = null;
    private Charset charset = null;

    @SuppressWarnings("unused")
    private EncDecConverter() {}

    public EncDecConverter(T t,String password){
        this.t = t;
        this.password = password;
        charset = Charset.defaultCharset();
    }

    public EncDecConverter(T t,String password,String charsetName){
        this.t = t;
        this.password = password;
        charset = Charset.forName(charsetName);
    }

    public boolean decrypt(){
        return convert(true);
    }

    public boolean encrypt(){
        return convert(false);
    }

    private boolean convert(boolean processDecrypt){
        if(t == null || password == null){
            return false;
        }

        Field[] fs = t.getClass().getDeclaredFields();
        String value = "";
        try {
            for(Field f : fs){
                f.setAccessible(true);
                if(f.getAnnotation(Encrypted.class) != null){
                    value = (processDecrypt?decrypt((String)f.get(t),password):encrypt((String)f.get(t),password));
                    f.set(t,removeLineSeparator(value));
                }
            }
        }catch(Throwable e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    private String removeLineSeparator(String s) {
        if(s == null) {
            return "";
        }
        return s.replaceAll("[\r]*[\n]*$", "");
    }

    private boolean getKeyAndGenerateIv(String password, byte[] salt, byte[] key_bytes, byte[] iv_bytes) {
        byte[] password_bytes = password.getBytes(charset);
        int length = password_bytes.length + salt.length;
        ByteBuffer byte_buffer = ByteBuffer.allocate(length);
        byte_buffer.put(password_bytes);
        byte_buffer.put(salt);
        byte_buffer.rewind();
        byte[] byte_array = new byte[length];
        byte_buffer.get(byte_array);
        try {
            System.arraycopy(MessageDigest.getInstance("MD5").digest(byte_array), 0, key_bytes, 0, key_bytes.length);
        }catch (NoSuchAlgorithmException e ) {
            e.printStackTrace();
            return false;
        }
        length = password_bytes.length + salt.length + key_bytes.length;
        byte_buffer = ByteBuffer.allocate(length);
        byte_buffer.put(key_bytes);
        byte_buffer.put(password_bytes);
        byte_buffer.put(salt);
        byte_buffer.rewind();
        byte_array = new byte[length];
        byte_buffer.get(byte_array);
        try {
            System.arraycopy(MessageDigest.getInstance("MD5").digest(byte_array), 0, iv_bytes, 0, iv_bytes.length);
        }catch (NoSuchAlgorithmException e ) {
            e.printStackTrace();
            return false;
        }

        return true;
    }

    private String encrypt(String plaintext, String password) throws Throwable{
        // Generate random salt.
        byte[] random_bytes = new byte[8];
        new SecureRandom().nextBytes(random_bytes);

        byte[] key_bytes = new byte[16];
        byte[] iv_bytes = new byte[16];
        getKeyAndGenerateIv(password, random_bytes, key_bytes, iv_bytes);

        SecretKey secret = new SecretKeySpec(key_bytes, "AES");
        IvParameterSpec ivspec = new IvParameterSpec(iv_bytes);
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        }catch(NoSuchPaddingException e) {
            throw e;
        }catch(NoSuchAlgorithmException e) {
            throw e;
        }

        try {
            cipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
        }catch(InvalidKeyException e) {
            throw e;
        }catch(InvalidAlgorithmParameterException e) {
            throw e;
        }

        byte[] encrypted_bytes;
        try {
            encrypted_bytes = cipher.doFinal(plaintext.getBytes(charset));
        }catch(IllegalBlockSizeException e) {
            throw e;
        }catch(BadPaddingException e) {
            throw e;
        }

        final String header_string = "Salted__";
        byte[] header_bytes = header_string.getBytes(charset);
        int length = header_bytes.length + random_bytes.length + encrypted_bytes.length;
        ByteBuffer byte_buffer = ByteBuffer.allocate(length);
        byte_buffer.put(header_bytes);
        byte_buffer.put(random_bytes);
        byte_buffer.put(encrypted_bytes);
        byte_buffer.rewind();
        byte[] byte_array = new byte[length];
        byte_buffer.get(byte_array);

        return new String(Base64.encodeBase64(byte_array));
        //Java8以降の場合は以下の通り記載可能です。
        //return new String(Base64.getEncoder().encodeToString(byte_array));
    }

    private String decrypt(String payload, String password) throws Throwable{
        byte[] payload_bytes = Base64.decodeBase64(payload.getBytes(charset));
        //Java8以降の場合は以下の通り記載可能です。
        //byte[] payload_bytes = Base64.getDecoder().decode(payload.getBytes(StandardCharsets.UTF_8));
        byte[] header_bytes = new byte[8];
        byte[] salt_bytes = new byte[8];
        int length = payload_bytes.length;
        ByteBuffer byte_buffer = ByteBuffer.allocate(length);
        byte_buffer.put(payload_bytes);
        byte_buffer.rewind();
        byte_buffer.get(header_bytes);
        byte_buffer.get(salt_bytes);
        length = payload_bytes.length - header_bytes.length - salt_bytes.length;
        byte[] data_bytes = new byte[length];
        byte_buffer.get(data_bytes);

        byte[] key_byte = new byte[16];
        byte[] iv_bytes = new byte[16];
        getKeyAndGenerateIv(password, salt_bytes, key_byte, iv_bytes);

        SecretKey secret = new SecretKeySpec(key_byte, "AES");
        IvParameterSpec ivspec = new IvParameterSpec(iv_bytes);
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        }catch(NoSuchPaddingException e) {
            throw e;
        }catch(NoSuchAlgorithmException e) {
            throw e;
        }

        try {
            cipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
        }catch(InvalidKeyException e) {
            throw e;
        }catch(InvalidAlgorithmParameterException e) {
            throw e;
        }

        byte[] decrypted;
        try {
            decrypted = cipher.doFinal(data_bytes);
        }catch(IllegalBlockSizeException e) {
            throw e;
        }catch(BadPaddingException e) {
            throw e;
        }

        return new String(decrypted);
    }
}
5.テストクラスを作成して動作確認します。
EncDecTest
import java.io.InputStream;

import javax.xml.bind.JAXB;

public class EncDecTest {
    public static void main(String[] args) {
        EncDecTest t = new EncDecTest();
        t.execute();
    }

    public void execute() {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream("property.xml");
        PropertyBean prop = JAXB.unmarshal(is, PropertyBean.class);
        EncDecConverter<PropertyBean> c = new EncDecConverter<PropertyBean>(prop,"yakisoba","UTF-8");
        if(!c.decrypt()) {
            System.err.println("error.");
            System.exit(1);
        }
        System.out.println(prop.getXxUser()); //hogeyamaを出力
        System.out.println(prop.getXxPass()); //hogeyamanopasswordを出力
        System.out.println(prop.getMiscItem1()); //hogeを出力
        System.out.println(prop.getMiscItem2()); //fugaを出力
        System.exit(0);
    }
}

decryptメソッドの結果がtrueであれば、
引数として渡したPropertyBeanインスタンスにおいて
復号対象としたフィールドが復号されています。
あとは普通にBeanからgetして使うだけです。

なお、上記の通り、EncDecConverterコンストラクタへの引数として
マスターパスワードを平文で渡す必要がありますが、
この前段としてマスターパスワードをどのように管理しておきどうやってI/Fしていくべきか?
については本稿では触れません。

まとめ

「テキスト丸ごと」ではなく部分的にOpenSSL暗号化文字列を運用していくパターンもあると思うんですよね。(それが正解かどうかは置いておいて)
なのでそんなTipsをまとめてみました。
あと、リフレクションを下手に多用するのはよくないと思いつつ、便利だから使っちゃう。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2