状況
・最新には程遠いJavaVM(6以降)で開発をしている。
・プロパティファイル内で、「パスワード」などの一部の項目値がOpenSSL(Base64)で暗号化されている。
・復号されるべき対象項目は増減するかもしれない。
・復号対象の項目を一つ一つちまちま復号するのが嫌だ。
目的
復号に手間をかけたくない。
あたかも平文で最初から定義されていたかのようにBeanを操作したい。
つまり、Beanごとドーンとやったらパーンと使いたい。(語彙)
参考にさせて頂いた記事
Javaのバージョン
6以降。
本稿ではとりあえず6に準拠します。
使用ライブラリ
Base64を使用するのでApacheCommonsCodecを使用します。
※今回はJava6で使用可能な「1.11」を使用します。
※なおJava8以降であれば「java.util.Base64」で代替できるので、当該ライブラリは不要です。
登場人物(用意するもの)
プロパティファイル
まずはJAXBで変換したいのでXML形式のプロパティファイルです。
Encryptedアノテーション
Bean用のカスタムアノテーションです。
PropertyBeanクラス
プロパティファイルの値たちが入るBeanです。
EncDecConverterクラス
掲題の通り、ドーンとやったらパーンとしてくれるクラスです。
Javaで使用するリソースは以上です。
別途、暗号化・復号に使用するマスターパスワードを脳内で準備しておいてください。
また、暗号化方式は「128bit鍵AES CBCモード」とします。
実践
1.プロパティファイルを作ります。
xxUser
とxxPass
を復号対象とします。
クラスパスが通っているディレクトリに置いておきます。
<?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で作成しておいてください。
$echo 【平文】 | openssl enc -e -aes-128-cbc -base64 -k 【マスターパスワード】
2.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アノテーションをつけておきます。
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クラスは以下の通りです。
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.テストクラスを作成して動作確認します。
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をまとめてみました。
あと、リフレクションを下手に多用するのはよくないと思いつつ、便利だから使っちゃう。