249 lines
8.3 KiB
Java
249 lines
8.3 KiB
Java
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<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());
|
|
}
|
|
|
|
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<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;
|
|
}
|
|
}
|
|
}
|