HSM, 암호화 관련 테스트 수행 중 수정

This commit is contained in:
curry772
2026-06-29 13:16:39 +09:00
parent ab9b86e7cf
commit a923ff3f9f
11 changed files with 548 additions and 1262 deletions
@@ -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());
@@ -128,6 +138,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);
@@ -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());