Files
elink-online-common/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java
T
curry772 98d815e659 CryptoModuleManager 기반 암호화모듈 진단/암복호화 테스트 관리 API 추가
/manage/crypto 하위에 등록된 암호화모듈 목록/단건 조회, DYNAMIC 키
전략(KeyDerivationStrategy) 로드 상태 확인, 동적 키 캐시 키 목록 조회,
Base64 기반 암복호화 라운드트립 테스트 엔드포인트를 제공한다.
원본 암호키(encKeyHex/decKeyHex/ivHex)는 응답에 포함하지 않는다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:59:09 +09:00

302 lines
10 KiB
Java

package com.eactive.eai.common.security;
import java.util.ArrayList;
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<String, CryptoModuleConfigVO> configMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, KeyDerivationStrategy> strategyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, CachedDerivedKey> 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<CryptoModuleConfig> 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<String, String> runtimeContext)
throws Exception {
CryptoModuleConfigVO vo = getVO(cryptoName);
if ("STATIC".equalsIgnoreCase(vo.getKeySourceType())) {
return createExtension(cryptoName);
}
KeyDerivationStrategy strategy = resolveStrategy(vo.getKeyDerivStrategy());
Map<String, String> 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());
}
/**
* 등록된 전체 암호화모듈의 진단정보를 반환한다.
* 원본 키(encKey/decKey/ivHex)는 포함하지 않는다.
*/
public List<CryptoModuleDiagnosticDTO> describeAll() {
List<CryptoModuleDiagnosticDTO> result = new ArrayList<>();
for (String cryptoName : configMap.keySet()) {
result.add(describe(cryptoName));
}
return result;
}
/**
* 단일 암호화모듈의 진단정보를 반환한다. DYNAMIC 키 방식인 경우 resolveStrategy를 통해
* 전략 클래스 로드 성공 여부를 함께 확인한다. 원본 키(encKey/decKey/ivHex)는 포함하지 않는다.
*/
public CryptoModuleDiagnosticDTO describe(String cryptoName) {
CryptoModuleConfigVO vo = getVO(cryptoName);
CryptoModuleDiagnosticDTO dto = new CryptoModuleDiagnosticDTO();
dto.setCryptoName(vo.getCryptoName());
dto.setCryptoDesc(vo.getCryptoDesc());
dto.setAlgType(vo.getAlgType());
dto.setCipherMode(vo.getCipherMode());
dto.setPadding(vo.getPadding());
dto.setHasIv(vo.getIvHex() != null);
dto.setKeySourceType(vo.getKeySourceType());
dto.setCacheYn(vo.getCacheYn());
dto.setCacheTtlSec(vo.getCacheTtlSec());
dto.setUseYn(vo.getUseYn());
if (!"STATIC".equalsIgnoreCase(vo.getKeySourceType()) && vo.getKeyDerivStrategy() != null) {
dto.setKeyDerivStrategy(vo.getKeyDerivStrategy());
try {
KeyDerivationStrategy strategy = resolveStrategy(vo.getKeyDerivStrategy());
dto.setStrategyLoaded(true);
dto.setStrategyClassName(strategy.getClass().getName());
} catch (Exception e) {
dto.setStrategyLoaded(false);
dto.setStrategyLoadError(e.getMessage());
}
}
return dto;
}
/**
* 동적 키 캐시에 존재하는 캐시 키 목록을 반환한다. (전략별 buildCacheKey 결과이며, 원본 키 값이 아니다)
*/
public List<String> listDynamicCacheKeys() {
return new ArrayList<>(dynamicKeyCache.keySet());
}
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.trim());
return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e);
}
});
}
private Map<String, String> parseParams(String json) throws Exception {
if (json == null || json.trim().isEmpty()) {
return new HashMap<>();
}
return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {});
}
private void evictDynamicCache(String cryptoId) {
configMap.values().stream()
.filter(vo -> cryptoId.equals(vo.getCryptoId()))
.map(CryptoModuleConfigVO::getCryptoName)
.forEach(name -> {
Iterator<String> 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;
}
}
}