package com.eactive.eai.common.security; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.xml.bind.DatatypeConverter; import org.springframework.stereotype.Component; 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.security.keyderiv.DerivedKey; import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy; 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.common.util.Logger; import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class CryptoModuleManager implements Lifecycle { static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); private static final ObjectMapper objectMapper = new ObjectMapper(); private final ConcurrentHashMap configMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap strategyCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap dynamicKeyCache = new ConcurrentHashMap<>(); private boolean started; private final LifecycleSupport lifecycle = new LifecycleSupport(this); private CryptoModuleManager() {} public static CryptoModuleManager getInstance() { return ApplicationContextProvider.getContext().getBean(CryptoModuleManager.class); } @Override public void start() throws LifecycleException { if (started) throw new LifecycleException("RECEAICRY001"); lifecycle.fireLifecycleEvent(STARTING_EVENT, this); try { loadAll(); } catch (Exception e) { throw new LifecycleException("RECEAICRY002"); } started = true; lifecycle.fireLifecycleEvent(STARTED_EVENT, this); } @Override public void stop() throws LifecycleException { if (!started) throw new LifecycleException("RECEAICRY003"); lifecycle.fireLifecycleEvent(STOPING_EVENT, this); configMap.clear(); dynamicKeyCache.clear(); strategyCache.clear(); started = false; lifecycle.fireLifecycleEvent(STOPPED_EVENT, this); } private void loadAll() { CryptoModuleConfigLoader loader = ctx().getBean(CryptoModuleConfigLoader.class); CryptoModuleConfigMapper mapper = ctx().getBean(CryptoModuleConfigMapper.class); List all = loader.findAll(); configMap.clear(); for (CryptoModuleConfig entity : all) { if ("Y".equalsIgnoreCase(entity.getUseYn())) { CryptoModuleConfigVO vo = mapper.toVo(entity); configMap.put(vo.getCryptoName(), vo); } } logger.warn("CryptoModuleManager] 암호화모듈 로드 완료. 건수=" + configMap.size()); } public void reload(String cryptoId) { CryptoModuleConfigLoader loader = ctx().getBean(CryptoModuleConfigLoader.class); CryptoModuleConfigMapper mapper = ctx().getBean(CryptoModuleConfigMapper.class); configMap.values().removeIf(vo -> cryptoId.equals(vo.getCryptoId())); evictDynamicCache(cryptoId); loader.findById(cryptoId).ifPresent(entity -> { if ("Y".equalsIgnoreCase(entity.getUseYn())) { CryptoModuleConfigVO vo = mapper.toVo(entity); configMap.put(vo.getCryptoName(), vo); logger.warn("CryptoModuleManager] 재로드 완료: " + vo.getCryptoName()); } }); } /** * STATIC 키 방식 — 초기화된 CryptoModuleExtension 반환. * Cipher는 thread-safe하지 않으므로 호출마다 새 인스턴스를 생성한다. */ public CryptoModuleExtension createExtension(String cryptoName) throws Exception { CryptoModuleConfigVO vo = getVO(cryptoName); byte[] encKey = DatatypeConverter.parseHexBinary(vo.getEncKeyHex()); byte[] decKey = vo.getDecKeyHex() != null ? DatatypeConverter.parseHexBinary(vo.getDecKeyHex()) : encKey; byte[] iv = vo.getIvHex() != null ? DatatypeConverter.parseHexBinary(vo.getIvHex()) : null; return buildExtension(vo, iv, encKey, decKey); } /** * DYNAMIC 키 방식 — 전략으로 키를 도출하고 캐시를 적용한 뒤 CryptoModuleExtension 반환. */ public CryptoModuleExtension createExtension(String cryptoName, Map runtimeContext) throws Exception { CryptoModuleConfigVO vo = getVO(cryptoName); if ("STATIC".equalsIgnoreCase(vo.getKeySourceType())) { return createExtension(cryptoName); } KeyDerivationStrategy strategy = resolveStrategy(vo.getKeyDerivStrategy()); Map params = parseParams(vo.getKeyDerivParams()); String cacheKey = strategy.buildCacheKey(cryptoName, params, runtimeContext); DerivedKey derivedKey = null; if ("Y".equalsIgnoreCase(vo.getCacheYn())) { CachedDerivedKey cached = dynamicKeyCache.get(cacheKey); if (cached != null && !cached.isExpired()) { derivedKey = cached.getDerivedKey(); } } if (derivedKey == null) { derivedKey = strategy.deriveKey(params, runtimeContext); if ("Y".equalsIgnoreCase(vo.getCacheYn())) { int ttl = vo.getCacheTtlSec() != null ? vo.getCacheTtlSec() : 300; dynamicKeyCache.put(cacheKey, new CachedDerivedKey(derivedKey, ttl)); } } byte[] iv = null; if(derivedKey.getIv() != null) { iv = derivedKey.getIv(); } else if (vo.getIvHex() != null){ iv = DatatypeConverter.parseHexBinary(vo.getIvHex()); } return buildExtension(vo, iv, derivedKey.getEncKey(), derivedKey.getDecKey()); } private CryptoModuleConfigVO getVO(String cryptoName) { CryptoModuleConfigVO vo = configMap.get(cryptoName); if (vo == null) { throw new IllegalArgumentException("암호화모듈 미등록: " + cryptoName); } return vo; } private CryptoModuleExtension buildExtension(CryptoModuleConfigVO vo, byte[] iv, byte[] encKey, byte[] decKey) throws Exception { CryptoModuleExtension ext = "ARIA".equalsIgnoreCase(vo.getAlgType()) ? new ARIACryptoModuleExtension() : new AESCryptoModuleExtension(); ext.init(vo.getAlgType(), vo.getCipherMode(), vo.getPadding(), iv, encKey, decKey); return ext; } /** * DB key_deriv_strategy 컬럼에 저장된 FQCN으로 전략 클래스를 로드한다. * 한 번 로드된 인스턴스는 strategyCache에 보관하여 재사용한다. */ private KeyDerivationStrategy resolveStrategy(String fqcn) { return strategyCache.computeIfAbsent(fqcn, key -> { try { Class clazz = Class.forName(key); return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e); } }); } private Map parseParams(String json) throws Exception { if (json == null || json.trim().isEmpty()) { return new HashMap<>(); } return objectMapper.readValue(json, new TypeReference>() {}); } private void evictDynamicCache(String cryptoId) { configMap.values().stream() .filter(vo -> cryptoId.equals(vo.getCryptoId())) .map(CryptoModuleConfigVO::getCryptoName) .forEach(name -> { Iterator it = dynamicKeyCache.keySet().iterator(); while (it.hasNext()) { if (it.next().startsWith(name + ":")) it.remove(); } }); } private org.springframework.context.ApplicationContext ctx() { return ApplicationContextProvider.getContext(); } @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; } private static class CachedDerivedKey { private final DerivedKey derivedKey; private final long expireAt; CachedDerivedKey(DerivedKey derivedKey, int ttlSec) { this.derivedKey = derivedKey; this.expireAt = System.currentTimeMillis() + (ttlSec * 1000L); } boolean isExpired() { return System.currentTimeMillis() > expireAt; } DerivedKey getDerivedKey() { return derivedKey; } } }