📝 BLOG.IK.AM

@making's memo
(🗃 Categories 🏷 Tags)

KeyStoreに保存した秘密鍵を使って Spring Boot アプリに渡すパスワードを暗号化する

🗃 {Programming/Java/org/springframework/boot}

🏷 Java 🏷 Spring 🏷 Spring Boot

🗓 Updated at 2014-07-17T05:38:05+09:00 by Toshiaki Maki  🗓 Created at 2014-07-16T00:24:31+09:00 by Toshiaki Maki  {✒️️ Edit  ⏰ History}


☠ Danger: This content is too old. You should avoid reading. If you want to read, please click .

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のパスワードが生だったらあんまり意味ないか・・・?)