초안 2022. 12. 6 작성
spring에서 DB접속 정보를 properties로 관리하게 되는데, 서버 보안 점검툴로 검출하게되면
항상 이 부분에서 보안취약점이 걸리게 된다. 그래서 접속정보를 암호화 할 방법을 찾게 되고 가장 일반적으로 널리 쓰는 방식으로는 jasypt 라이브러리를 활용한 방법이 존재한다.
첨엔 쉽게 적용할수 있어서 좋아했다.
jdbc.properties
jdbc.driverClassName=net.sf.log4jdbc.DriverSpy
jdbc.url=jdbc:log4jdbc:mysql://xx.x.x.x:3306/database?....
jdbc.username=root
jdbc.password=ENC(YzFSlZP38ibqrWQ==)
jdbc.validationQuery=select 1
스프링 설정(xml)도 간단하다.
<...생략>
<bean id="propertyConfigurer" class="org.jasypt.spring4.properties.EncryptablePropertyPlaceholderConfigurer">
<constructor-arg ref="configurationEncryptor"/>
<property name="location" value="classpath:config/jdbc.properties"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="initialSize" value="5"/>
<property name="maxTotal" value="20"/>
<property name="maxIdle" value="20"/>
<property name="minIdle" value="5"/>
<property name="validationQuery" value="${jdbc.validationQuery}" />
</bean>
익이 봐왔던 코드일것이다.
여기서 문제가 발생하게 되는데
엔지니어의 경우 실제 서버에 패치를 해야하고 암호문을 만들어내야 하는데, 암호화를 적용해야하는 부분이 늘어남에 따라 각각 다른 암호화 방식을 채택하고 있어 불편함이 있었다.
[DB테이블 안에 계정정보, 서버 접속정보config 파일, 웹 jdbc 설정 정보]의 암호화 방식을 한가지로 통일하자는 의견이 나왔고 해당 부분의 개발을 맡게 되었다.
납품하는 프로그램의 기본 DB인 Mysql에서 기본 제공하는 AES_ENCRYPT()라는 함수를 활용하자는 의견이 나왔다.
찾아보니 jasypt의 알고리즘리스트에 'PBEWITHSHA256AND128BITAES-CBC-BC'가 있다고 해서 테스트해보았다
1. jdk1.8.0_101/jer/lib/ext 에 bcprov-jdk18on-172.jar를 넣고
2. jdk1.8.0_101/jer/lib/security/java.security 제일 마지막에
security.provider.2=org.bouncycastle.jce.provider.BouncyCastleProvider 도 넣고 테스트코드 실행
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
encryptor.setProvider(new BouncyCastleProvider());
encryptor.setPassword("somePassword");
encryptor.setAlgorithm("PBEWITHSHA256AND128BITAES-CBC-BC");
encryptor.setSaltGenerator(new StringFixedSaltGenerator(""));
new BouncyCastleProvider(); 에서 계속 실패 .... 결국 실패 했다.
그럼 EncryptablePropertyPlaceholderConfigurer 를 뜯어보자
EncryptablePropertyPlaceholderConfigurer 는 propertyPlaceholderConfigurer를 상속받아 사용하는데
propertyPlaceholderConfigurer에 setLocation하기 전에 조작해볼수 있지 않을까?
@Bean
public static PropertySourcesPlaceholderConfigurer loadProperties(){
PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
Resource[] resourceLocations = new Resource[] {
new ClassPathResource("jdbc.properties")
};
pspc.setLocations(resourceLocations);
pspc.setIgnoreUnresolvablePlaceholders(true);
return pspc;
}
여기까지 하고 멈칫... resource 안에 값을 어떻게 바꿔야될지 감이 안잡혔다.
그래서 DataSource를 만드는 부분을 class로 빼서
@Value({jdbc.url}) 요런거 사용하는 부분을 바꿔보려 했는데
xml 하단에서 dataSource를 못읽어서 실패... (내가 뭔가 잘못한듯)
결국 다 실패하고 DBConfig 라는 클래스로 활용
public class DbConfig {
/* maria 세팅 */
private static String jdbcDriverClassName;
private static String jdbcUrl;
private static String jdbcUserName;
private static String jdbcPassword;
private static String jdbcValidationQuery;
/**
* @Method setJdbcProperties
* @Date 2022. 12. 02
* @Writter 22gas
* @Return void
* @Discript jdbc_maria.properties 셋팅
*/
public void setJdbcProperties(Properties props) {
DbConfig.jdbcDriverClassName = getPropertiesValue(StringUtils.trim(props.getProperty("jdbc.driverClassName")));
DbConfig.jdbcUrl = getPropertiesValue(StringUtils.trim(props.getProperty("jdbc.url")));
DbConfig.jdbcUserName = getPropertiesValue(StringUtils.trim(props.getProperty("jdbc.username")));
DbConfig.jdbcPassword = getPropertiesValue(StringUtils.trim(props.getProperty("jdbc.password")));
DbConfig.jdbcValidationQuery = getPropertiesValue(StringUtils.trim(props.getProperty("jdbc.validationQuery")));
}
/**
* 프로퍼티 값을 읽어서 복호화 해야하는지 아닌지 판단한다.
* ENC() 로 되어있으면 복호화 하고 아니면 그냥 쓴다.
* @param _value
* @return String
*/
private String getPropertiesValue(String _value){
if(_value.startsWith("ENC(") && _value.endsWith(")")){
try{
String _encStr = _value.replace("ENC(", "").replace(")", "");
Cipher decryptCipher = Cipher.getInstance("AES");
decryptCipher.init(Cipher.DECRYPT_MODE, generateMySQLAESKey(AppGlobal.dbKey));
String _decryptStr = new String(decryptCipher.doFinal(Base64.getDecoder().decode(_encStr)));
return _decryptStr;
} catch (Exception e){
return _value;
}
} else {
return _value;
}
}
/**
* mysql암호화를 위한 필요작업
* @param key
* @return
*/
private SecretKeySpec generateMySQLAESKey(final String key) {
try {
final byte[] finalKey = new byte[16];
int i = 0;
for(byte b : key.getBytes("UTF-8"))
finalKey[i++%16] ^= b;
return new SecretKeySpec(finalKey, "AES");
} catch(UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
//.... getter, setter
}
xml 에서는 dbConfig['변수명'] 으로 사용...
<!-- jdbc_maria.properties를 읽어서 dataBase로 사용 -->
<bean id="dataBase" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="classpath:config/jdbc.properties" />
<property name="fileEncoding" value="UTF-8" />
</bean>
<!-- 위에서 만든 dataBase를 DbConfig 클래스에 매핑 -->
<bean id="dbConfig" class="path.to.DbConfig">
<property name="jdbcProperties" ref="dataBase" />
</bean>
<!-- 위에서 만든 dbConfig로 내부 값 접근-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="#{dbConfig['jdbcDriverClassName']}" />
<property name="url" value="#{dbConfig['jdbcUrl']}" />
<property name="username" value="#{dbConfig['jdbcUserName']}" />
<property name="password" value="#{dbConfig['jdbcPassword']}" />
<property name="initialSize" value="5"/>
<property name="maxTotal" value="20"/>
<property name="maxIdle" value="20"/>
<property name="minIdle" value="5"/>
<property name="validationQuery" value="#{dbConfig['jdbcValidationQuery']}" />
</bean>
※ xml 에서 dbConfig['변수명'] 으로 접근하게 되는데 getter가 없다면 에러가 뜬다... 참고!