AES 암호화 방식을 통한 DBCP 암호화
암호화 처리는 어떤 데이터를 암호화 하느냐에 따라
일방향 암호화 처리를 할 것인지
양방향 암호화 처리를 할 것인지 결정하게 됩니다.
일방향암호화 : 사용자비밀번호와 같이 시스템관리자도 복호화가 필요없이 비교만 하면 되는 경우
양방향암호화 : 사용자이름, 휴대폰번호, 계좌번호 등 복호화하여 시스템운영상 확인이 필요한 경우
복호화가 필요한 경우는 암호화 했던 비밀키를 입력하여 풀어냅니다.
이번 포스팅에서는 tomcat 을 was 로 사용하는 경우
server.xml 에 평문으로 입력된 DB접속정보가 노출되어
2차 피해는 막자는 취지에서 샘플 소스를 공유합니다.
준비
AES 암호화를 위한 필요라이브러리
https://commons.apache.org/proper/commons-codec/download_codec.cgi
이클립스를 사용하는 경우
commons-codec-1.11-bin.zip 파일 다운로드 받은 후 commons-codec-1.11.jar 를 java build path 에 등록합니다.
아래는 server.xml 에 등록된 DB 접속정보입니다.
username , password , url 정보가 평문으로 입력되어 있어
인증되지 않은 사용자가 해당 파일을 오픈 했을 때 서버정보를 쉽게 알 수 있게 됩니다.
목표
username , password , url 와 같은 평문 저장을 원치 않는 속성을 암호화 처리
암호화 처리 방식 : AES-256
( SEED 방식도 많이 쓰니 다음에 정리하겠습니다..)
STEP 1.
라이브러리 등록 : commons-codec-1.11.jar
파일 다운로드 경로 : https://commons.apache.org/proper/commons-codec/download_codec.cgi
commons-codec-1.11-bin.zip 파일을 받아 압축을 풀면
해당 jar 라이브러리 파일이 존재하는데
Java Build Path에 등록합니다.
STEP 2
평문 데이터를 암호화 및 복호화 function 제공 ( AES256Util.java )
package com.encrypt;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
public class AES256Util {
private String iv;
private Key keySpec;
public AES256Util(String key) throws UnsupportedEncodingException {
this.iv = key.substring(0, 16);
byte[] keyBytes = new byte[16];
byte[] b = key.getBytes("UTF-8");
int len = b.length;
if(len>keyBytes.length){
len = keyBytes.length;
}
System.arraycopy(b, 0, keyBytes, 0 , len);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
this.keySpec = keySpec;
}
public String encrypt(String str) throws NoSuchAlgorithmException, GeneralSecurityException, UnsupportedEncodingException {
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes()));
byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
String enStr = new String(Base64.encodeBase64(encrypted));
return enStr;
}
public String decrypt(String str) throws NoSuchAlgorithmException, GeneralSecurityException, UnsupportedEncodingException {
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv.getBytes()));
byte[] byteStr = Base64.decodeBase64(str.getBytes());
return new String(c.doFinal(byteStr), "UTF-8");
}
}
작업 내용 : 암호화 / 복호화 처리 함수 제공
STEP 3
서버정보 암호화 내용 확인 ( TestMain.java )
package com.encrypt;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
public class TestMain {
public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, GeneralSecurityException {
AES256Util aes = new AES256Util("testestestestses"); // 암호화 키 16자리
// 암호화 된 내용
System.out.println("scott : " + aes.encrypt("scott"));
System.out.println("tiger : " + aes.encrypt("tiger"));
}
}
작업내용 : STEP 2 에서 작성한 함수를 main 을 통해서 테스트할 수 있습니다.
TestMain.java 실행결과
scott : BxoN1jurrKiXrlSSvf0/ng==
tiger : JcHanmccsCwU4Z4NpnYQKg==
복호화 했을 때 입력했던 값을 받을 수 있다면 성공!
System.out.println(aes.decrypt("BxoN1jurrKiXrlSSvf0/ng=="));
System.out.println(aes.decrypt("JcHanmccsCwU4Z4NpnYQKg=="));
STEP 4
암호화 된 정보를 복호화 하는 factory 를 생성
DBCP 의 기본은 org.apache.commons.dbcp.BasicDataSourceFacroty 를 사용하고 있는데
해당 class 파일을 decompile 후 아래와 같이 customizing 했습니다.
package com.encrypt;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import javax.sql.DataSource;
import org.apache.tomcat.dbcp.dbcp.BasicDataSource;
public class EncryptDataSourceFactory implements ObjectFactory
{
private static final String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit";
private static final String PROP_DEFAULTREADONLY = "defaultReadOnly";
private static final String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation";
private static final String PROP_DEFAULTCATALOG = "defaultCatalog";
private static final String PROP_DRIVERCLASSNAME = "driverClassName";
private static final String PROP_MAXACTIVE = "maxActive";
private static final String PROP_MAXIDLE = "maxIdle";
private static final String PROP_MINIDLE = "minIdle";
private static final String PROP_INITIALSIZE = "initialSize";
private static final String PROP_MAXWAIT = "maxWait";
private static final String PROP_TESTONBORROW = "testOnBorrow";
private static final String PROP_TESTONRETURN = "testOnReturn";
private static final String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis";
private static final String PROP_NUMTESTSPEREVICTIONRUN = "numTestsPerEvictionRun";
private static final String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis";
private static final String PROP_TESTWHILEIDLE = "testWhileIdle";
private static final String PROP_PASSWORD = "password";
private static final String PROP_URL = "url";
private static final String PROP_USERNAME = "username";
private static final String PROP_VALIDATIONQUERY = "validationQuery";
private static final String PROP_VALIDATIONQUERY_TIMEOUT = "validationQueryTimeout";
private static final String PROP_INITCONNECTIONSQLS = "initConnectionSqls";
private static final String PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED = "accessToUnderlyingConnectionAllowed";
private static final String PROP_REMOVEABANDONED = "removeAbandoned";
private static final String PROP_REMOVEABANDONEDTIMEOUT = "removeAbandonedTimeout";
private static final String PROP_LOGABANDONED = "logAbandoned";
private static final String PROP_POOLPREPAREDSTATEMENTS = "poolPreparedStatements";
private static final String PROP_MAXOPENPREPAREDSTATEMENTS = "maxOpenPreparedStatements";
private static final String PROP_CONNECTIONPROPERTIES = "connectionProperties";
private static final String[] ALL_PROPERTIES = { "defaultAutoCommit", "defaultReadOnly", "defaultTransactionIsolation", "defaultCatalog", "driverClassName", "maxActive", "maxIdle", "minIdle", "initialSize", "maxWait", "testOnBorrow", "testOnReturn", "timeBetweenEvictionRunsMillis", "numTestsPerEvictionRun", "minEvictableIdleTimeMillis", "testWhileIdle", "password", "url", "username", "validationQuery", "validationQueryTimeout", "initConnectionSqls", "accessToUnderlyingConnectionAllowed", "removeAbandoned", "removeAbandonedTimeout", "logAbandoned", "poolPreparedStatements", "maxOpenPreparedStatements", "connectionProperties" };
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
if ((obj == null) || (!(obj instanceof Reference))) {
return null;
}
Reference ref = (Reference)obj;
if (!"javax.sql.DataSource".equals(ref.getClassName())) {
return null;
}
Properties properties = new Properties();
for (int i = 0; i < ALL_PROPERTIES.length; i++)
{
String propertyName = ALL_PROPERTIES[i];
RefAddr ra = ref.get(propertyName);
if (ra != null)
{
String propertyValue = ra.getContent().toString();
properties.setProperty(propertyName, propertyValue);
}
}
return createDataSource(properties);
}
public static DataSource createDataSource(Properties properties)
throws Exception
{
BasicDataSource dataSource = new BasicDataSource();
String value = null;
value = properties.getProperty("defaultAutoCommit");
if (value != null) {
dataSource.setDefaultAutoCommit(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("defaultReadOnly");
if (value != null) {
dataSource.setDefaultReadOnly(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("defaultTransactionIsolation");
if (value != null)
{
int level = -1;
if ("NONE".equalsIgnoreCase(value)) {
level = 0;
} else if ("READ_COMMITTED".equalsIgnoreCase(value)) {
level = 2;
} else if ("READ_UNCOMMITTED".equalsIgnoreCase(value)) {
level = 1;
} else if ("REPEATABLE_READ".equalsIgnoreCase(value)) {
level = 4;
} else if ("SERIALIZABLE".equalsIgnoreCase(value)) {
level = 8;
} else {
try
{
level = Integer.parseInt(value);
}
catch (NumberFormatException e)
{
System.err.println("Could not parse defaultTransactionIsolation: " + value);
System.err.println("WARNING: defaultTransactionIsolation not set");
System.err.println("using default value of database driver");
level = -1;
}
}
dataSource.setDefaultTransactionIsolation(level);
}
value = properties.getProperty("defaultCatalog");
if (value != null) {
dataSource.setDefaultCatalog(value);
}
value = properties.getProperty("driverClassName");
if (value != null) {
dataSource.setDriverClassName(value);
}
value = properties.getProperty("maxActive");
if (value != null) {
dataSource.setMaxActive(Integer.parseInt(value));
}
value = properties.getProperty("maxIdle");
if (value != null) {
dataSource.setMaxIdle(Integer.parseInt(value));
}
value = properties.getProperty("minIdle");
if (value != null) {
dataSource.setMinIdle(Integer.parseInt(value));
}
value = properties.getProperty("initialSize");
if (value != null) {
dataSource.setInitialSize(Integer.parseInt(value));
}
value = properties.getProperty("maxWait");
if (value != null) {
dataSource.setMaxWait(Long.parseLong(value));
}
value = properties.getProperty("testOnBorrow");
if (value != null) {
dataSource.setTestOnBorrow(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("testOnReturn");
if (value != null) {
dataSource.setTestOnReturn(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("timeBetweenEvictionRunsMillis");
if (value != null) {
dataSource.setTimeBetweenEvictionRunsMillis(Long.parseLong(value));
}
value = properties.getProperty("numTestsPerEvictionRun");
if (value != null) {
dataSource.setNumTestsPerEvictionRun(Integer.parseInt(value));
}
value = properties.getProperty("minEvictableIdleTimeMillis");
if (value != null) {
dataSource.setMinEvictableIdleTimeMillis(Long.parseLong(value));
}
value = properties.getProperty("testWhileIdle");
if (value != null) {
dataSource.setTestWhileIdle(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("password");
if (value != null) {
dataSource.setPassword(decryptDBCPProperty(value));
}
value = properties.getProperty("url");
if (value != null) {
dataSource.setUrl(decryptDBCPProperty(value));
}
value = properties.getProperty("username");
if (value != null) {
dataSource.setUsername(decryptDBCPProperty(value));
}
value = properties.getProperty("validationQuery");
if (value != null) {
dataSource.setValidationQuery(value);
}
value = properties.getProperty("validationQueryTimeout");
if (value != null) {
dataSource.setValidationQueryTimeout(Integer.parseInt(value));
}
value = properties.getProperty("accessToUnderlyingConnectionAllowed");
if (value != null) {
dataSource.setAccessToUnderlyingConnectionAllowed(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("removeAbandoned");
if (value != null) {
dataSource.setRemoveAbandoned(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("removeAbandonedTimeout");
if (value != null) {
dataSource.setRemoveAbandonedTimeout(Integer.parseInt(value));
}
value = properties.getProperty("logAbandoned");
if (value != null) {
dataSource.setLogAbandoned(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("poolPreparedStatements");
if (value != null) {
dataSource.setPoolPreparedStatements(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty("maxOpenPreparedStatements");
if (value != null) {
dataSource.setMaxOpenPreparedStatements(Integer.parseInt(value));
}
value = properties.getProperty("initConnectionSqls");
if (value != null)
{
StringTokenizer tokenizer = new StringTokenizer(value, ";");
dataSource.setConnectionInitSqls(Collections.list(tokenizer));
}
value = properties.getProperty("connectionProperties");
if (value != null)
{
Properties p = getProperties(value);
Enumeration e = p.propertyNames();
while (e.hasMoreElements())
{
String propertyName = (String)e.nextElement();
dataSource.addConnectionProperty(propertyName, p.getProperty(propertyName));
}
}
if (dataSource.getInitialSize() > 0) {
dataSource.getLogWriter();
}
return dataSource;
}
private static Properties getProperties(String propText)
throws Exception
{
Properties p = new Properties();
if (propText != null) {
p.load(new ByteArrayInputStream(propText.replace(';', '\n').getBytes()));
}
return p;
}
private static String decryptDBCPProperty(String encryptStr) throws UnsupportedEncodingException, NoSuchAlgorithmException, GeneralSecurityException {
AES256Util aes = new AES256Util("testestestestses");
return aes.decrypt(encryptStr);
}
}
위 소스에서 중요내용
username, password, url 에 해당하는 부분을 복호화하도록 변경
변경전 : dataSource.setPassword(value);
변경후 : dataSource.setPassword(decryptDBCPProperty(value));
눈치빠른 분들은 아시겠지만
server.xml 에서 입력된 username, password , url 을 암호화 하는 부분이었습니다.
( 위 3가지 정보 중 필요한 부분만 암호화 처리해도 됩니다. )
STEP 5
server.xml 에 암호화 된 내용으로 개인정보 변경
여기서 중요한 부분은 STEP 4 에서 만든 factory 를 지정하여
DBCP 할 때 사용할 수 있도록 합니다.
default 값은 BasicDataSourceFactory 로 별도로 지정해주지 않으면
tomcat-dbcp.jar 에 있는 내용을 사용하게 되니
반드시 우리가 만든 factory 를 설정해주시기 바랍니다.
여기까지 작업하면 해당 내용이 암호화 처리가 됩니다!
궁금한점이나 안되는 부분있으면 댓글 달아주세요^^