From a923ff3f9f162a8144642994c15165b195d60bff Mon Sep 17 00:00:00 2001 From: curry772 Date: Mon, 29 Jun 2026 13:16:39 +0900 Subject: [PATCH] =?UTF-8?q?HSM,=20=EC=95=94=ED=98=B8=ED=99=94=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=ED=96=89=20?= =?UTF-8?q?=EC=A4=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/HttpClient5AdapterServiceRest.java | 568 ++---------------- .../http/dynamic/filter/InCryptoFilter.java | 10 +- .../agent/encryption/EncryptionManager.java | 2 +- .../eai/common/hsm/HsmCryptoService.java | 43 +- .../eactive/eai/common/hsm/HsmManager.java | 89 ++- .../common/security/CryptoModuleManager.java | 2 +- ...HsmContextSha256KeyDerivationStrategy.java | 62 -- .../com/eactive/eai/util/HexaConverter.java | 27 + .../CryptoFilterHsmIntegrationTest.java | 466 -------------- .../eactive/eai/common/hsm/HsmAddKeyTest.java | 183 ++++++ ...ontextSha256KeyDerivationStrategyTest.java | 358 +++++------ 11 files changed, 548 insertions(+), 1262 deletions(-) delete mode 100644 src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmContextSha256KeyDerivationStrategy.java delete mode 100644 src/test/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java create mode 100644 src/test/java/com/eactive/eai/common/hsm/HsmAddKeyTest.java diff --git a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java index 8510867..7588d87 100644 --- a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java +++ b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java @@ -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 통신에 사용
@@ -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 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 parseInboundHeader(String str) { - java.util.HashMap 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 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 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 getParameters(Object message) throws Exception { @@ -1573,7 +1073,13 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp HashMap result = new HashMap(); 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 inboundPathVariables) { if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) { try { diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/InCryptoFilter.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/InCryptoFilter.java index 7425447..a9c9847 100644 --- a/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/InCryptoFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/filter/InCryptoFilter.java @@ -31,7 +31,11 @@ public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterF * 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다. */ protected Map buildRuntimeContext(Properties prop, HttpServletRequest request) { - return new HashMap<>(); + Map 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); } diff --git a/src/main/java/com/eactive/eai/agent/encryption/EncryptionManager.java b/src/main/java/com/eactive/eai/agent/encryption/EncryptionManager.java index d4fc147..5dde0b7 100644 --- a/src/main/java/com/eactive/eai/agent/encryption/EncryptionManager.java +++ b/src/main/java/com/eactive/eai/agent/encryption/EncryptionManager.java @@ -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; } diff --git a/src/main/java/com/eactive/eai/common/hsm/HsmCryptoService.java b/src/main/java/com/eactive/eai/common/hsm/HsmCryptoService.java index 3c1b9a1..5dab96d 100644 --- a/src/main/java/com/eactive/eai/common/hsm/HsmCryptoService.java +++ b/src/main/java/com/eactive/eai/common/hsm/HsmCryptoService.java @@ -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 { + 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 secretKeyCache = new ConcurrentHashMap(); - private final ConcurrentHashMap publicKeyCache = new ConcurrentHashMap(); + private final ConcurrentHashMap> secretKeyCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> 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 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 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) { diff --git a/src/main/java/com/eactive/eai/common/hsm/HsmManager.java b/src/main/java/com/eactive/eai/common/hsm/HsmManager.java index 1340a96..9749acf 100644 --- a/src/main/java/com/eactive/eai/common/hsm/HsmManager.java +++ b/src/main/java/com/eactive/eai/common/hsm/HsmManager.java @@ -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 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()); } diff --git a/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java b/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java index bb2286b..0144d51 100644 --- a/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java +++ b/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java @@ -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); diff --git a/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmContextSha256KeyDerivationStrategy.java b/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmContextSha256KeyDerivationStrategy.java deleted file mode 100644 index 26bb696..0000000 --- a/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmContextSha256KeyDerivationStrategy.java +++ /dev/null @@ -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 params, Map 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 params, Map runtimeContext) { - String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, ""); - String contextValue = runtimeContext.getOrDefault(contextKey, ""); - return cryptoName + ":" + contextValue; - } - - private String required(Map 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); - } -} diff --git a/src/main/java/com/eactive/eai/util/HexaConverter.java b/src/main/java/com/eactive/eai/util/HexaConverter.java index ea8989b..df44db9 100644 --- a/src/main/java/com/eactive/eai/util/HexaConverter.java +++ b/src/main/java/com/eactive/eai/util/HexaConverter.java @@ -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()); diff --git a/src/test/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java b/src/test/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java deleted file mode 100644 index 7023c1b..0000000 --- a/src/test/java/com/eactive/eai/adapter/http/dynamic/filter/CryptoFilterHsmIntegrationTest.java +++ /dev/null @@ -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 buildRuntimeContext(Properties prop, HttpServletRequest request) { - Map 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 hsmCtor = HsmManager.class.getDeclaredConstructor(); - hsmCtor.setAccessible(true); - hsmManager = hsmCtor.newInstance(); - - // 5. CryptoModuleManager (private 생성자 → 리플렉션) - CryptoModuleConfigLoader mockLoader = mock(CryptoModuleConfigLoader.class); - when(mockLoader.findAll()).thenReturn(buildModuleConfigs()); - - Constructor 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 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(); - } -} diff --git a/src/test/java/com/eactive/eai/common/hsm/HsmAddKeyTest.java b/src/test/java/com/eactive/eai/common/hsm/HsmAddKeyTest.java new file mode 100644 index 0000000..8fcbb1a --- /dev/null +++ b/src/test/java/com/eactive/eai/common/hsm/HsmAddKeyTest.java @@ -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 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(); + } + +} diff --git a/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java b/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java index b16da5c..d64c6f3 100644 --- a/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java +++ b/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java @@ -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 params; - private Map 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 params; +// private Map 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); +// } +//}