HSM, 암호화 관련 테스트 수행 중 수정
This commit is contained in:
+36
-532
@@ -14,6 +14,7 @@ import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@@ -80,6 +81,7 @@ import com.eactive.eai.common.util.TxFileLogger;
|
||||
import com.eactive.eai.common.util.XMLUtils;
|
||||
import com.eactive.eai.util.JsonPathUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.kjbank.encrypt.exchange.crypto.AES256Cipher;
|
||||
@@ -124,8 +126,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : ";
|
||||
|
||||
private boolean useAdapterToken;
|
||||
private boolean encryptResponseApply;
|
||||
private String httpHeaderSettingByAdapter = "{}";
|
||||
|
||||
/**
|
||||
* 1. 기능 : REST API 통신에 사용 <br>
|
||||
@@ -161,9 +161,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
|
||||
//String inboundToken = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_TOKEN, "");
|
||||
|
||||
//Outbound 요청에 대한 응답 메시지의 복호화 여부 결정위해 jwhong
|
||||
encryptResponseApply = StringUtils.equalsIgnoreCase(prop.getProperty("ENCRYPT_RESPONSE_APPLY", "N"), "Y");
|
||||
|
||||
//업체 300응답시 특정 http status code 경우 에러 아닌걸로 처리. 광주은행
|
||||
String errIgnore = prop.getProperty("ERROR_IGNORE_CODE", "");
|
||||
|
||||
@@ -219,6 +216,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
sendData = new String((byte[]) data, vo.getEncode());
|
||||
} else if (data instanceof JSONObject) {
|
||||
sendData = data;
|
||||
} else if (data instanceof JsonNode) {
|
||||
sendData = data;
|
||||
}
|
||||
|
||||
////mclient.getParams().setContentCharset(vo.getEncode());
|
||||
@@ -228,13 +227,12 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
//Object dataContent = null;
|
||||
|
||||
if (MessageType.JSON.equals(messageType)) {
|
||||
Object parsed = JSONUtils.parseJsonGeneric(sendData);
|
||||
if(parsed instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) parsed;
|
||||
dataObject = jsonObject;
|
||||
|
||||
} else if(parsed instanceof JSONArray) {
|
||||
// not support
|
||||
if (sendData instanceof ObjectNode) {
|
||||
dataObject = sendData;
|
||||
} else if(sendData instanceof JSONObject) {
|
||||
dataObject = sendData;
|
||||
} else {
|
||||
dataObject = JsonPathUtil.toTree(sendData);
|
||||
}
|
||||
} else if (MessageType.XML.equals(messageType)) {
|
||||
Document doc = XMLUtils.convertXmlDocument(sendData);
|
||||
@@ -265,7 +263,12 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
}
|
||||
|
||||
if(dataObject instanceof JSONObject) {
|
||||
if(dataObject instanceof ObjectNode) {
|
||||
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) {
|
||||
httpHeader = ((ObjectNode)dataObject).get(headerGroupName);
|
||||
((ObjectNode)dataObject).remove(headerGroupName);
|
||||
}
|
||||
} else if(dataObject instanceof JSONObject) {
|
||||
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) {
|
||||
httpHeader = ((JSONObject)dataObject).get(headerGroupName);
|
||||
((JSONObject)dataObject).remove(headerGroupName);
|
||||
@@ -885,26 +888,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
// String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
/* nice clientid 추출을 위하여 추가한 내용임. jwhong
|
||||
*
|
||||
*/
|
||||
private String JwtClientIdExtractor(String inboundToken) {
|
||||
|
||||
String tokenClientId ="";
|
||||
try {
|
||||
SignedJWT signedJWT = SignedJWT.parse(inboundToken);
|
||||
tokenClientId = (String) signedJWT.getJWTClaimsSet().getClaim(PAYLOAD_PARAM_NAME_CLIENT_ID);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
tokenClientId = "";
|
||||
}
|
||||
|
||||
logger.debug("[ NICE Token Client ID ]" + tokenClientId);
|
||||
return tokenClientId;
|
||||
|
||||
}
|
||||
|
||||
|
||||
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
|
||||
String encode) {
|
||||
if (responseMessage == null) {
|
||||
@@ -937,7 +920,13 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
|
||||
protected void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader, Properties prop) {
|
||||
if (httpHeader != null) {
|
||||
if (httpHeader instanceof JSONObject) {
|
||||
if ( httpHeader instanceof ObjectNode) {
|
||||
ObjectNode objectNode = (ObjectNode) httpHeader;
|
||||
objectNode.fields().forEachRemaining(e -> {
|
||||
JsonNode value = e.getValue();
|
||||
method.setHeader((String) e.getKey(), value.isNull() ? null : value.asText());
|
||||
});
|
||||
} else if (httpHeader instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) httpHeader;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
Object obj = jsonObject.get(key);
|
||||
@@ -1008,79 +997,7 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* KJBank Target System으로 요청 보낼때 header에 clientId(업체정보), APIM거래추적자(UUID), 최초 요청시간, guid 추가해서 보낸다. jwhong
|
||||
* token 만 return 하는것으로 수정함
|
||||
*/
|
||||
private String assignRequestKJHeaders(HttpUriRequestBase method, Properties prop, Properties tempProp){
|
||||
|
||||
//weblogic 에서는 tempProp.get 이 property로 return 해어 아래처럼 수정함. ClassCastException 발생함.
|
||||
//Properties inboundHeaders = (Properties) tempProp.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
Properties inboundHeaders = null;
|
||||
String authorization ="";
|
||||
try {
|
||||
Object headerObj = tempProp.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
inboundHeaders = (Properties) headerObj;
|
||||
for ( String k : inboundHeaders.stringPropertyNames()) {
|
||||
if(k.equalsIgnoreCase("authorization")) {
|
||||
authorization = inboundHeaders.getProperty(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// 2. key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
// map에 담기
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
map.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
for (String k : map.keySet()) {
|
||||
if (k.equalsIgnoreCase("authorization")) {
|
||||
authorization = map.get(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
String jwtToken = "";
|
||||
|
||||
if (authorization != null && authorization.startsWith("Bearer ")) {
|
||||
jwtToken = authorization.substring(7); // "Bearer " 이후의 JWT만 추출
|
||||
}
|
||||
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseInboundHeader(String str) {
|
||||
java.util.HashMap<String, String> map = new java.util.HashMap<>();
|
||||
if (str == null || str.isEmpty()) return map;
|
||||
|
||||
// 중괄호 제거
|
||||
str = str.trim();
|
||||
if (str.startsWith("{") && str.endsWith("}")) {
|
||||
str = str.substring(1, str.length() - 1);
|
||||
}
|
||||
|
||||
// key=value 쌍 분리
|
||||
String[] pairs = str.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] kv = pair.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
map.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
protected String assignPostBody(Object eaiBody, String mimeType, String charset, HttpUriRequestBase method,
|
||||
boolean chunked) throws Exception {
|
||||
@@ -1102,6 +1019,12 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key)));
|
||||
}
|
||||
} else if (eaiBody instanceof ObjectNode) {
|
||||
ObjectNode objectNode = (ObjectNode) eaiBody;
|
||||
objectNode.fields().forEachRemaining(e -> {
|
||||
JsonNode value = e.getValue();
|
||||
params.add(new BasicNameValuePair(e.getKey(), value.isNull() ? null : value.asText()));
|
||||
});
|
||||
} else if (eaiBody instanceof Document) {
|
||||
Document bodyDoc = (Document) eaiBody;
|
||||
Element root = bodyDoc.getRootElement();
|
||||
@@ -1143,429 +1066,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
}
|
||||
|
||||
// from here by jwhong
|
||||
private String doOutboundPreFilter(Object eaiBody, String adapterName, String niceAuth, String inboundToken, Date currentDate) throws Exception {
|
||||
|
||||
Properties properties = AdapterPropManager.getInstance().getProperties(adapterName); // jwhong
|
||||
String encryptAlgorithm = properties.getProperty("ENCRYPT_ALGORITHM",""); // jwhong
|
||||
String encryptBaseKeyType = properties.getProperty("ENCRYPT_BASE_TYPE");
|
||||
String encryptClientId = properties.getProperty("ENCRYPT_CLIENT_ID");
|
||||
String encryptAES256Iv = properties.getProperty("ENCRYPT_AES256_IV");
|
||||
String encryptAES256Key = properties.getProperty("ENCRYPT_AES256_KEY");
|
||||
String certPath = properties.getProperty("ENCRYPT_CERT_PATH");
|
||||
|
||||
// outbound encrypt 는 SecretKey 또는 NICE-auth 가 암호화 key로 사용된다.
|
||||
// outbound 알림결과는 SecretKey, NICE-auth, Token을 사용한다.
|
||||
// 참고로 inbound encrypt 에는 Token 또는 SecretKey 가 암호화 key로 사용된다.
|
||||
// 여기에 NICE-auth logic 추가함 .
|
||||
String clientSecret = "";
|
||||
if ("TOKEN".equalsIgnoreCase(encryptBaseKeyType)) {
|
||||
clientSecret = inboundToken;
|
||||
} else if ( "SECRETKEY".equalsIgnoreCase(encryptBaseKeyType)) {
|
||||
OAuth2Manager manager = OAuth2Manager.getInstance();
|
||||
ClientDetails clientDetail = manager.getClientDeatilsStore().get(encryptClientId);
|
||||
if ( clientDetail != null ) { // not null 이라는 것은 APIM Oauth Server에 등록된 client ID 이다.
|
||||
// 업체가 우리 OAuth 를 사용하는 경우임
|
||||
clientSecret = clientDetail.getClientSecret();
|
||||
} else { // 이경우는 우리 OAuth를 사용안하는 경우임, 업체에서 사용하는 secretkey를 사용한다.
|
||||
clientSecret = encryptClientId;
|
||||
}
|
||||
}
|
||||
|
||||
// Nice-auth , token 값을 substring 해서 사용한다. 나이스 지키미는 adapter token에서 추출된 token을 사용한다.
|
||||
if (useAdapterToken && "AES256-NICEON".equals(encryptAlgorithm)) {
|
||||
clientSecret = niceAuth;
|
||||
}
|
||||
|
||||
// 여기서 SECRETY인지 nice-auth 인지 check 하여 clientSecret 값을 정하는 logic 추가하여 넘겨야한다.
|
||||
|
||||
String responseMessage = doOutboundPreEncrypt(eaiBody, encryptAlgorithm, clientSecret, encryptAES256Iv, encryptAES256Key, certPath, currentDate);
|
||||
logger.info("outbound pre encrypte value is :"+responseMessage);
|
||||
return responseMessage;
|
||||
|
||||
}
|
||||
private String doOutboundPreEncrypt(Object eaiBody, String encryptAlgorithm, String clientSecretKey, String encryptAES256Iv, String encryptAES256Key, String certPath, Date currentDate) throws Exception {
|
||||
|
||||
String encryptedMessage = "";
|
||||
String encryptedJsonKey = "";
|
||||
|
||||
encryptedJsonKey = getJsonKey(encryptAlgorithm);
|
||||
|
||||
switch (encryptAlgorithm) {
|
||||
case "AES":
|
||||
encryptedMessage = encrypt(eaiBody.toString()); // encrypt by jwhong
|
||||
break;
|
||||
case "AES128":
|
||||
case "AES128-TOSS":
|
||||
try {
|
||||
String key = clientSecretKey.substring(22,38);
|
||||
encryptedMessage = AESCipher.encrypt(eaiBody.toString(), key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES128 Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES128 Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "AES128-TOGETHER":
|
||||
try {
|
||||
String key = clientSecretKey.substring(44,60);
|
||||
encryptedMessage = AESCipher.encrypt(eaiBody.toString(), key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES128 Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES128 Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "AES256":
|
||||
try {
|
||||
String key = clientSecretKey.substring(10,42);
|
||||
encryptedMessage = AES256Cipher.encrypt(eaiBody.toString(), key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES256 Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "AES256-KAKAO":
|
||||
try {
|
||||
encryptedMessage = AES256Cipher.encrypt(eaiBody.toString(), encryptAES256Iv, encryptAES256Key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES256 Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "AES256-TOSS":
|
||||
try {
|
||||
encryptedMessage = AES256Cipher.encrypt(eaiBody.toString(), encryptAES256Iv, encryptAES256Key);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES256 Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "AES256-NICEON":
|
||||
try {
|
||||
String key = clientSecretKey.substring(10,42);
|
||||
logger.debug("HttpClientAdapterServiceRest] NICE ON AES256 Encryption key is :+="+key);
|
||||
encryptedMessage = AES256Cipher.encrypt(eaiBody.toString(), key);
|
||||
//encryptedMessage = ExchangeCrypto.encodedData(eaiStringBody, clientSecretKey);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES256 Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "AES256GCM":
|
||||
try {
|
||||
byte[] key = AES256GCMCipher.generateKey();
|
||||
AES256GCMCipher cipher = new AES256GCMCipher(key);
|
||||
byte[] aad = "metadata".getBytes();
|
||||
|
||||
encryptedMessage = cipher.encrypt(eaiBody.toString(), aad);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256GCM Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, AES256GCM Key Invalid");
|
||||
}
|
||||
break;
|
||||
case "ECDHESA256":
|
||||
try {
|
||||
ECDHESA256Cipher cipher = new ECDHESA256Cipher();
|
||||
|
||||
encryptedMessage = cipher.encrypt(eaiBody.toString());
|
||||
// System.out.println("Encrypted JWE: " + encryptedMessage);
|
||||
|
||||
String decrypted = cipher.decrypt(encryptedMessage);
|
||||
// System.out.println("Decrypted Payload: " + decrypted);
|
||||
|
||||
//아래 인증서로 테스트
|
||||
ECDHESA256Cipher cipherCert = new ECDHESA256Cipher(certPath);
|
||||
encryptedMessage = cipherCert.encrypt(eaiBody.toString());
|
||||
|
||||
decrypted = cipherCert.decrypt(encryptedMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256GCM Encryption error=" + e.getMessage());
|
||||
throw new Exception("[HttpClient5AdapterServiceRest] Pre Encrypt Error : Encrypted Error, ECDHESA256 Key Invalid");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
encryptedMessage = eaiBody.toString();
|
||||
return encryptedMessage;
|
||||
//break;
|
||||
}
|
||||
|
||||
if (encryptAlgorithm.equals("AES256-NICEON")) {
|
||||
String goodsCls = PropManager.getInstance().getProperty("NiceOnProperty","goods.cls.value");
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
SimpleDateFormat sdfDateTime =new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
map.put("req_dtm", sdfDateTime.format(currentDate));
|
||||
map.put("goods_cls", goodsCls);
|
||||
map.put(encryptedJsonKey, encryptedMessage);
|
||||
JSONObject dataBody = new JSONObject();
|
||||
dataBody.putAll(map);
|
||||
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("dataBody", dataBody);
|
||||
return json.toJSONString();
|
||||
} else {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put(encryptedJsonKey, encryptedMessage);
|
||||
JSONObject json = new JSONObject();
|
||||
json.putAll(map);
|
||||
return json.toJSONString();
|
||||
}
|
||||
|
||||
//return encryptedMessage;
|
||||
}
|
||||
|
||||
|
||||
private byte[] doOutboundPostFilter(byte[] eaiBody, String decryptAlgorithm, String adapterName, OAuth2AccessTokenVO niceAccessToken ) {
|
||||
|
||||
if ( !encryptResponseApply) {
|
||||
return eaiBody;
|
||||
}
|
||||
|
||||
Properties properties = AdapterPropManager.getInstance().getProperties(adapterName); // jwhong
|
||||
//String encryptAlgorithm = properties.getProperty("ENCRYPT_ALGORITHM"); // jwhong
|
||||
//String encryptBaseKeyType = properties.getProperty("ENCRYPT_BASE_TYPE");
|
||||
String encryptClientId = properties.getProperty("ENCRYPT_CLIENT_ID");
|
||||
String encryptAES256Iv = properties.getProperty("ENCRYPT_AES256_IV");
|
||||
String encryptAES256Key = properties.getProperty("ENCRYPT_AES256_KEY");
|
||||
|
||||
// outbound encrypt 는 SecretKey 또는 NICE-auth 가 암호화 key로 사용된다.
|
||||
OAuth2Manager manager = OAuth2Manager.getInstance();
|
||||
ClientDetails clientDetail = manager.getClientDeatilsStore().get(encryptClientId);
|
||||
String clientSecret = "";
|
||||
if ( clientDetail != null ) {
|
||||
clientSecret = clientDetail.getClientSecret();
|
||||
} else {
|
||||
clientSecret = encryptClientId;
|
||||
}
|
||||
|
||||
// Nice-auth , token 값을 substring 해서 사용한다. 나이스 지키미는 adapter token에서 추출된 token을 사용한다.
|
||||
if (useAdapterToken && "AES256-NICEON".equals(decryptAlgorithm)) {
|
||||
clientSecret = niceAccessToken.getAccessToken();
|
||||
}
|
||||
|
||||
byte[] resPostMessage = doOutboundPostDecrypt(eaiBody, decryptAlgorithm, clientSecret, encryptAES256Iv, encryptAES256Key);
|
||||
return resPostMessage;
|
||||
}
|
||||
|
||||
private byte[] doOutboundPostDecrypt(byte[] eaiBody, String decryptAlgorithm, String clientSecretKey, String encryptAES256Iv, String encryptAES256Key ) {
|
||||
|
||||
byte[] decryptedMessage = null;
|
||||
String eaiStringBody = new String(eaiBody, StandardCharsets.UTF_8);
|
||||
|
||||
try {
|
||||
eaiStringBody = extractBodyMsg(decryptAlgorithm, eaiStringBody);
|
||||
} catch (ParseException e) {
|
||||
logger.debug("[JSON 파싱 오류 발생 : ]" +e.getMessage());
|
||||
return eaiBody;
|
||||
}
|
||||
|
||||
//test source
|
||||
/*
|
||||
logger.debug("eaiBody 바이트 배열 길이: " + eaiBody.length);
|
||||
logger.debug("Base64 문자열: " + eaiStringBody);
|
||||
logger.debug("Base64 문자열 길이: " + eaiStringBody.length());
|
||||
logger.debug("Base64 문자열 끝: " + eaiStringBody.substring(eaiStringBody.length() - 10));
|
||||
*/
|
||||
|
||||
// Base64 디코딩 테스트
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(eaiStringBody);
|
||||
logger.debug("[디코딩 성공, 길이: ]" + decoded.length);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("[Base64 디코딩 실패: ]" + e.getMessage());
|
||||
}
|
||||
// end test source
|
||||
|
||||
switch (decryptAlgorithm) {
|
||||
case "AES":
|
||||
decryptedMessage = decrypt(eaiBody); // encrypt by jwhong
|
||||
break;
|
||||
case "AES128":
|
||||
case "AES128-TOSS":
|
||||
try {
|
||||
String key128 = clientSecretKey.substring(22,38);
|
||||
String decryptedStrMessage = AESCipher.decryptExistException(eaiStringBody, key128);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES128 Decryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case "AES128-TOGETHER":
|
||||
try {
|
||||
String key128 = clientSecretKey.substring(44,60);
|
||||
String decryptedStrMessage = AESCipher.decryptExistException(eaiStringBody, key128);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES128 Decryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case "AES256":
|
||||
try {
|
||||
String key = clientSecretKey.substring(10,42);
|
||||
String decryptedStrMessage = AES256Cipher.decrypt(eaiStringBody, key);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Decryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case "AES256-KAKAO":
|
||||
try {
|
||||
String decryptedStrMessage = AES256Cipher.decrypt(eaiStringBody, encryptAES256Iv, encryptAES256Key);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case "AES256-TOSS":
|
||||
try {
|
||||
String decryptedStrMessage = AES256Cipher.decrypt(eaiStringBody, encryptAES256Iv, encryptAES256Key);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case "AES256-NICEON":
|
||||
try {
|
||||
String key = clientSecretKey.substring(10,42);
|
||||
String decryptedStrMessage = AES256Cipher.decrypt(eaiStringBody, key);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Decryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
case "AES256GCM":
|
||||
try {
|
||||
byte[] key = AES256GCMCipher.generateKey();
|
||||
AES256GCMCipher cipher = new AES256GCMCipher(key);
|
||||
byte[] aad = "metadata".getBytes();
|
||||
String decryptedStrMessage = cipher.decrypt(eaiStringBody, aad);
|
||||
decryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] AES256 Decryption error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
decryptedMessage = Arrays.copyOf(eaiBody, eaiBody.length);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClientAdapterServiceRest] doOutboundPostFilter Array copy error=" + e.getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return decryptedMessage;
|
||||
}
|
||||
|
||||
|
||||
private String extractBodyMsg(String decryptAlgorithm, String eaiStringBody) throws ParseException {
|
||||
|
||||
String decryptedJsonKey = "";
|
||||
String decryptedBodyMessage = "";
|
||||
|
||||
decryptedJsonKey = getJsonKey(decryptAlgorithm);
|
||||
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
JSONObject jsonObject = (JSONObject) parser.parse(eaiStringBody);
|
||||
decryptedBodyMessage = (String) jsonObject.get(decryptedJsonKey);
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("HttpClient5AdapterServiceRest] extractBodyMsg error : " + e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
return decryptedBodyMessage;
|
||||
|
||||
}
|
||||
|
||||
private String getJsonKey(String Algorithm) {
|
||||
|
||||
String jsonKey = "";
|
||||
|
||||
switch (Algorithm) {
|
||||
case "AES128":
|
||||
case "AES128-TOSS":
|
||||
jsonKey = "preScreeningRequest";
|
||||
break;
|
||||
case "AES128-TOGETHER":
|
||||
jsonKey = "obpTxData";
|
||||
break;
|
||||
case "AES256":
|
||||
jsonKey = "preScreeningRequest";
|
||||
break;
|
||||
case "AES256-KAKAO":
|
||||
jsonKey = "encrypted_data";
|
||||
break;
|
||||
case "AES256-TOSS":
|
||||
jsonKey = "encryptedData";
|
||||
break;
|
||||
case "AES256-NICEON":
|
||||
jsonKey = "enc_data";
|
||||
break;
|
||||
case "AES256GCM":
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return jsonKey;
|
||||
}
|
||||
// by jwhong
|
||||
private String encrypt(String plainText) {
|
||||
try {
|
||||
SecretKeySpec key = new SecretKeySpec("1234567890123456".getBytes(), "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
// 예외 로그 출력 또는 사용자 정의 처리
|
||||
e.printStackTrace();
|
||||
return null; // 또는 에러 메시지 반환
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static byte[] decrypt(byte[] encryptedBytes) {
|
||||
try {
|
||||
// byte[] --> Base64 문자열로 변환
|
||||
String encryptedText = new String(encryptedBytes, StandardCharsets.UTF_8).trim();
|
||||
// 암호화에 사용한 키와 동일한 키 사용
|
||||
SecretKeySpec key = new SecretKeySpec("1234567890123456".getBytes(), "AES");
|
||||
|
||||
// 암호화와 동일한 알고리즘 설정
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
|
||||
// Base64로 인코딩된 문자열을 디코딩
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(encryptedText);
|
||||
|
||||
// 복호화 수행
|
||||
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
|
||||
|
||||
// UTF-8 문자열로 변환하여 반환
|
||||
//return new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||
//복호화된 바이트 배열 반환
|
||||
return decryptedBytes;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private HashMap<String, String> getParameters(Object message) throws Exception {
|
||||
@@ -1573,7 +1073,13 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
HashMap<String, String> result = new HashMap<String, String>();
|
||||
|
||||
if (message != null) {
|
||||
if (message instanceof JSONObject) {
|
||||
if (message instanceof JsonNode) {
|
||||
ObjectNode objectNode = (ObjectNode) message;
|
||||
objectNode.fields().forEachRemaining(e -> {
|
||||
JsonNode value = e.getValue();
|
||||
result.put(e.getKey(), value.isNull() ? null : value.asText());
|
||||
});
|
||||
} else if (message instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) message;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
Object obj = jsonObject.get(key);
|
||||
@@ -1618,7 +1124,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
if (StringUtils.isBlank(jsonData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
Object parsedObj = parser.parse(jsonData);
|
||||
@@ -1644,7 +1149,6 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
return document;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private String changeUrl(String messageType, String url, String restOption, Object sendData, Map<String, String> inboundPathVariables) {
|
||||
if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) {
|
||||
try {
|
||||
|
||||
@@ -31,7 +31,11 @@ public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterF
|
||||
* 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다.
|
||||
*/
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
|
||||
return new HashMap<>();
|
||||
Map<String, String> context = new HashMap<>();
|
||||
for (String key : prop.stringPropertyNames()) {
|
||||
context.put(key, prop.getProperty(key));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +47,9 @@ public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterF
|
||||
if (StringUtils.isBlank(headerName) || request == null) {
|
||||
return null;
|
||||
}
|
||||
String headerValue = request.getHeader(headerName);
|
||||
Properties inboundHeaderProp = (Properties)prop.get("INBOUND_HEADER");
|
||||
String headerValue = inboundHeaderProp.getProperty(headerName);
|
||||
// String headerValue = request.getHeader(headerName);
|
||||
return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ public class EncryptionManager implements Lifecycle {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isBase64(String str) {
|
||||
public static boolean isBase64(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -44,9 +44,25 @@ public class HsmCryptoService implements PropertyChangeListener {
|
||||
private static final String PROP_GROUP = "HSM";
|
||||
private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";
|
||||
|
||||
private static final long CACHE_TTL_MS = 10 * 60 * 1000; // 10분, 필요시 PropManager로 외부화
|
||||
|
||||
private static class CachedKey<T> {
|
||||
final T key;
|
||||
final long cachedAt;
|
||||
|
||||
CachedKey(T key) {
|
||||
this.key = key;
|
||||
this.cachedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() - cachedAt > CACHE_TTL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
// HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
|
||||
private final ConcurrentHashMap<String, SecretKey> secretKeyCache = new ConcurrentHashMap<String, SecretKey>();
|
||||
private final ConcurrentHashMap<String, PublicKey> publicKeyCache = new ConcurrentHashMap<String, PublicKey>();
|
||||
private final ConcurrentHashMap<String, CachedKey<SecretKey>> secretKeyCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, CachedKey<PublicKey>> publicKeyCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private PropManager propManager;
|
||||
@@ -81,9 +97,9 @@ public class HsmCryptoService implements PropertyChangeListener {
|
||||
*/
|
||||
public PublicKey getPublicKey(String keyAlias) throws HsmException {
|
||||
checkReady();
|
||||
PublicKey cached = publicKeyCache.get(keyAlias);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
CachedKey<PublicKey> cached = publicKeyCache.get(keyAlias);
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
return cached.key;
|
||||
}
|
||||
try {
|
||||
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
|
||||
@@ -91,7 +107,7 @@ public class HsmCryptoService implements PropertyChangeListener {
|
||||
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
PublicKey key = cert.getPublicKey();
|
||||
publicKeyCache.put(keyAlias, key);
|
||||
publicKeyCache.put(keyAlias, new CachedKey<>(key));
|
||||
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
|
||||
return key;
|
||||
} catch (HsmException e) {
|
||||
@@ -107,20 +123,21 @@ public class HsmCryptoService implements PropertyChangeListener {
|
||||
*/
|
||||
public SecretKey getSecretKey(String keyAlias) throws HsmException {
|
||||
checkReady();
|
||||
SecretKey cached = secretKeyCache.get(keyAlias);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
CachedKey<SecretKey> cached = secretKeyCache.get(keyAlias);
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
return cached.key;
|
||||
}
|
||||
try {
|
||||
KeyStore keyStore = HsmManager.getInstance().getKeyStore();
|
||||
java.security.Key key = keyStore.getKey(keyAlias, null);
|
||||
if (!(key instanceof SecretKey)) {
|
||||
throw new HsmException("AES 키를 찾을 수 없습니다 (CKA_EXTRACTABLE=true 확인 필요). alias=" + keyAlias);
|
||||
if (key == null) {
|
||||
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
SecretKey secretKey = (SecretKey) key;
|
||||
secretKeyCache.put(keyAlias, secretKey);
|
||||
logger.warn("HsmCryptoService] 대칭키 캐시 등록: alias=" + keyAlias);
|
||||
secretKeyCache.put(keyAlias, new CachedKey<>(secretKey));
|
||||
logger.warn("HsmCryptoService] 대칭키 캐시 등록(갱신): alias=" + keyAlias);
|
||||
return secretKey;
|
||||
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -7,10 +7,12 @@ import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.security.KeyStore;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
@@ -52,11 +54,18 @@ public class HsmManager implements Lifecycle {
|
||||
private static final String PROP_PIN = "PIN";
|
||||
|
||||
private Provider pkcs11Provider;
|
||||
private KeyStore keyStore;
|
||||
private volatile KeyStore keyStore;
|
||||
private boolean started;
|
||||
|
||||
private final LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
private volatile char[] pin;
|
||||
|
||||
// 재로드 주기 (분 단위). 필요시 PropManager로 외부화 가능.
|
||||
private static final long RELOAD_INTERVAL_MINUTES = 1;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private ScheduledFuture<?> reloadFuture;
|
||||
|
||||
private HsmManager() {
|
||||
}
|
||||
|
||||
@@ -73,6 +82,7 @@ public class HsmManager implements Lifecycle {
|
||||
|
||||
try {
|
||||
init();
|
||||
startReloadScheduler();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
|
||||
}
|
||||
@@ -85,7 +95,7 @@ public class HsmManager implements Lifecycle {
|
||||
EncryptionManager encManager = EncryptionManager.getInstance();
|
||||
|
||||
String configContent = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
|
||||
String pin = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));
|
||||
String pinStr = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));
|
||||
|
||||
if (configContent == null || configContent.trim().isEmpty()) {
|
||||
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
|
||||
@@ -102,8 +112,8 @@ public class HsmManager implements Lifecycle {
|
||||
Security.addProvider(pkcs11Provider);
|
||||
|
||||
keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
|
||||
char[] pinChars = (pin != null) ? pin.toCharArray() : null;
|
||||
keyStore.load(null, pinChars);
|
||||
this.pin = (pinStr != null) ? pinStr.toCharArray() : null;
|
||||
keyStore.load(null, pin);
|
||||
|
||||
logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
|
||||
|
||||
@@ -129,6 +139,66 @@ public class HsmManager implements Lifecycle {
|
||||
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 별도 스레드에서 주기적으로 KeyStore.load() 를 다시 호출하여
|
||||
* HSM 에 새로 생성/추가된 키를 인식하도록 한다.
|
||||
*/
|
||||
private void startReloadScheduler() {
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "hsm-keystore-reloader");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
reloadFuture = scheduler.scheduleWithFixedDelay(
|
||||
this::reloadKeyStoreSafely,
|
||||
RELOAD_INTERVAL_MINUTES,
|
||||
RELOAD_INTERVAL_MINUTES,
|
||||
TimeUnit.MINUTES
|
||||
);
|
||||
|
||||
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + RELOAD_INTERVAL_MINUTES + "분");
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다.
|
||||
* (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다)
|
||||
*/
|
||||
private void reloadKeyStoreSafely() {
|
||||
try {
|
||||
reloadKeyStoreIfNeeded();
|
||||
} catch (Throwable t) {
|
||||
logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
|
||||
* 즉시 재시도용으로 직접 호출할 수도 있다.
|
||||
*/
|
||||
public synchronized void reloadKeyStoreIfNeeded() throws Exception {
|
||||
if (pkcs11Provider == null) {
|
||||
return; // HSM 비활성화 상태
|
||||
}
|
||||
|
||||
KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
|
||||
ks.load(null, pin);
|
||||
this.keyStore = ks; // volatile 필드 교체 - 다른 스레드에서 즉시 가시성 확보
|
||||
|
||||
logger.warn("HsmManager] KeyStore 재로드 완료.");
|
||||
logAliases(ks);
|
||||
}
|
||||
|
||||
private void logAliases(KeyStore ks) throws Exception {
|
||||
java.util.Enumeration<String> aliases = ks.aliases();
|
||||
StringBuilder aliasList = new StringBuilder();
|
||||
while (aliases.hasMoreElements()) {
|
||||
if (aliasList.length() > 0) aliasList.append(", ");
|
||||
aliasList.append(aliases.nextElement());
|
||||
}
|
||||
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
|
||||
*
|
||||
@@ -164,6 +234,13 @@ public class HsmManager implements Lifecycle {
|
||||
}
|
||||
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
|
||||
|
||||
if (reloadFuture != null) {
|
||||
reloadFuture.cancel(false);
|
||||
}
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
if (pkcs11Provider != null) {
|
||||
Security.removeProvider(pkcs11Provider.getName());
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ public class CryptoModuleManager implements Lifecycle {
|
||||
private KeyDerivationStrategy resolveStrategy(String fqcn) {
|
||||
return strategyCache.computeIfAbsent(fqcn, key -> {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(key);
|
||||
Class<?> clazz = Class.forName(key.trim());
|
||||
return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e);
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HSM 마스터키 + 런타임 컨텍스트값을 연결(concatenate)한 뒤 SHA-256 해싱으로 키를 도출하는 전략.
|
||||
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
|
||||
*
|
||||
* key_deriv_params (JSON) 예시:
|
||||
* {
|
||||
* "hsmKeyAlias" : "MASTER_KEY_AES",
|
||||
* "contextKey" : "X-Api-Group-Seq" -- runtimeContext에서 꺼낼 키 이름
|
||||
* }
|
||||
*/
|
||||
public class HsmContextSha256KeyDerivationStrategy implements KeyDerivationStrategy {
|
||||
|
||||
@Override
|
||||
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
|
||||
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
|
||||
String contextKey = required(params, PARAM_CONTEXT_KEY);
|
||||
|
||||
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
|
||||
byte[] masterKeyBytes = masterKey.getEncoded();
|
||||
byte[] contextBytes = runtimeContext.getOrDefault(contextKey, "")
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] combined = new byte[masterKeyBytes.length + contextBytes.length];
|
||||
System.arraycopy(masterKeyBytes, 0, combined, 0, masterKeyBytes.length);
|
||||
System.arraycopy(contextBytes, 0, combined, masterKeyBytes.length, contextBytes.length);
|
||||
|
||||
byte[] derived = MessageDigest.getInstance("SHA-256").digest(combined);
|
||||
return new DerivedKey(derived, derived);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
|
||||
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
|
||||
String contextValue = runtimeContext.getOrDefault(contextKey, "");
|
||||
return cryptoName + ":" + contextValue;
|
||||
}
|
||||
|
||||
private String required(Map<String, String> params, String key) {
|
||||
String value = params.get(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private HsmCryptoService hsmCryptoService() {
|
||||
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,33 @@ public class HexaConverter {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static String binToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static byte[] hexToBin(String hexStr) {
|
||||
String lookup = "0123456789ABCDEF";
|
||||
|
||||
int len = hexStr.length();
|
||||
byte[] bArray = new byte[len / 2];
|
||||
|
||||
for (int i = 0; i < bArray.length; i++) {
|
||||
char highChar = Character.toUpperCase(hexStr.charAt(i * 2));
|
||||
char lowChar = Character.toUpperCase(hexStr.charAt(i * 2 + 1));
|
||||
|
||||
int high = lookup.indexOf(highChar);
|
||||
int low = lookup.indexOf(lowChar);
|
||||
|
||||
bArray[i] = (byte) ((high << 4) | low);
|
||||
}
|
||||
|
||||
return bArray;
|
||||
}
|
||||
|
||||
// public static void main(String[] argv) {
|
||||
// String str = "TEST\nÇѱÛ";
|
||||
// String hexStr = HexaConverter.bytesToHexa(str.getBytes());
|
||||
|
||||
-466
@@ -1,466 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.InCryptoFilter;
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.hsm.HsmManager;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.security.CryptoModuleConfigVO;
|
||||
import com.eactive.eai.common.security.CryptoModuleManager;
|
||||
import com.eactive.eai.common.security.CryptoModuleService;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmKeyDerivationStrategy;
|
||||
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
|
||||
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
|
||||
/**
|
||||
* CryptoFilter HSM 연동 통합 테스트 (SoftHSM2 / SunPKCS11)
|
||||
*
|
||||
* SoftHSM2에서 MASTER_KEY를 조회하여 키를 도출한 뒤
|
||||
* InCryptoFilter 의 암복호화 전체 흐름을 검증한다.
|
||||
*
|
||||
* 사전 조건:
|
||||
* C:\SoftHSM2 에 SoftHSM2 설치 (미설치 시 자동 건너뜀)
|
||||
* MASTER_KEY alias는 없으면 자동 생성됨
|
||||
*
|
||||
* 검증 전략:
|
||||
* HSM → 키 도출 → CryptoModuleExtension → InCryptoFilter doPostFilter(암호화) → doPreFilter(복호화) → 원문 일치
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
public class CryptoFilterHsmIntegrationTest {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 상수
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static final String SOFTHSM2_DLL = "C:/SoftHSM2/lib/softhsm2-x64.dll";
|
||||
private static final String SOFTHSM2_UTIL = "C:/SoftHSM2/bin/softhsm2-util.exe";
|
||||
private static final String TOKEN_LABEL = "eapim-test";
|
||||
private static final String PIN = "1234";
|
||||
private static final String MASTER_KEY_ALIAS = "MASTER_KEY";
|
||||
|
||||
/** HsmKeyDerivationStrategy: HSM 마스터키를 파생 없이 직접 사용 */
|
||||
private static final String MOD_HSM_GCM = "HSM_GCM";
|
||||
/** HsmContextSha256KeyDerivationStrategy: HSM 마스터키 + 컨텍스트값 SHA-256 파생 */
|
||||
private static final String MOD_CTX_SHA_GCM = "CTX_SHA256_GCM";
|
||||
|
||||
private static final String CTX_KEY = "X-Api-Group-Seq";
|
||||
private static final String CTX_VAL_A = "GROUP-A";
|
||||
private static final String CTX_VAL_B = "GROUP-B";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 정적 필드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static GenericApplicationContext springCtx;
|
||||
private static HsmManager hsmManager;
|
||||
private static File tempCfgFile;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 내부 클래스: runtimeContext를 고정값으로 반환하는 필터
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static class ContextAwareCryptoFilter extends InCryptoFilter {
|
||||
private final String key;
|
||||
private final String value;
|
||||
|
||||
ContextAwareCryptoFilter(String key, String value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
|
||||
Map<String, String> ctx = new HashMap<>();
|
||||
ctx.put(key, value);
|
||||
return ctx;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 인스턴스 필드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private HttpServletRequest mockReq;
|
||||
private HttpServletResponse mockRes;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 전체 설정 / 해제
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@BeforeAll
|
||||
static void setUpAll() throws Exception {
|
||||
assumeTrue(new File(SOFTHSM2_DLL).exists(),
|
||||
"SoftHSM2 미설치 (C:/SoftHSM2) - 테스트 건너뜀");
|
||||
System.setProperty("SOFTHSM2_CONF", "C:\\SoftHSM2\\etc\\softhsm2.conf");
|
||||
|
||||
// 1. 토큰 초기화
|
||||
ensureTokenInitialized();
|
||||
|
||||
// 2. PKCS11 설정 문자열
|
||||
// attributes(*,...): generate/import 모두 CKA_SENSITIVE=false → getEncoded() 사용 가능
|
||||
String cfgContent = "name = SoftHSM\n"
|
||||
+ "library = " + SOFTHSM2_DLL + "\n"
|
||||
+ "slotListIndex = 0\n"
|
||||
+ "attributes(*, CKO_SECRET_KEY, CKK_AES) = {\n"
|
||||
+ " CKA_SENSITIVE = false\n"
|
||||
+ " CKA_EXTRACTABLE = true\n"
|
||||
+ "}\n";
|
||||
tempCfgFile = File.createTempFile("pkcs11-filter-it-", ".cfg");
|
||||
Files.write(tempCfgFile.toPath(), cfgContent.getBytes("UTF-8"));
|
||||
|
||||
// 3. Mock PropManager (DB 없이 HSM 설정값 제공)
|
||||
PropManager mockPropManager = mock(PropManager.class);
|
||||
when(mockPropManager.getProperty("HSM", "PKCS11_CONFIG")).thenReturn(cfgContent);
|
||||
when(mockPropManager.getProperty("HSM", "PIN")).thenReturn(PIN);
|
||||
when(mockPropManager.getProperty(eq("HSM"), eq("CACHE_RELOAD_YN"), anyString())).thenReturn("N");
|
||||
|
||||
// 4. HsmManager (private 생성자 → 리플렉션)
|
||||
Constructor<HsmManager> hsmCtor = HsmManager.class.getDeclaredConstructor();
|
||||
hsmCtor.setAccessible(true);
|
||||
hsmManager = hsmCtor.newInstance();
|
||||
|
||||
// 5. CryptoModuleManager (private 생성자 → 리플렉션)
|
||||
CryptoModuleConfigLoader mockLoader = mock(CryptoModuleConfigLoader.class);
|
||||
when(mockLoader.findAll()).thenReturn(buildModuleConfigs());
|
||||
|
||||
Constructor<CryptoModuleManager> managerCtor = CryptoModuleManager.class.getDeclaredConstructor();
|
||||
managerCtor.setAccessible(true);
|
||||
CryptoModuleManager manager = managerCtor.newInstance();
|
||||
|
||||
CryptoModuleService cryptoService = new CryptoModuleService();
|
||||
HsmCryptoService hsmCryptoService = new HsmCryptoService();
|
||||
|
||||
// 6. Spring ApplicationContext 구성
|
||||
springCtx = new GenericApplicationContext();
|
||||
springCtx.getBeanFactory().registerSingleton("propManager", mockPropManager);
|
||||
springCtx.getBeanFactory().registerSingleton("hsmManager", hsmManager);
|
||||
springCtx.getBeanFactory().registerSingleton("hsmCryptoService", hsmCryptoService);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleService", cryptoService);
|
||||
springCtx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
|
||||
springCtx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
springCtx.refresh();
|
||||
|
||||
// 7. HsmManager 시작 (SunPKCS11 Provider 초기화 + KeyStore 오픈)
|
||||
hsmManager.start();
|
||||
assertTrue(hsmManager.isReady(), "HsmManager 초기화 실패");
|
||||
System.out.println("[HSM] Provider: " + hsmManager.getPkcs11Provider().getName());
|
||||
|
||||
// 8. MASTER_KEY 등록 (없으면 자동 생성)
|
||||
ensureMasterKey();
|
||||
|
||||
// 9. CryptoModuleManager 시작 (모듈 설정 로드)
|
||||
manager.start();
|
||||
System.out.println("[Crypto] 모듈 로드 완료");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownAll() throws Exception {
|
||||
if (hsmManager != null && hsmManager.isStarted()) hsmManager.stop();
|
||||
if (springCtx != null) springCtx.close();
|
||||
if (tempCfgFile != null) tempCfgFile.delete();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockReq = mock(HttpServletRequest.class);
|
||||
mockRes = mock(HttpServletResponse.class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. HsmKeyDerivationStrategy — HSM 마스터키 직접 사용 (InCryptoFilter)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. FIELD/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
|
||||
void testHsmGcm_field_roundTrip() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
// SCOPE 기본값 FIELD, 경로 기본값 사용
|
||||
|
||||
// 암호화: body 전체 → /encrypted_data 필드
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
System.out.println("[1-1] encrypted: " + encrypted);
|
||||
assertTrue(encrypted.contains("encrypted_data"), "/encrypted_data 필드가 있어야 한다");
|
||||
|
||||
// 복호화: /encrypted_data 필드 → body 전체 교체
|
||||
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[1-1] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. BODY/GCM HsmKey — doPostFilter 암호화 → doPreFilter 복호화 라운드트립")
|
||||
void testHsmGcm_body_roundTrip() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"acno\":\"1234567890\",\"amount\":\"100000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_SCOPE, "BODY");
|
||||
|
||||
// 암호화: body 전체 → Base64 암호문
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
System.out.println("[1-2] encrypted(Base64): " + encrypted);
|
||||
assertNotEquals(original, encrypted, "암호문은 평문과 달라야 한다");
|
||||
|
||||
// 복호화: Base64 암호문 → 원문
|
||||
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[1-2] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. FIELD/GCM HsmKey + AAD 헤더 — 동일 요청 ID로 암복호화 성공")
|
||||
void testHsmGcm_field_withAad_success() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"amount\":\"500000\"}";
|
||||
String requestId = "REQ-20240101-001";
|
||||
|
||||
when(mockReq.getHeader("X-Request-Id")).thenReturn(requestId);
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id");
|
||||
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
String decrypted = (String) filter.doPreFilter( "G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[1-3] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. FIELD/GCM HsmKey + AAD 헤더 — 다른 요청 ID로 복호화 시 GCM 인증 실패")
|
||||
void testHsmGcm_field_withAad_wrongAad_fails() throws Exception {
|
||||
InCryptoFilter filter = new InCryptoFilter();
|
||||
String original = "{\"amount\":\"500000\"}";
|
||||
|
||||
// 암호화 시 요청 ID
|
||||
HttpServletRequest encReq = mock(HttpServletRequest.class);
|
||||
when(encReq.getHeader("X-Request-Id")).thenReturn("REQ-CORRECT");
|
||||
|
||||
// 복호화 시 다른 요청 ID
|
||||
HttpServletRequest decReq = mock(HttpServletRequest.class);
|
||||
when(decReq.getHeader("X-Request-Id")).thenReturn("REQ-WRONG");
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_HSM_GCM);
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_AAD_HEADER, "X-Request-Id");
|
||||
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, encReq, mockRes);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> filter.doPreFilter("G", "A", encrypted, prop, decReq, mockRes),
|
||||
"잘못된 AAD로 GCM 복호화 시 예외가 발생해야 한다");
|
||||
System.out.println("[1-4] 다른 AAD 복호화 예외 확인 완료");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. HsmContextSha256KeyDerivationStrategy — 컨텍스트 기반 SHA-256 파생 키
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. FIELD/GCM ContextSha256 — 동일 컨텍스트로 암복호화 라운드트립")
|
||||
void testCtxSha256Gcm_field_roundTrip() throws Exception {
|
||||
InCryptoFilter filter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
|
||||
String original = "{\"accNo\":\"0011234567890\",\"amount\":\"50000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
|
||||
|
||||
String encrypted = (String) filter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
System.out.println("[2-1] encrypted: " + encrypted);
|
||||
assertTrue(encrypted.contains("encrypted_data"));
|
||||
|
||||
String decrypted = (String) filter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes);
|
||||
System.out.println("[2-1] decrypted: " + decrypted);
|
||||
assertEquals(original, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. FIELD/GCM ContextSha256 — 암호화와 다른 컨텍스트로 복호화 시 GCM 인증 실패")
|
||||
void testCtxSha256Gcm_differentContext_throwsException() throws Exception {
|
||||
InCryptoFilter encFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
|
||||
InCryptoFilter decFilter = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
|
||||
String original = "{\"data\":\"sensitive\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
|
||||
|
||||
String encrypted = (String) encFilter.doPostFilter("G", "A", original, prop, mockReq, mockRes);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> decFilter.doPreFilter("G", "A", encrypted, prop, mockReq, mockRes),
|
||||
"다른 컨텍스트 키로 파생된 키는 복호화에 실패해야 한다");
|
||||
System.out.println("[2-2] 다른 컨텍스트 복호화 예외 확인 완료");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. FIELD/GCM ContextSha256 — 컨텍스트별 독립 암복호화 (GROUP-A, GROUP-B 각각 성공)")
|
||||
void testCtxSha256Gcm_perGroupRoundTrip() throws Exception {
|
||||
InCryptoFilter filterA = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_A);
|
||||
InCryptoFilter filterB = new ContextAwareCryptoFilter(CTX_KEY, CTX_VAL_B);
|
||||
String plainA = "{\"group\":\"A\",\"amount\":\"1000\"}";
|
||||
String plainB = "{\"group\":\"B\",\"amount\":\"2000\"}";
|
||||
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(AbstractCryptoFilter.PROP_MODULE_NAME, MOD_CTX_SHA_GCM);
|
||||
|
||||
String encA = (String) filterA.doPostFilter("G", "A", plainA, prop, mockReq, mockRes);
|
||||
String encB = (String) filterB.doPostFilter("G", "B", plainB, prop, mockReq, mockRes);
|
||||
|
||||
assertEquals(plainA, (String) filterA.doPreFilter("G", "A", encA, prop, mockReq, mockRes));
|
||||
assertEquals(plainB, (String) filterB.doPreFilter("G", "B", encB, prop, mockReq, mockRes));
|
||||
System.out.println("[2-3] GROUP-A, GROUP-B 각각 독립 암복호화 성공");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private static List<CryptoModuleConfig> buildModuleConfigs() {
|
||||
return Arrays.asList(
|
||||
buildDynamic(MOD_HSM_GCM, "AES", "GCM", "NoPadding",
|
||||
HsmKeyDerivationStrategy.class.getName(),
|
||||
"{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\"}"),
|
||||
buildDynamic(MOD_CTX_SHA_GCM, "AES", "GCM", "NoPadding",
|
||||
HsmContextSha256KeyDerivationStrategy.class.getName(),
|
||||
"{\"hsmKeyAlias\":\"" + MASTER_KEY_ALIAS + "\",\"contextKey\":\"" + CTX_KEY + "\"}")
|
||||
);
|
||||
}
|
||||
|
||||
private static CryptoModuleConfig buildDynamic(String name, String alg, String mode, String padding,
|
||||
String strategy, String params) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setKeySourceType("DYNAMIC");
|
||||
c.setKeyDerivStrategy(strategy);
|
||||
c.setKeyDerivParams(params);
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(60);
|
||||
c.setUseYn("Y");
|
||||
// ivHex = null: GCM에서 IV를 AAD 대체값으로 사용하지 않도록 의도적으로 미설정
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void ensureMasterKey() throws Exception {
|
||||
java.security.KeyStore ks = hsmManager.getKeyStore();
|
||||
|
||||
// 이전 실행 키 삭제 (sensitive 여부 무관하게 항상 고정키로 재등록)
|
||||
if (ks.containsAlias(MASTER_KEY_ALIAS)) {
|
||||
ks.deleteEntry(MASTER_KEY_ALIAS);
|
||||
System.out.println("[HSM] 기존 MASTER_KEY 삭제");
|
||||
}
|
||||
|
||||
// CryptoModuleServiceTest.KEY_128 과 동일한 고정 키 → 두 테스트 간 암호문 비교 가능
|
||||
// attributes(*, CKO_SECRET_KEY, CKK_AES) 지시어로 임포트 시 CKA_SENSITIVE=false 적용
|
||||
byte[] keyBytes = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
SecretKey fixedKey = new SecretKeySpec(keyBytes, "AES");
|
||||
ks.setKeyEntry(MASTER_KEY_ALIAS, fixedKey, null, null);
|
||||
|
||||
// 임포트 후 getEncoded() 검증
|
||||
SecretKey stored = (SecretKey) ks.getKey(MASTER_KEY_ALIAS, null);
|
||||
byte[] encoded = stored.getEncoded();
|
||||
System.out.println("[HSM] MASTER_KEY 등록 완료: alias=" + MASTER_KEY_ALIAS
|
||||
+ " / getEncoded()=" + (encoded != null ? encoded.length + "bytes, " + new String(encoded) : "null (추출 불가!)"));
|
||||
}
|
||||
|
||||
private static void ensureTokenInitialized() throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
SOFTHSM2_UTIL, "--init-token", "--free",
|
||||
"--label", TOKEN_LABEL, "--pin", PIN, "--so-pin", PIN);
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
String out = readOutput(p);
|
||||
p.waitFor(10, TimeUnit.SECONDS);
|
||||
System.out.println("[SoftHSM2] init-token: " + out.trim());
|
||||
}
|
||||
|
||||
private static CryptoModuleConfigMapper testMapper() {
|
||||
return new CryptoModuleConfigMapper() {
|
||||
@Override
|
||||
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
|
||||
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
|
||||
vo.setCryptoId(e.getCryptoId());
|
||||
vo.setCryptoName(e.getCryptoName());
|
||||
vo.setCryptoDesc(e.getCryptoDesc());
|
||||
vo.setAlgType(e.getAlgType());
|
||||
vo.setCipherMode(e.getCipherMode());
|
||||
vo.setPadding(e.getPadding());
|
||||
vo.setIvHex(e.getIvHex());
|
||||
vo.setKeySourceType(e.getKeySourceType());
|
||||
vo.setEncKeyHex(e.getEncKeyHex());
|
||||
vo.setDecKeyHex(e.getDecKeyHex());
|
||||
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
|
||||
vo.setKeyDerivParams(e.getKeyDerivParams());
|
||||
vo.setCacheYn(e.getCacheYn());
|
||||
vo.setCacheTtlSec(e.getCacheTtlSec());
|
||||
vo.setUseYn(e.getUseYn());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String readOutput(Process p) throws Exception {
|
||||
InputStream is = p.getInputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len;
|
||||
while ((len = is.read(buf)) != -1) {
|
||||
sb.append(new String(buf, 0, len, "UTF-8"));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class HsmAddKeyTest {
|
||||
|
||||
/**
|
||||
* JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
|
||||
*
|
||||
* JDK 8 : SunPKCS11(InputStream) 생성자를 리플렉션으로 호출 JDK 9+:
|
||||
* Provider.configure(configFilePath) 를 리플렉션으로 호출
|
||||
*
|
||||
* 두 경로 모두 리플렉션을 사용하므로 컴파일 타임에 JDK 버전 의존성이 없다.
|
||||
*/
|
||||
static Provider createProvider(String cfgContent) throws Exception {
|
||||
String javaVersion = System.getProperty("java.version");
|
||||
if (javaVersion.startsWith("1.")) {
|
||||
// JDK 8: new SunPKCS11(InputStream)
|
||||
InputStream is = new ByteArrayInputStream(cfgContent.getBytes("UTF-8"));
|
||||
return (Provider) Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class)
|
||||
.newInstance(is);
|
||||
} else {
|
||||
// JDK 9+: Security.getProvider("SunPKCS11").configure(configFilePath)
|
||||
// configure() 는 JDK 9 에서 추가된 메서드이므로 리플렉션으로 호출
|
||||
File tmp = File.createTempFile("pkcs11-hsm-", ".cfg");
|
||||
tmp.deleteOnExit();
|
||||
Files.write(tmp.toPath(), cfgContent.getBytes("UTF-8"));
|
||||
Provider base = Security.getProvider("SunPKCS11");
|
||||
Method configure = Provider.class.getMethod("configure", String.class);
|
||||
return (Provider) configure.invoke(base, tmp.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// String config = "name = ProtectServer\r\n"
|
||||
// + "library = /usr/safenet/protecttoolkit5/ptk/lib/libcryptoki.so\r\n"
|
||||
// + "slot = 0";
|
||||
|
||||
// String config = "name = SoftHSM\r\nlibrary = C:/SoftHSM2/lib/softhsm2-x64.dll\r\nslotListIndex = 0\r\n";
|
||||
|
||||
String config = "name = SoftHSM\r\n"
|
||||
+ "library = C:/SoftHSM2/lib/softhsm2-x64.dll\r\n"
|
||||
+ "slotListIndex = 0\r\n"
|
||||
+ "attributes(*, CKO_SECRET_KEY, CKK_AES) = {\r\n"
|
||||
+ " CKA_TOKEN = true\r\n"
|
||||
+ " CKA_SENSITIVE = false\r\n"
|
||||
+ " CKA_EXTRACTABLE = true\r\n"
|
||||
+ " CKA_ENCRYPT = true\r\n"
|
||||
+ " CKA_DECRYPT = true\r\n"
|
||||
+ "}\r\n";
|
||||
|
||||
KeyStore keyStore;
|
||||
// Provider pkcs11Provider = HsmManagerSingleTest.createProvider(config);
|
||||
InputStream is = new ByteArrayInputStream(config.getBytes("UTF-8"));
|
||||
Provider pkcs11Provider = (Provider) Class.forName("sun.security.pkcs11.SunPKCS11")
|
||||
.getConstructor(InputStream.class).newInstance(is);
|
||||
Security.addProvider(pkcs11Provider);
|
||||
String pin = "1234";
|
||||
keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
|
||||
|
||||
char[] pinChars = (pin != null) ? pin.toCharArray() : null;
|
||||
keyStore.load(null, pinChars);
|
||||
|
||||
// addKey("20250916_openapi_erpbank_t", "55187afc5d36a2e3b71f3bf992947389964ac5ea0bc8ebe2297265134fa34cb9",
|
||||
// keyStore, pkcs11Provider);
|
||||
// addKey("20240401_openapi_kakaobank_t", "f009328cb94f5c866ff730735ce8031b43ca84f9266ced641b4046c6a5c21038",
|
||||
// keyStore, pkcs11Provider);
|
||||
// addKey("20250620_openapi_coocon_t", "3e7a7673392c424e58265c7b6f2e596f7a59542b277472704c3d5d6c70226673",
|
||||
// keyStore, pkcs11Provider);
|
||||
// addKey("20211221_openapi_kakaopay_t", "23667b665037785c3c312a414b6c566d2f3832476b254646727c4d703d484534",
|
||||
// keyStore, pkcs11Provider);
|
||||
//
|
||||
// addKey("20250916_openapi_erpbank_t_0", "55187afc5d36a2e3b71f3bf992947389964ac5ea0bc8ebe2297265134fa34cb9",
|
||||
// keyStore, pkcs11Provider);
|
||||
// addKey("20240401_openapi_kakaobank_t_0", "f009328cb94f5c866ff730735ce8031b43ca84f9266ced641b4046c6a5c21038",
|
||||
// keyStore, pkcs11Provider);
|
||||
// addKey("20250620_openapi_coocon_t_0", "3e7a7673392c424e58265c7b6f2e596f7a59542b277472704c3d5d6c70226673",
|
||||
// keyStore, pkcs11Provider);
|
||||
// addKey("20211221_openapi_kakaopay_t_0", "23667b665037785c3c312a414b6c566d2f3832476b254646727c4d703d484534",
|
||||
// keyStore, pkcs11Provider);
|
||||
|
||||
System.out.println("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
|
||||
|
||||
java.util.Enumeration<String> aliases = keyStore.aliases();
|
||||
StringBuilder aliasList = new StringBuilder();
|
||||
while (aliases.hasMoreElements()) {
|
||||
if (aliasList.length() > 0)
|
||||
aliasList.append(", ");
|
||||
|
||||
String alias = aliases.nextElement();
|
||||
aliasList.append(alias);
|
||||
|
||||
java.security.Key key = keyStore.getKey(alias, null);
|
||||
if (key instanceof SecretKey) {
|
||||
SecretKey secretKey = (SecretKey) key;
|
||||
if (secretKey.getEncoded() != null) {
|
||||
String encBase64 = Base64.getEncoder().encodeToString(secretKey.getEncoded());
|
||||
System.out.println("HsmManager] HSM - alias:" + alias + ", base64:" + encBase64 + ", secretKey:" + secretKey);
|
||||
} else {
|
||||
System.out
|
||||
.println("HsmManager] HSM - secretKey null - alias:" + alias + ", secretKey:" + secretKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
||||
}
|
||||
|
||||
private static void addKey(String keyAlias, String keyValue, KeyStore keyStore, Provider pkcs11Provider)
|
||||
throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
byte[] keyBytes = hexToBytes(keyValue);
|
||||
|
||||
SecretKeySpec spec = new SecretKeySpec(keyBytes, "AES");
|
||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("AES", pkcs11Provider);
|
||||
SecretKey fixedKey = (SecretKey) factory.generateSecret(spec);
|
||||
|
||||
// SecretKey fixedKey = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
keyStore.setKeyEntry(keyAlias, fixedKey, null, null);
|
||||
|
||||
// // 2. wrapping key 생성 (최초 1회만 — 이후엔 재사용)
|
||||
// KeyGenerator wrapKeyGen = KeyGenerator.getInstance("AES", pkcs11Provider);
|
||||
// wrapKeyGen.init(256);
|
||||
// SecretKey wrappingKey = wrapKeyGen.generateKey();
|
||||
//
|
||||
// // 3. 테스트 환경 key 값 지정
|
||||
// byte[] testKeyBytes = hexToBytes(keyValue);
|
||||
// SecretKey targetKeySpec = new SecretKeySpec(testKeyBytes, "AES");
|
||||
//
|
||||
// // 4. wrap
|
||||
// Cipher wrapCipher = Cipher.getInstance("AESWRAP", pkcs11Provider);
|
||||
// wrapCipher.init(Cipher.WRAP_MODE, wrappingKey);
|
||||
// byte[] wrapped = wrapCipher.wrap(targetKeySpec);
|
||||
//
|
||||
// // 5. unwrap → SoftHSM2 내부에 실제 등록
|
||||
// Cipher unwrapCipher = Cipher.getInstance("AESWRAP", pkcs11Provider);
|
||||
// unwrapCipher.init(Cipher.UNWRAP_MODE, wrappingKey);
|
||||
// SecretKey hsmKey = (SecretKey) unwrapCipher.unwrap(wrapped, "AES", Cipher.SECRET_KEY
|
||||
//
|
||||
// );
|
||||
//
|
||||
// // 6. 평문 byte 배열 즉시 제거
|
||||
// Arrays.fill(testKeyBytes, (byte) 0);
|
||||
//
|
||||
// System.out.println("Key 등록 완료: " + hsmKey.getAlgorithm() + " / " + hsmKey.getFormat());
|
||||
//
|
||||
// // 7. 등록 확인 — alias 지정해서 KeyStore에 올리고 싶으면:
|
||||
// keyStore.setKeyEntry(keyAlias, hsmKey, null, null);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String hex) {
|
||||
int len = hex.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
+179
-179
@@ -1,179 +1,179 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HsmContextSha256KeyDerivationStrategy 단위 테스트
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class HsmContextSha256KeyDerivationStrategyTest {
|
||||
|
||||
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
|
||||
private static final byte[] MASTER_KEY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static HsmCryptoService mockHsmCryptoService;
|
||||
private static HsmContextSha256KeyDerivationStrategy strategy;
|
||||
|
||||
private Map<String, String> params;
|
||||
private Map<String, String> runtimeContext;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockHsmCryptoService = mock(HsmCryptoService.class);
|
||||
SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
strategy = new HsmContextSha256KeyDerivationStrategy();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpParams() {
|
||||
params = new HashMap<>();
|
||||
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
|
||||
params.put("contextKey", "X-Api-Group-Seq");
|
||||
|
||||
runtimeContext = new HashMap<>();
|
||||
runtimeContext.put("X-Api-Group-Seq", "GROUP_001");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. deriveKey
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, 길이 32바이트(SHA-256 고정 출력)")
|
||||
void testDeriveKey_validParams_returns32ByteKey() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertNotNull(dk);
|
||||
assertNotNull(dk.getEncKey());
|
||||
assertNotNull(dk.getDecKey());
|
||||
assertEquals(32, dk.getEncKey().length, "SHA-256 출력은 항상 32바이트여야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. SHA-256(masterKey || contextValue) 계산 결과 검증")
|
||||
void testDeriveKey_sha256ResultIsCorrect() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
byte[] contextBytes = "GROUP_001".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] combined = new byte[MASTER_KEY.length + contextBytes.length];
|
||||
System.arraycopy(MASTER_KEY, 0, combined, 0, MASTER_KEY.length);
|
||||
System.arraycopy(contextBytes, 0, combined, MASTER_KEY.length, contextBytes.length);
|
||||
byte[] expected = MessageDigest.getInstance("SHA-256").digest(combined);
|
||||
|
||||
assertArrayEquals(expected, dk.getEncKey(), "SHA-256 해싱 결과가 일치해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
|
||||
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리")
|
||||
void testDeriveKey_missingContextValue_emptyStringUsed() throws Exception {
|
||||
runtimeContext.remove("X-Api-Group-Seq");
|
||||
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
byte[] expected = MessageDigest.getInstance("SHA-256").digest(MASTER_KEY);
|
||||
|
||||
assertArrayEquals(expected, dk.getEncKey(), "컨텍스트 값 없으면 masterKey만 해싱해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
|
||||
void testDeriveKey_differentContextValue_differentKey() throws Exception {
|
||||
DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
runtimeContext.put("X-Api-Group-Seq", "GROUP_002");
|
||||
DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);
|
||||
|
||||
assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
|
||||
"컨텍스트 값이 다르면 도출된 키도 달라야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
|
||||
void testDeriveKey_missingHsmKeyAlias_throwsException() {
|
||||
params.remove("hsmKeyAlias");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> strategy.deriveKey(params, runtimeContext),
|
||||
"hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
|
||||
void testDeriveKey_missingContextKey_throwsException() {
|
||||
params.remove("contextKey");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> strategy.deriveKey(params, runtimeContext),
|
||||
"contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. buildCacheKey
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
|
||||
void testBuildCacheKey_format() {
|
||||
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
|
||||
assertEquals("CRYPTO_A:GROUP_001", cacheKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
|
||||
void testBuildCacheKey_missingContextValue_emptyString() {
|
||||
runtimeContext.remove("X-Api-Group-Seq");
|
||||
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
|
||||
assertEquals("CRYPTO_A:", cacheKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
|
||||
void testBuildCacheKey_differentCryptoName_differentKey() {
|
||||
String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
|
||||
|
||||
assertNotEquals(key1, key2);
|
||||
}
|
||||
}
|
||||
//package com.eactive.eai.common.security.keyderiv;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.*;
|
||||
//import static org.mockito.Mockito.*;
|
||||
//
|
||||
//import java.nio.charset.StandardCharsets;
|
||||
//import java.security.MessageDigest;
|
||||
//import java.util.HashMap;
|
||||
//import java.util.Map;
|
||||
//
|
||||
//import javax.crypto.SecretKey;
|
||||
//import javax.crypto.spec.SecretKeySpec;
|
||||
//
|
||||
//import org.junit.jupiter.api.BeforeAll;
|
||||
//import org.junit.jupiter.api.BeforeEach;
|
||||
//import org.junit.jupiter.api.DisplayName;
|
||||
//import org.junit.jupiter.api.MethodOrderer;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.junit.jupiter.api.TestMethodOrder;
|
||||
//import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
//import org.springframework.context.support.GenericApplicationContext;
|
||||
//
|
||||
//import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
//import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
//import com.eactive.eai.common.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
|
||||
//import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
//
|
||||
///**
|
||||
// * HsmContextSha256KeyDerivationStrategy 단위 테스트
|
||||
// */
|
||||
//@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
//class HsmContextSha256KeyDerivationStrategyTest {
|
||||
//
|
||||
// private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
|
||||
// private static final byte[] MASTER_KEY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
//
|
||||
// private static GenericApplicationContext ctx;
|
||||
// private static HsmCryptoService mockHsmCryptoService;
|
||||
// private static HsmContextSha256KeyDerivationStrategy strategy;
|
||||
//
|
||||
// private Map<String, String> params;
|
||||
// private Map<String, String> runtimeContext;
|
||||
//
|
||||
// @BeforeAll
|
||||
// static void setUpClass() throws Exception {
|
||||
// mockHsmCryptoService = mock(HsmCryptoService.class);
|
||||
// SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
|
||||
// when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);
|
||||
//
|
||||
// ctx = new GenericApplicationContext();
|
||||
// ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
|
||||
// ctx.registerBeanDefinition("applicationContextProvider",
|
||||
// BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
// .getBeanDefinition());
|
||||
// ctx.refresh();
|
||||
//
|
||||
// strategy = new HsmContextSha256KeyDerivationStrategy();
|
||||
// }
|
||||
//
|
||||
// @BeforeEach
|
||||
// void setUpParams() {
|
||||
// params = new HashMap<>();
|
||||
// params.put("hsmKeyAlias", HSM_KEY_ALIAS);
|
||||
// params.put("contextKey", "X-Api-Group-Seq");
|
||||
//
|
||||
// runtimeContext = new HashMap<>();
|
||||
// runtimeContext.put("X-Api-Group-Seq", "GROUP_001");
|
||||
// }
|
||||
//
|
||||
// // =========================================================================
|
||||
// // 1. deriveKey
|
||||
// // =========================================================================
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, 길이 32바이트(SHA-256 고정 출력)")
|
||||
// void testDeriveKey_validParams_returns32ByteKey() throws Exception {
|
||||
// DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
//
|
||||
// assertNotNull(dk);
|
||||
// assertNotNull(dk.getEncKey());
|
||||
// assertNotNull(dk.getDecKey());
|
||||
// assertEquals(32, dk.getEncKey().length, "SHA-256 출력은 항상 32바이트여야 한다");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-2. SHA-256(masterKey || contextValue) 계산 결과 검증")
|
||||
// void testDeriveKey_sha256ResultIsCorrect() throws Exception {
|
||||
// DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
//
|
||||
// byte[] contextBytes = "GROUP_001".getBytes(StandardCharsets.UTF_8);
|
||||
// byte[] combined = new byte[MASTER_KEY.length + contextBytes.length];
|
||||
// System.arraycopy(MASTER_KEY, 0, combined, 0, MASTER_KEY.length);
|
||||
// System.arraycopy(contextBytes, 0, combined, MASTER_KEY.length, contextBytes.length);
|
||||
// byte[] expected = MessageDigest.getInstance("SHA-256").digest(combined);
|
||||
//
|
||||
// assertArrayEquals(expected, dk.getEncKey(), "SHA-256 해싱 결과가 일치해야 한다");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
|
||||
// void testDeriveKey_encKeyEqualsDecKey() throws Exception {
|
||||
// DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
//
|
||||
// assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리")
|
||||
// void testDeriveKey_missingContextValue_emptyStringUsed() throws Exception {
|
||||
// runtimeContext.remove("X-Api-Group-Seq");
|
||||
// DerivedKey dk = strategy.deriveKey(params, runtimeContext);
|
||||
//
|
||||
// byte[] expected = MessageDigest.getInstance("SHA-256").digest(MASTER_KEY);
|
||||
//
|
||||
// assertArrayEquals(expected, dk.getEncKey(), "컨텍스트 값 없으면 masterKey만 해싱해야 한다");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-5. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
|
||||
// void testDeriveKey_differentContextValue_differentKey() throws Exception {
|
||||
// DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);
|
||||
//
|
||||
// runtimeContext.put("X-Api-Group-Seq", "GROUP_002");
|
||||
// DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);
|
||||
//
|
||||
// assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
|
||||
// "컨텍스트 값이 다르면 도출된 키도 달라야 한다");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
|
||||
// void testDeriveKey_missingHsmKeyAlias_throwsException() {
|
||||
// params.remove("hsmKeyAlias");
|
||||
//
|
||||
// assertThrows(IllegalArgumentException.class,
|
||||
// () -> strategy.deriveKey(params, runtimeContext),
|
||||
// "hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
|
||||
// void testDeriveKey_missingContextKey_throwsException() {
|
||||
// params.remove("contextKey");
|
||||
//
|
||||
// assertThrows(IllegalArgumentException.class,
|
||||
// () -> strategy.deriveKey(params, runtimeContext),
|
||||
// "contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
|
||||
// }
|
||||
//
|
||||
// // =========================================================================
|
||||
// // 2. buildCacheKey
|
||||
// // =========================================================================
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
|
||||
// void testBuildCacheKey_format() {
|
||||
// String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
//
|
||||
// assertEquals("CRYPTO_A:GROUP_001", cacheKey);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
|
||||
// void testBuildCacheKey_missingContextValue_emptyString() {
|
||||
// runtimeContext.remove("X-Api-Group-Seq");
|
||||
// String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
//
|
||||
// assertEquals("CRYPTO_A:", cacheKey);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// @DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
|
||||
// void testBuildCacheKey_differentCryptoName_differentKey() {
|
||||
// String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
|
||||
// String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
|
||||
//
|
||||
// assertNotEquals(key1, key2);
|
||||
// }
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user