package com.eactive.eai.common.hsm; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; 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.AuthProvider; 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.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import javax.crypto.SecretKey; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import com.eactive.eai.agent.encryption.EncryptionManager; 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.PropGroupVO; 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 - primary pkcs11.cfg 파일 내용 * PIN - primary 슬롯 PIN * PKCS11_CONFIG_SECONDARY - secondary pkcs11.cfg 파일 내용 (미설정 시 이중화 비활성) * PIN_SECONDARY - secondary 슬롯 PIN (미설정 시 PIN 재사용) * RELOAD_INTERVAL_MINUTES - KeyStore 주기적 재로드 간격(분) * * PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력): * name = ProtectServer * library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so * slot = 0 * * name = SoftHSM * library = C:/SoftHSM2/lib/softhsm2-x64.dll * slotListIndex = 0 * * --------------------------------------------------------------------- * [설정 이중화 / primary 자동 복귀] * 기동(init) 과 재연결 시 항상 primary -> secondary 순서로 연결을 시도하고, * 최초로 성공한 설정을 사용한다. secondary 로 절체된 뒤에는 주기적 재로드마다 * primary 재연결을 먼저 시도하므로 primary 가 복구되면 자동으로 되돌아온다. * * [세션 누적 방지 / 회로차단 로직] * 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로 호출하면 * PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의 최대 세션 수를 * 초과하면서 reload 가 영구적으로 실패한다. 이를 방지하기 위해: * 1) primary 로 동작 중이고 설정도 그대로면 "기존 keyStore 인스턴스"에 다시 * load() 하여 세션을 재사용한다 (= primary 우선 시도와 동일한 의미) * 2) 실제 키 조회(probe)로 세션이 살아있는지 검증 * 3) 재로드가 실패하거나 프로퍼티가 변경되면 primary -> secondary 순서로 * Provider 를 완전히 재생성 * 4) 재생성마저 실패하면 마지막으로 성공한 keyStore 를 유지 (서비스 연속성 우선) * * [프로퍼티 즉시 반영] * PropManager 의 PropertyChangeListener 로 등록되어 있어, 관리 포털에서 HSM 그룹을 * reload 하면 다음 스케줄을 기다리지 않고 즉시 설정을 다시 읽어 재연결을 시도한다. * 이 경우에도 신규 연결이 완전히 성공한 뒤에만 keyStore 멤버변수를 교체한다. * --------------------------------------------------------------------- */ @Component public class HsmManager implements Lifecycle, PropertyChangeListener { 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 static final String PROP_CONFIG_SECONDARY = "PKCS11_CONFIG_SECONDARY"; private static final String PROP_PIN_SECONDARY = "PIN_SECONDARY"; private static final String PROP_RELOAD_INTERVAL_MINUTES = "RELOAD_INTERVAL_MINUTES"; /** 설정 구분자. HSM 상태 조회 API 응답에도 그대로 노출된다. */ public static final String CONFIG_PRIMARY = "PRIMARY"; public static final String CONFIG_SECONDARY = "SECONDARY"; private static final long DEFAULT_RELOAD_INTERVAL_MINUTES = 10; private Provider pkcs11Provider; private volatile KeyStore keyStore; private boolean started; private final LifecycleSupport lifecycle = new LifecycleSupport(this); private volatile char[] pin; /** 현재 연결에 사용 중인 설정. 미연결이면 null. */ private volatile HsmConfig activeConfig; // ---- 모니터링용 상태값 (HsmStatusController 에서 조회) ---- private volatile long activeSince; private volatile long lastReloadAt; private volatile String lastReloadResult; private volatile String lastErrorMessage; private volatile long lastErrorAt; private volatile int reloadSuccessCount; private volatile int reloadFailCount; private volatile int failoverCount; private volatile long reloadIntervalMinutes = DEFAULT_RELOAD_INTERVAL_MINUTES; private ScheduledExecutorService scheduler; private ScheduledFuture reloadFuture; private volatile boolean propListenerRegistered; private HsmManager() { } public static HsmManager getInstance() { return ApplicationContextProvider.getContext().getBean(HsmManager.class); } // ------------------------------------------------------------------------- // 설정 후보 (primary / secondary) // ------------------------------------------------------------------------- /** * 한 번의 연결 시도에 필요한 설정 한 벌. PropManager 에서 읽어 복호화까지 마친 값이다. */ static final class HsmConfig { final String name; final String configContent; final char[] pin; HsmConfig(String name, String configContent, char[] pin) { this.name = name; this.configContent = configContent; this.pin = pin; } /** PropManager 값이 바뀌었는지 판단하기 위한 비교. */ boolean sameAs(HsmConfig other) { if (other == null) { return false; } return name.equals(other.name) && configContent.equals(other.configContent) && Arrays.equals(pin, other.pin); } } /** * 연결 시도 결과. Security 등록 전 상태이며, 완전히 성공한 경우에만 * applyConnection() 을 통해 멤버변수로 승격된다. */ private static final class HsmConnection { final HsmConfig config; final Provider provider; final KeyStore keyStore; HsmConnection(HsmConfig config, Provider provider, KeyStore keyStore) { this.config = config; this.provider = provider; this.keyStore = keyStore; } } /** * PropManager 에서 매번 새로 읽어 primary -> secondary 순서의 연결 후보를 만든다. * 스케줄러/프로퍼티 변경 이벤트 모두 이 메서드를 통하므로 변경된 값이 즉시 반영된다. * PKCS11_CONFIG 가 비어 있는 후보는 제외한다. */ private List loadConfigCandidates() { PropManager propManager = PropManager.getInstance(); EncryptionManager encManager = EncryptionManager.getInstance(); List candidates = new ArrayList<>(); String primaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG)); String primaryPin = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_PIN)); if (StringUtils.isNotBlank(primaryConfig)) { candidates.add(new HsmConfig(CONFIG_PRIMARY, normalizeConfig(primaryConfig), toPin(primaryPin))); } String secondaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG_SECONDARY)); if (StringUtils.isNotBlank(secondaryConfig)) { // PIN_SECONDARY 미설정이면 primary PIN 을 재사용한다 (슬롯만 이중화하는 구성 지원) String rawSecondaryPin = propManager.getProperty(GROUP_NAME, PROP_PIN_SECONDARY); String secondaryPin = StringUtils.isNotBlank(rawSecondaryPin) ? decrypt(encManager, rawSecondaryPin) : primaryPin; candidates.add(new HsmConfig(CONFIG_SECONDARY, normalizeConfig(secondaryConfig), toPin(secondaryPin))); } return candidates; } private static String decrypt(EncryptionManager encManager, String value) { if (value == null) { return null; } return encManager.decryptDBData(value); } private static String normalizeConfig(String configContent) { return configContent.trim().replace("\\n", "\n"); } private static char[] toPin(String pinStr) { return (pinStr != null) ? pinStr.toCharArray() : null; } private static HsmConfig findByName(List candidates, String name) { if (name == null) { return null; } for (HsmConfig candidate : candidates) { if (name.equals(candidate.name)) { return candidate; } } return null; } // ------------------------------------------------------------------------- // Lifecycle // ------------------------------------------------------------------------- @Override public void start() throws LifecycleException { if (started) { throw new LifecycleException("RECEAIHSM001"); } lifecycle.fireLifecycleEvent(STARTING_EVENT, this); try { init(); startReloadScheduler(); registerPropertyChangeListener(); } catch (Exception e) { throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002")); } started = true; lifecycle.fireLifecycleEvent(STARTED_EVENT, this); } private void init() throws Exception { applyReloadInterval(); List candidates = loadConfigCandidates(); if (candidates.isEmpty()) { logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다."); return; } HsmConnection connection = connectFirstAvailable(candidates); applyConnection(connection); logger.warn("HsmManager] 초기화 완료. config=" + connection.config.name + ", Provider=" + connection.provider.getName()); } /** * RELOAD_INTERVAL_MINUTES 를 다시 읽어 적용한다. * * @return 값이 변경되어 스케줄 재등록이 필요하면 true */ private boolean applyReloadInterval() { long newInterval = reloadIntervalMinutes; try { String value = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES, String.valueOf(DEFAULT_RELOAD_INTERVAL_MINUTES)); if (StringUtils.isNotBlank(value)) { newInterval = Long.parseLong(value.trim()); } } catch (Exception e) { logger.warn("HsmManager] " + PROP_RELOAD_INTERVAL_MINUTES + " 값이 올바르지 않아 기존 값(" + reloadIntervalMinutes + "분)을 유지합니다: " + e.getMessage()); return false; } if (newInterval <= 0) { newInterval = DEFAULT_RELOAD_INTERVAL_MINUTES; } if (newInterval == reloadIntervalMinutes) { return false; } reloadIntervalMinutes = newInterval; return true; } // ------------------------------------------------------------------------- // 연결 // ------------------------------------------------------------------------- /** * 후보 설정을 primary -> secondary 순서로 시도하여 최초로 성공한 연결을 반환한다. * 각 후보는 Provider 생성 + KeyStore.load + 키 목록 조회(probe)까지 모두 성공해야 * 정상으로 간주한다. 전부 실패하면 마지막 예외를 던진다. */ private HsmConnection connectFirstAvailable(List candidates) throws Exception { Exception lastException = null; for (HsmConfig config : candidates) { try { HsmConnection connection = connect(config); logger.warn("HsmManager] HSM 연결 성공. config=" + config.name + ", Provider=" + connection.provider.getName()); return connection; } catch (Exception e) { lastException = e; recordError(config.name + " 설정 연결 실패: " + e.getMessage()); logger.warn("HsmManager] HSM 연결 실패. config=" + config.name + " : " + e.getMessage(), e); } } if (lastException != null) { throw lastException; } throw new HsmException("HSM 연결 후보 설정이 없습니다."); } /** * 단일 설정으로 Provider/KeyStore 를 새로 생성한다. Security 에는 아직 등록하지 않으므로 * 여기서 실패해도 현재 사용 중인 Provider/keyStore 는 영향을 받지 않는다. */ private HsmConnection connect(HsmConfig config) throws Exception { Provider provider = createProvider(config.configContent); KeyStore ks = KeyStore.getInstance("PKCS11", provider); ks.load(null, config.pin); logKeyStore(ks); return new HsmConnection(config, provider, ks); } /** * 신규 연결을 멤버변수로 승격한다. 기존 Provider 는 이 시점에서만 logout/제거된다. */ private void applyConnection(HsmConnection connection) { Provider oldProvider = this.pkcs11Provider; HsmConfig oldConfig = this.activeConfig; if (oldProvider != null && oldProvider != connection.provider) { if (oldProvider instanceof AuthProvider) { try { ((AuthProvider) oldProvider).logout(); } catch (Exception logoutEx) { logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage()); } } Security.removeProvider(oldProvider.getName()); } // 동일 이름으로 이미 등록된 Provider 가 있으면 제거 후 재등록 Provider registered = Security.getProvider(connection.provider.getName()); if (registered != null && registered != connection.provider) { Security.removeProvider(registered.getName()); } Security.addProvider(connection.provider); this.pkcs11Provider = connection.provider; this.keyStore = connection.keyStore; this.activeConfig = connection.config; this.pin = connection.config.pin; this.activeSince = System.currentTimeMillis(); if (oldConfig != null && !oldConfig.name.equals(connection.config.name)) { failoverCount++; logger.warn("HsmManager] HSM 설정 절체: " + oldConfig.name + " -> " + connection.config.name); } } private void logKeyStore(KeyStore keyStore) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { 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) { logger.warn("HsmManager] HSM - secretKey null {} : [{}]", alias, secretKey); } } } logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]"); } // ------------------------------------------------------------------------- // 주기적 재로드 // ------------------------------------------------------------------------- /** * 별도 스레드에서 주기적으로 KeyStore 를 다시 로드하여 * 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, reloadIntervalMinutes, reloadIntervalMinutes, TimeUnit.MINUTES ); logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + reloadIntervalMinutes + "분"); } /** RELOAD_INTERVAL_MINUTES 변경 시 스케줄을 다시 등록한다. */ private synchronized void rescheduleReload() { if (scheduler == null || scheduler.isShutdown()) { return; } if (reloadFuture != null) { reloadFuture.cancel(false); } reloadFuture = scheduler.scheduleWithFixedDelay( this::reloadKeyStoreSafely, reloadIntervalMinutes, reloadIntervalMinutes, TimeUnit.MINUTES ); logger.warn("HsmManager] KeyStore 재로드 주기 변경 적용: " + reloadIntervalMinutes + "분"); } /** * 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다. * (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다) */ private void reloadKeyStoreSafely() { try { reloadKeyStoreIfNeeded(); } catch (Throwable t) { logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage(), t); } } /** * KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시 * 즉시 재시도용으로 직접 호출할 수도 있다. * * 처리 순서: * 1) PropManager 에서 설정을 매번 새로 읽는다 (변경값 즉시 반영) * 2) 설정이 변경되었거나 아직 연결이 없으면 primary -> secondary 순으로 전체 재연결 * 3) 현재 secondary 로 동작 중이면 primary 복귀를 먼저 시도 * 4) 그 외에는 기존 keyStore 인스턴스에 다시 load() (PKCS11 세션 재사용). * 실패하면 primary -> secondary 순으로 전체 재연결 * * 어느 경로든 신규 연결이 완전히 성공한 경우에만 keyStore 멤버변수를 교체한다. */ public synchronized void reloadKeyStoreIfNeeded() throws Exception { List candidates = loadConfigCandidates(); if (candidates.isEmpty()) { logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않아 재로드를 건너뜁니다."); return; } HsmConfig active = this.activeConfig; HsmConfig currentCandidate = findByName(candidates, active == null ? null : active.name); // 2) 미연결 상태이거나 현재 사용 중인 설정값 자체가 변경된 경우 if (active == null || pkcs11Provider == null || currentCandidate == null || !currentCandidate.sameAs(active)) { logger.warn("HsmManager] HSM 설정 변경 또는 미연결 상태 감지 → 전체 재연결을 수행합니다."); reconnect(candidates); return; } // 3) secondary 로 동작 중이면 primary 복귀를 우선 시도 HsmConfig preferred = candidates.get(0); if (!preferred.name.equals(active.name)) { try { HsmConnection connection = connect(preferred); applyConnection(connection); recordReloadSuccess(preferred.name + " 설정으로 복귀 성공"); logger.warn("HsmManager] " + preferred.name + " 설정으로 복귀했습니다."); return; } catch (Exception e) { logger.warn("HsmManager] " + preferred.name + " 복귀 시도 실패. 현재 " + active.name + " 설정을 유지합니다: " + e.getMessage()); } } // 4) 현재 연결 유지 재로드 (세션 재사용) try { if (keyStore != null) { keyStore.load(null, pin); logKeyStore(keyStore); logger.warn("HsmManager] KeyStore 재로드 완료 (config=" + active.name + ", 기존 세션 재사용)."); } else { KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider); ks.load(null, pin); logKeyStore(ks); this.keyStore = ks; logger.warn("HsmManager] KeyStore 신규 생성 완료 (config=" + active.name + ")."); } recordReloadSuccess(active.name + " 설정 재로드 성공"); } catch (Exception e) { logger.warn("HsmManager] KeyStore 재로드 실패:" + e.getMessage(), e); logger.warn("HsmManager] 후보 설정(PRIMARY→SECONDARY) 전체 재연결을 시도합니다."); reconnect(candidates); } } /** * 세션 누적, 네트워크 단절, 설정 변경 등으로 일반 재로드가 더 이상 복구되지 않을 때 * primary -> secondary 순서로 Provider 를 완전히 새로 생성한다. * * 신규 Provider/KeyStore 준비가 완전히 성공한 후에만 기존 Provider 를 제거하고 교체한다. * 모든 후보가 실패하면 기존 Provider 와 keyStore 를 그대로 유지한다 * (서비스 중단보다 마지막 정상 상태 보존을 우선). */ private void reconnect(List candidates) throws Exception { try { HsmConnection connection = connectFirstAvailable(candidates); applyConnection(connection); recordReloadSuccess(connection.config.name + " 설정 재연결 성공"); logger.warn("HsmManager] HSM 재연결 성공. config=" + connection.config.name); } catch (Exception e) { recordReloadFailure(e.getMessage()); logger.warn("HsmManager] HSM 재연결 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e); throw e; } } private void recordReloadSuccess(String detail) { lastReloadAt = System.currentTimeMillis(); lastReloadResult = "SUCCESS - " + detail; reloadSuccessCount++; } private void recordReloadFailure(String detail) { lastReloadAt = System.currentTimeMillis(); lastReloadResult = "FAIL - " + detail; reloadFailCount++; recordError(detail); } private void recordError(String message) { lastErrorMessage = message; lastErrorAt = System.currentTimeMillis(); } // ------------------------------------------------------------------------- // PropManager 변경 즉시 반영 // ------------------------------------------------------------------------- private void registerPropertyChangeListener() { if (propListenerRegistered) { return; } try { PropManager.getInstance().addPropertyChangeListener(this); propListenerRegistered = true; logger.warn("HsmManager] PropManager PropertyChangeListener 등록 완료"); } catch (Exception e) { logger.warn("HsmManager] PropManager PropertyChangeListener 등록 실패(주기적 재로드로 대체): " + e.getMessage()); } } private void unregisterPropertyChangeListener() { if (!propListenerRegistered) { return; } try { PropManager.getInstance().removePropertyChangeListener(this); } catch (Exception e) { logger.warn("HsmManager] PropManager PropertyChangeListener 해제 실패(무시): " + e.getMessage()); } propListenerRegistered = false; } /** * 관리 포털에서 PropManager.reload("HSM") 또는 setProperty 를 호출하면 수신된다. * 재로드 자체는 HSM 통신을 수반하므로 호출 스레드를 막지 않도록 스케줄러 스레드에 위임한다. */ @Override public void propertyChange(PropertyChangeEvent evt) { if (!isHsmGroupEvent(evt)) { return; } if (applyReloadInterval()) { rescheduleReload(); } ScheduledExecutorService currentScheduler = this.scheduler; if (currentScheduler == null || currentScheduler.isShutdown()) { return; } logger.warn("HsmManager] HSM 프로퍼티 변경 감지 → 설정 재적용을 요청합니다."); currentScheduler.execute(this::reloadKeyStoreSafely); } /** * PropManager 는 reload(group) 시 propertyName 에 그룹명을, setProperty 시 키명을 담고 * source 에는 항상 해당 그룹의 PropGroupVO 를 담는다. 두 경우를 모두 인식한다. */ private boolean isHsmGroupEvent(PropertyChangeEvent evt) { if (evt == null) { return false; } if (GROUP_NAME.equals(evt.getPropertyName())) { return true; } Object source = evt.getSource(); return (source instanceof PropGroupVO) && GROUP_NAME.equals(((PropGroupVO) source).getName()); } // ------------------------------------------------------------------------- // Provider 생성 // ------------------------------------------------------------------------- /** * 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); unregisterPropertyChangeListener(); if (reloadFuture != null) { reloadFuture.cancel(false); } if (scheduler != null) { scheduler.shutdown(); } if (pkcs11Provider != null) { if (pkcs11Provider instanceof AuthProvider) { try { ((AuthProvider) pkcs11Provider).logout(); } catch (Exception e) { logger.warn("HsmManager] 종료 시 logout 실패(무시): " + e.getMessage()); } } Security.removeProvider(pkcs11Provider.getName()); } pkcs11Provider = null; keyStore = null; activeConfig = null; activeSince = 0; 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; } /** * isReady() 와 달리 실제 HSM 호출로 세션 생존 여부까지 확인하는 헬스체크. * 모니터링/헬스체크 엔드포인트에서 사용을 권장한다. */ public boolean isHealthy() { if (!isReady()) { return false; } try { logKeyStore(keyStore); return true; } catch (Exception e) { return false; } } // ------------------------------------------------------------------------- // 모니터링용 조회 (HsmStatusController) // ------------------------------------------------------------------------- /** 현재 연결에 사용 중인 설정 이름(PRIMARY/SECONDARY). 미연결이면 null. */ public String getActiveConfigName() { HsmConfig config = this.activeConfig; return (config == null) ? null : config.name; } /** 현재 secondary 설정으로 절체된 상태인지 여부. */ public boolean isUsingSecondaryConfig() { return CONFIG_SECONDARY.equals(getActiveConfigName()); } /** 현재 연결에 실제로 사용된 pkcs11.cfg 내용. PIN 은 포함되지 않는다. */ public String getActiveConfigContent() { HsmConfig config = this.activeConfig; return (config == null) ? null : config.configContent; } /** * 현재 PropManager 값 기준으로 실제 적용될 pkcs11.cfg 내용을 후보별로 반환한다. * PIN 은 포함하지 않는다. (설정이름 -> cfg 내용, primary 우선순) */ public java.util.Map getResolvedConfigContents() { java.util.Map contents = new java.util.LinkedHashMap<>(); try { for (HsmConfig config : loadConfigCandidates()) { contents.put(config.name, config.configContent); } } catch (Exception e) { logger.warn("HsmManager] 연결 후보 설정 조회 실패: " + e.getMessage()); } return contents; } /** 현재 KeyStore 의 alias 목록. HSM 통신이 발생한다. */ public List getKeyAliases() throws Exception { KeyStore ks = this.keyStore; if (ks == null) { return Collections.emptyList(); } List aliases = new ArrayList<>(); java.util.Enumeration e = ks.aliases(); while (e.hasMoreElements()) { aliases.add(e.nextElement()); } return aliases; } public String getProviderName() { Provider provider = this.pkcs11Provider; return (provider == null) ? null : provider.getName(); } public long getReloadIntervalMinutes() { return reloadIntervalMinutes; } /** 현재 설정으로 연결된 시각(epoch millis). 미연결이면 0. */ public long getActiveSince() { return activeSince; } public long getLastReloadAt() { return lastReloadAt; } public String getLastReloadResult() { return lastReloadResult; } public String getLastErrorMessage() { return lastErrorMessage; } public long getLastErrorAt() { return lastErrorAt; } public int getReloadSuccessCount() { return reloadSuccessCount; } public int getReloadFailCount() { return reloadFailCount; } /** primary <-> secondary 절체가 발생한 횟수. */ public int getFailoverCount() { return failoverCount; } }