Spring Bootを使ってアプリを作ると、環境情報を外出しした上で、実行可能jarを作成し、
$ java -jar myapp.jar --barPassword=mypassword
みたいに、実行時に外からプロパティを渡せる。便利。
でも、生パスワードはのせたくないですよね。
そこで、外からは暗号化したプロパティを渡し、Bean生成時に復号する方法を紹介します。
秘密鍵生成
keytoolを使ってAES256の秘密鍵を生成します。
$ keytool -genseckey -storetype jceks -keyalg AES -keysize 256 -alias mykey -storepass changeme -keypass changeme
このKeyStoreを使って暗号化・復号するサンプルコードは以下の通り。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.Key;
import java.security.KeyStore;
import java.util.Base64;
public class Hoge {
public static void main(String[] args) throws Exception {
try (InputStream keystoreStream = Files.newInputStream(Paths.get(System.getProperty("user.home") + "/.keystore"))) {
KeyStore keystore = KeyStore.getInstance("JCEKS");
keystore.load(keystoreStream, "changeme".toCharArray());
Key key = keystore.getKey("mykey", "changeme".toCharArray());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 暗号化
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec("0123456789012345".getBytes()));
byte[] encrypted = cipher.doFinal("mypassword".getBytes(StandardCharsets.UTF_8));
String encryptedString = new String(Base64.getEncoder().encode(encrypted), StandardCharsets.UTF_8);
System.out.println(encryptedString);
// 復号
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec("0123456789012345".getBytes()));
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedString.getBytes(StandardCharsets.UTF_8)));
String decryptedString = new String(decrypted, StandardCharsets.UTF_8);
System.out.println(decryptedString);
}
}
}
実行結果
jYldzgRvHJwPWRBOvMC2Tg==
mypassword
"mypassword"という文字列を暗号化すると"jYldzgRvHJwPWRBOvMC2Tg=="になります。
復号可能なJavaConfigの作成
JavaConfigで以下のように定義すればよいです。
package com.example;
import com.example.App.Bar;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.Key;
import java.security.KeyStore;
import java.util.Base64;
@Configuration
public class AppConfig {
@Value("${keyStore:#{systemProperties['user.home']}/.keystore}")
String keyStore;
@Value("${keyAlias}")
String keyAlias;
@Value("${keyStorePass}")
char[] keyStorePass;
@Value("${keyPass}")
char[] keyPass;
@Value("${initialVector:0123456789012345}")
String initialVector;
@Value("${barPassword}")
String barPassword;
private static final String ENCRYPTED_PREFIX = "encrypted:";
@Lazy // 復号するときだけ遅延評価されるように@Lazyをつける
@Bean
Key secretKey() throws Exception {
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keyStore))) {
KeyStore keystore = KeyStore.getInstance("JCEKS");
keystore.load(keystoreStream, keyStorePass);
return keystore.getKey(keyAlias, keyPass);
}
}
String decrypt(String target) throws Exception {
if (target != null && target.startsWith(ENCRYPTED_PREFIX)) {
String encryptedString = target.substring(ENCRYPTED_PREFIX.length());
// 値が"encrypted:"から始まる場合は、それ以降を復号する
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey(), new IvParameterSpec(initialVector.getBytes()));
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedString.getBytes(StandardCharsets.UTF_8)));
return new String(decrypted, StandardCharsets.UTF_8);
} else {
// 値が"encrypted:"から始まらない場合は、生の値をそのまま使う
return target;
}
}
@Bean
Bar bar() throws Exception {
Bar bar = new Bar();
bar.setPassword(decrypt(barPassword) /* 復号 */);
return bar;
}
}
src/main/resources/application.ymlに以下を設定しておきます。
keyAlias: mykey
keyStorePass: changeme
keyPass: changeme
barPassword: password
開発中は開発用パスワードを生で使用すればよいです。
SecretKey
の生成に@Lazy
をつけているので、生パスワードを使っている限りはkeystoreへのアクセスはないです。開発中はこれで。
本番実行
実行可能jarにビルドして、本番時は実行時に"encrypted:暗号化パスワード"を渡すとよいです。
$ java -jar myapp.jar --barPassword=encrypted:jYldzgRvHJwPWRBOvMC2Tg==
実行ディレクトリにapplication.ymlをおいて、オーバーライドしてもOK。 (keystoreのパスワードが生だったらあんまり意味ないか・・・?)