From a1a5a34874b2d1579f82981eef90a21f1e6ace28 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 12:58:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20HSM=20=ED=82=A4=20=EC=BA=90=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=8F=20PropManager=20=EC=97=B0=EB=8F=99=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94,=20DB=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HsmCryptoService: getPublicKey/getSecretKey ConcurrentHashMap 캐시 추가 (HSM 통신 최소화) - HsmCryptoService: encryptAes/decryptAes Provider 파라미터 오버로드 추가 (non-extractable 키 지원) - HsmCryptoService: PropertyChangeListener 구현, HSM.CACHE_RELOAD_YN=Y 시 clearKeyCache() 자동 호출 - HsmCryptoService: @PostConstruct NPE 수정 - PropManager.getInstance() → @Autowired 직접 주입 - HsmManager: DB PKCS11_CONFIG 리터럴 \n → 실제 줄바꿈 치환 (파싱 오류 수정) - HsmManager: 기동 시 HSM 키 목록 로그 추가 Co-Authored-By: Claude Sonnet 4.6 --- .../eai/common/hsm/HsmCryptoService.java | 106 +++++++++++++++--- .../eactive/eai/common/hsm/HsmManager.java | 10 +- 2 files changed, 99 insertions(+), 17 deletions(-) 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 ff9b6d7..3c1b9a1 100644 --- a/src/main/java/com/eactive/eai/common/hsm/HsmCryptoService.java +++ b/src/main/java/com/eactive/eai/common/hsm/HsmCryptoService.java @@ -1,57 +1,99 @@ package com.eactive.eai.common.hsm; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.security.KeyStore; import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; import java.security.cert.Certificate; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.PostConstruct; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.Logger; /** * SafeNet ProtectServer HSM 암복호화 서비스 (JDK 8) * * [키 조회] - * getPublicKey - 공개키 반환 (외부 노출) - * getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요, 외부 노출) - * getPrivateKey - 내부 전용 (개인키는 HSM 외부로 반출하지 않는 것이 원칙) + * getPublicKey - 공개키 반환 (외부 노출). 최초 1회만 HSM 통신, 이후 캐시 반환. + * getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요). 최초 1회만 HSM 통신, 이후 캐시 반환. * * [암복호화] * encryptRsa - 공개키로 JVM 소프트웨어 암호화 (HSM 불필요) - * decryptRsa - alias 기반, HSM 내부 복호화 (HsmManager 미초기화 시 JVM fallback) + * decryptRsa - alias 기반, HSM 내부 복호화 (매 호출마다 HSM 통신 발생 - 불가피) * encryptAes - SecretKey 객체로 AES/CBC 암호화 * decryptAes - SecretKey 객체로 AES/CBC 복호화 */ @Service -public class HsmCryptoService { +public class HsmCryptoService implements PropertyChangeListener { static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding"; private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding"; + private static final String PROP_GROUP = "HSM"; + private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN"; + + // HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱 + private final ConcurrentHashMap secretKeyCache = new ConcurrentHashMap(); + private final ConcurrentHashMap publicKeyCache = new ConcurrentHashMap(); + + @Autowired + private PropManager propManager; + + @PostConstruct + public void registerPropertyChangeListener() { + propManager.addPropertyChangeListener(this); + logger.warn("HsmCryptoService] PropManager PropertyChangeListener 등록 완료"); + } + + /** + * 관리포털에서 PropManager.reload("HSM") 호출 시 이벤트를 수신한다. + * HSM.CACHE_RELOAD_YN = Y 이면 키 캐시를 초기화한다. + */ + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!PROP_GROUP.equals(evt.getPropertyName())) { + return; + } + String reloadYn = propManager.getProperty(PROP_GROUP, PROP_CACHE_RELOAD_YN, "N"); + if ("Y".equalsIgnoreCase(reloadYn)) { + clearKeyCache(); + } + } + // ------------------------------------------------------------------------- // 키 조회 (외부 노출) // ------------------------------------------------------------------------- /** - * HSM KeyStore 에서 공개키를 반환합니다. - * 공개키는 외부 노출에 제약이 없으며, 직접 암호화에 활용할 수 있습니다. + * HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다. */ public PublicKey getPublicKey(String keyAlias) throws HsmException { checkReady(); + PublicKey cached = publicKeyCache.get(keyAlias); + if (cached != null) { + return cached; + } try { Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias); if (cert == null) { throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias); } - return cert.getPublicKey(); + PublicKey key = cert.getPublicKey(); + publicKeyCache.put(keyAlias, key); + logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias); + return key; } catch (HsmException e) { throw e; } catch (Exception e) { @@ -60,18 +102,25 @@ public class HsmCryptoService { } /** - * HSM KeyStore 에서 AES 대칭키를 반환합니다. + * HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다. * HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다. */ public SecretKey getSecretKey(String keyAlias) throws HsmException { checkReady(); + SecretKey cached = secretKeyCache.get(keyAlias); + if (cached != null) { + return cached; + } 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); } - return (SecretKey) key; + SecretKey secretKey = (SecretKey) key; + secretKeyCache.put(keyAlias, secretKey); + logger.warn("HsmCryptoService] 대칭키 캐시 등록: alias=" + keyAlias); + return secretKey; } catch (HsmException e) { throw e; } catch (Exception e) { @@ -79,6 +128,13 @@ public class HsmCryptoService { } } + /** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */ + public void clearKeyCache() { + secretKeyCache.clear(); + publicKeyCache.clear(); + logger.warn("HsmCryptoService] 키 캐시 초기화 완료"); + } + // ------------------------------------------------------------------------- // RSA // ------------------------------------------------------------------------- @@ -146,15 +202,19 @@ public class HsmCryptoService { /** * AES/CBC/PKCS5Padding 암호화. - * HSM 없이도 호출 가능합니다 (getSecretKey 로 키를 미리 가져온 경우). + * extractable 키: provider=null 로 JVM 소프트웨어 암호화. + * non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다. * * @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키) * @param iv 초기화 벡터 (16바이트) * @param plaintext 평문 바이트 + * @param provider null 이면 JVM 기본 Provider 사용 */ - public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException { + public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext, Provider provider) throws HsmException { try { - Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + Cipher cipher = (provider != null) + ? Cipher.getInstance(AES_ALGORITHM, provider) + : Cipher.getInstance(AES_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); return cipher.doFinal(plaintext); } catch (Exception e) { @@ -162,17 +222,26 @@ public class HsmCryptoService { } } + /** AES 암호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */ + public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException { + return encryptAes(secretKey, iv, plaintext, null); + } + /** * AES/CBC/PKCS5Padding 복호화. - * HSM 없이도 호출 가능합니다 (getSecretKey 로 키를 미리 가져온 경우). + * extractable 키: provider=null 로 JVM 소프트웨어 복호화. + * non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다. * * @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키) * @param iv 초기화 벡터 (16바이트) * @param ciphertext 암호문 바이트 + * @param provider null 이면 JVM 기본 Provider 사용 */ - public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException { + public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext, Provider provider) throws HsmException { try { - Cipher cipher = Cipher.getInstance(AES_ALGORITHM); + Cipher cipher = (provider != null) + ? Cipher.getInstance(AES_ALGORITHM, provider) + : Cipher.getInstance(AES_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); return cipher.doFinal(ciphertext); } catch (Exception e) { @@ -180,6 +249,11 @@ public class HsmCryptoService { } } + /** AES 복호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */ + public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException { + return decryptAes(secretKey, iv, ciphertext, null); + } + // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- 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 374ef1a..ecad065 100644 --- a/src/main/java/com/eactive/eai/common/hsm/HsmManager.java +++ b/src/main/java/com/eactive/eai/common/hsm/HsmManager.java @@ -80,7 +80,7 @@ public class HsmManager implements Lifecycle { return; } - pkcs11Provider = createProvider(configContent.trim()); + pkcs11Provider = createProvider(configContent.trim().replace("\\n", "\n")); // 이미 등록된 Provider 가 있으면 제거 후 재등록 Provider existing = Security.getProvider(pkcs11Provider.getName()); @@ -94,6 +94,14 @@ public class HsmManager implements Lifecycle { keyStore.load(null, pinChars); logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName()); + + java.util.Enumeration aliases = keyStore.aliases(); + StringBuilder aliasList = new StringBuilder(); + while (aliases.hasMoreElements()) { + if (aliasList.length() > 0) aliasList.append(", "); + aliasList.append(aliases.nextElement()); + } + logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]"); } /**