---
title: KeyStoreに保存した秘密鍵を使って Spring Boot アプリに渡すパスワードを暗号化する
tags: ["Java", "Spring", "Spring Boot"]
categories: ["Programming", "Java", "org", "springframework", "boot"]
date: 2014-07-16T00:24:31Z
updated: 2014-07-17T05:38:05Z
---

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


* [Keystore Tutorial](http://www.javacodegeeks.com/2014/07/java-keystore-tutorial.html)
