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.Provider; import java.security.Security; import org.springframework.stereotype.Component; import com.eactive.eai.common.exception.ExceptionUtil; import com.eactive.eai.common.lifecycle.Lifecycle; import com.eactive.eai.common.lifecycle.LifecycleException; import com.eactive.eai.common.lifecycle.LifecycleListener; import com.eactive.eai.common.lifecycle.LifecycleSupport; import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.Logger; /** * SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11) * * PropManager 그룹 "HSM" 에서 읽는 키: * PKCS11_CONFIG - pkcs11.cfg 파일 내용 (name/library/slot 설정을 DB에 직접 저장) * PIN - HSM 슬롯 PIN * * PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력): * name = ProtectServer * library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so * slot = 0 */ @Component public class HsmManager implements Lifecycle { static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); private static final String GROUP_NAME = "HSM"; private static final String PROP_CONFIG = "PKCS11_CONFIG"; private static final String PROP_PIN = "PIN"; private Provider pkcs11Provider; private KeyStore keyStore; private boolean started; private final LifecycleSupport lifecycle = new LifecycleSupport(this); private HsmManager() { } public static HsmManager getInstance() { return ApplicationContextProvider.getContext().getBean(HsmManager.class); } @Override public void start() throws LifecycleException { if (started) { throw new LifecycleException("RECEAIHSM001"); } lifecycle.fireLifecycleEvent(STARTING_EVENT, this); try { init(); } catch (Exception e) { throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002")); } started = true; lifecycle.fireLifecycleEvent(STARTED_EVENT, this); } private void init() throws Exception { String configContent = PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG); String pin = PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN); if (configContent == null || configContent.trim().isEmpty()) { logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다."); return; } pkcs11Provider = createProvider(configContent.trim().replace("\\n", "\n")); // 이미 등록된 Provider 가 있으면 제거 후 재등록 Provider existing = Security.getProvider(pkcs11Provider.getName()); if (existing != null) { Security.removeProvider(existing.getName()); } Security.addProvider(pkcs11Provider); keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider); char[] pinChars = (pin != null) ? pin.toCharArray() : null; 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 + "]"); } /** * 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()); } } @Override public void stop() throws LifecycleException { if (!started) { throw new LifecycleException("RECEAIHSM003"); } lifecycle.fireLifecycleEvent(STOPING_EVENT, this); if (pkcs11Provider != null) { Security.removeProvider(pkcs11Provider.getName()); } pkcs11Provider = null; keyStore = null; started = false; lifecycle.fireLifecycleEvent(STOPPED_EVENT, this); } @Override public void addLifecycleListener(LifecycleListener listener) { lifecycle.addLifecycleListener(listener); } @Override public LifecycleListener[] findLifecycleListeners() { return lifecycle.findLifecycleListeners(); } @Override public void removeLifecycleListener(LifecycleListener listener) { lifecycle.removeLifecycleListener(listener); } @Override public boolean isStarted() { return started; } public Provider getPkcs11Provider() { return pkcs11Provider; } public KeyStore getKeyStore() { return keyStore; } public boolean isReady() { return started && pkcs11Provider != null && keyStore != null; } }