feat: 암호화모듈 프레임워크 신규 구현
- CryptoModuleExtension 인터페이스: BC 프로바이더, AAD, 편의 메서드 오버로드 추가 - AESCryptoModuleExtension: CBC/GCM/ECB/FF1 지원, GCM 랜덤 nonce, AAD 처리 - ARIACryptoModuleExtension: ARIA 알고리즘 지원 - CryptoModuleConfigVO: JPA Entity 분리를 위한 Lombok VO - CryptoModuleManager: VO 패턴 적용, FQCN 기반 전략 동적 로딩(Class.forName) - CryptoModuleService: STATIC/DYNAMIC × AAD 조합 8가지 오버로드 - CryptoModuleConfigMapper: MapStruct Entity→VO 매퍼 - KeyDerivationStrategy 인터페이스 및 DerivedKey - HsmContextXorKeyDerivationStrategy: HSM 마스터키 XOR 컨텍스트값 전략 - ReloadCryptoModuleCommand: 설정 런타임 재로드 커맨드 - build.gradle: BouncyCastle bcprov-jdk15on:1.70 의존성 추가 - 단위 테스트 전체 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.eactive.eai.agent.security;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.security.CryptoModuleManager;
|
||||
|
||||
public class ReloadCryptoModuleCommand extends Command {
|
||||
|
||||
@Override
|
||||
public Object execute() throws CommandException {
|
||||
try {
|
||||
String cryptoId = (String) args;
|
||||
CryptoModuleManager.getInstance().reload(cryptoId);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new CommandException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Key;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.jcajce.spec.FPEParameterSpec;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
public class AESCryptoModuleExtension implements CryptoModuleExtension {
|
||||
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
private static final String DEFAULT_PROVIDER = "BC";
|
||||
|
||||
private Cipher encryptEngine;
|
||||
private Cipher decryptEngine;
|
||||
private String mode;
|
||||
private String pad;
|
||||
private byte[] iv;
|
||||
private byte[] encKey;
|
||||
private byte[] decKey;
|
||||
/** 명시적 프로바이더. null이면 모든 모드에서 BC(DEFAULT_PROVIDER)를 사용한다. */
|
||||
private String provider;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// init — 모든 오버로드는 최종적으로 6+provider 버전에 위임한다
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey) throws Exception {
|
||||
init(alg, mode, pad, null, encKey, decKey, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey) throws Exception {
|
||||
init(alg, mode, pad, iv, encKey, decKey, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
init(alg, mode, pad, null, encKey, decKey, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
this.mode = mode;
|
||||
this.pad = pad;
|
||||
this.iv = iv;
|
||||
this.encKey = encKey;
|
||||
this.decKey = decKey;
|
||||
this.provider = (provider != null && !provider.isEmpty()) ? provider : null;
|
||||
|
||||
// GCM 포함 모든 모드에서 BC 프로바이더를 사용 — 기본값 BC
|
||||
if (Security.getProvider("BC") == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
if (!mode.equalsIgnoreCase("GCM")) {
|
||||
String m = mode != null ? mode : "";
|
||||
String p = pad != null ? pad : "";
|
||||
String transformation = String.format("AES/%s/%s", m, p);
|
||||
|
||||
Key encKeySpec = new SecretKeySpec(encKey, "AES");
|
||||
Key decKeySpec = new SecretKeySpec(decKey, "AES");
|
||||
AlgorithmParameterSpec param = iv != null ? new IvParameterSpec(iv) : null;
|
||||
if (m.toLowerCase().contains("ff")) {
|
||||
param = new FPEParameterSpec(256, iv);
|
||||
}
|
||||
|
||||
String prov = this.provider != null ? this.provider : DEFAULT_PROVIDER;
|
||||
this.encryptEngine = Cipher.getInstance(transformation, prov);
|
||||
this.decryptEngine = Cipher.getInstance(transformation, prov);
|
||||
this.encryptEngine.init(Cipher.ENCRYPT_MODE, encKeySpec, param, (SecureRandom) null);
|
||||
this.decryptEngine.init(Cipher.DECRYPT_MODE, decKeySpec, param, (SecureRandom) null);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// encrypt
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int encrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.encryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
return encrypt(src, sindex, slength, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AAD를 명시한 암호화.
|
||||
* GCM 모드에서 aad != null 이면 해당 값을 사용하고, null 이면 iv를 AAD로 사용하는 기본 동작을 따른다.
|
||||
* 비-GCM 모드는 AAD를 무시하고 사전 초기화된 엔진으로 처리한다.
|
||||
*/
|
||||
@Override
|
||||
public byte[] encrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
if (this.mode.equalsIgnoreCase("GCM")) {
|
||||
Key key = new SecretKeySpec(this.encKey, "AES");
|
||||
byte[] nonce = new byte[GCM_IV_LENGTH];
|
||||
new SecureRandom().nextBytes(nonce);
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce);
|
||||
|
||||
String prov = this.provider != null ? this.provider : DEFAULT_PROVIDER;
|
||||
Cipher cipher = Cipher.getInstance(String.format("AES/%s/%s", this.mode, this.pad), prov);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);
|
||||
|
||||
byte[] effectiveAad = aad != null ? aad : this.iv;
|
||||
if (effectiveAad != null) cipher.updateAAD(effectiveAad);
|
||||
|
||||
byte[] cipherTextWithTag = cipher.doFinal(src, sindex, slength);
|
||||
byte[] output = new byte[GCM_IV_LENGTH + cipherTextWithTag.length];
|
||||
System.arraycopy(nonce, 0, output, 0, GCM_IV_LENGTH);
|
||||
System.arraycopy(cipherTextWithTag, 0, output, GCM_IV_LENGTH, cipherTextWithTag.length);
|
||||
return output;
|
||||
}
|
||||
|
||||
int outLen = this.encryptEngine.getOutputSize(slength);
|
||||
byte[] dst = new byte[outLen];
|
||||
int actual = encrypt(src, sindex, slength, dst, 0);
|
||||
return ByteBuffer.allocate(actual).put(dst, 0, actual).array();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// decrypt
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int decrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.decryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
return decrypt(src, sindex, slength, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AAD를 명시한 복호화.
|
||||
* GCM 모드에서 aad != null 이면 해당 값을 사용하고, null 이면 iv를 AAD로 사용하는 기본 동작을 따른다.
|
||||
* 비-GCM 모드는 AAD를 무시하고 사전 초기화된 엔진으로 처리한다.
|
||||
*/
|
||||
@Override
|
||||
public byte[] decrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
if (this.mode.equalsIgnoreCase("GCM")) {
|
||||
Key key = new SecretKeySpec(this.decKey, "AES");
|
||||
byte[] nonce = new byte[GCM_IV_LENGTH];
|
||||
System.arraycopy(src, sindex, nonce, 0, GCM_IV_LENGTH);
|
||||
byte[] cipherTextWithTag = new byte[slength - GCM_IV_LENGTH];
|
||||
System.arraycopy(src, sindex + GCM_IV_LENGTH, cipherTextWithTag, 0, cipherTextWithTag.length);
|
||||
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, nonce);
|
||||
String prov = this.provider != null ? this.provider : DEFAULT_PROVIDER;
|
||||
Cipher cipher = Cipher.getInstance(String.format("AES/%s/%s", this.mode, this.pad), prov);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);
|
||||
|
||||
byte[] effectiveAad = aad != null ? aad : this.iv;
|
||||
if (effectiveAad != null) cipher.updateAAD(effectiveAad);
|
||||
|
||||
return cipher.doFinal(cipherTextWithTag);
|
||||
}
|
||||
return this.decryptEngine.doFinal(src, sindex, slength);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Key;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.jcajce.spec.FPEParameterSpec;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
public class ARIACryptoModuleExtension implements CryptoModuleExtension {
|
||||
|
||||
private Cipher encryptEngine;
|
||||
private Cipher decryptEngine;
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey) throws Exception {
|
||||
this.init("ARIA", mode, pad, (byte[]) null, encKey, decKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey) throws Exception {
|
||||
if (Security.getProvider("BC") == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
String m = mode != null ? mode : "";
|
||||
String p = pad != null ? pad : "";
|
||||
Key encKeySpec = new SecretKeySpec(encKey, "ARIA");
|
||||
Key decKeySpec = new SecretKeySpec(decKey, "ARIA");
|
||||
AlgorithmParameterSpec param = iv != null ? new IvParameterSpec(iv) : null;
|
||||
if (m.toLowerCase().contains("ff")) {
|
||||
param = new FPEParameterSpec(256, iv);
|
||||
}
|
||||
this.encryptEngine = Cipher.getInstance(String.format("ARIA/%s/%s", m, p));
|
||||
this.encryptEngine.init(Cipher.ENCRYPT_MODE, encKeySpec, param, (SecureRandom) null);
|
||||
this.decryptEngine = Cipher.getInstance(String.format("ARIA/%s/%s", m, p));
|
||||
this.decryptEngine.init(Cipher.DECRYPT_MODE, decKeySpec, param, (SecureRandom) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int encrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.encryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
int size = (slength + 17) / this.encryptEngine.getBlockSize() * this.encryptEngine.getBlockSize();
|
||||
size += (slength + 17) % this.encryptEngine.getBlockSize() == 0 ? 0 : this.encryptEngine.getBlockSize();
|
||||
byte[] dst = new byte[this.encryptEngine.getOutputSize(size)];
|
||||
size = this.encrypt(src, sindex, slength, dst, 0);
|
||||
return ByteBuffer.allocate(size).put(dst, 0, size).array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception {
|
||||
return this.decryptEngine.doFinal(src, sindex, slength, dst, dindex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] src, int sindex, int slength) throws Exception {
|
||||
return this.decryptEngine.doFinal(src, sindex, slength);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CryptoModuleConfigVO {
|
||||
|
||||
String cryptoId;
|
||||
String cryptoName;
|
||||
String cryptoDesc;
|
||||
String algType;
|
||||
String cipherMode;
|
||||
String padding;
|
||||
String ivHex;
|
||||
String keySourceType;
|
||||
String encKeyHex;
|
||||
String decKeyHex;
|
||||
String keyDerivStrategy;
|
||||
String keyDerivParams;
|
||||
String cacheYn;
|
||||
Integer cacheTtlSec;
|
||||
String useYn;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
public interface CryptoModuleExtension {
|
||||
|
||||
void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey) throws Exception;
|
||||
|
||||
void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey) throws Exception;
|
||||
|
||||
/** 프로바이더를 명시한 초기화. 미지원 구현체는 provider를 무시하고 기본 동작한다. */
|
||||
default void init(String alg, String mode, String pad, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
init(alg, mode, pad, encKey, decKey);
|
||||
}
|
||||
|
||||
/** 프로바이더를 명시한 초기화 (IV 포함). 미지원 구현체는 provider를 무시하고 기본 동작한다. */
|
||||
default void init(String alg, String mode, String pad, byte[] iv, byte[] encKey, byte[] decKey, String provider) throws Exception {
|
||||
init(alg, mode, pad, iv, encKey, decKey);
|
||||
}
|
||||
|
||||
int encrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception;
|
||||
|
||||
byte[] encrypt(byte[] src, int sindex, int slength) throws Exception;
|
||||
|
||||
/** AAD를 명시한 암호화. null이면 구현체 기본 동작(IV를 AAD로 사용하거나 AAD 없음)을 따른다. */
|
||||
default byte[] encrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
return encrypt(src, sindex, slength);
|
||||
}
|
||||
|
||||
default byte[] encrypt(byte[] src) throws Exception {
|
||||
return encrypt(src, 0, src.length);
|
||||
}
|
||||
|
||||
default byte[] encrypt(byte[] src, byte[] aad) throws Exception {
|
||||
return encrypt(src, 0, src.length, aad);
|
||||
}
|
||||
|
||||
int decrypt(byte[] src, int sindex, int slength, byte[] dst, int dindex) throws Exception;
|
||||
|
||||
byte[] decrypt(byte[] src, int sindex, int slength) throws Exception;
|
||||
|
||||
/** AAD를 명시한 복호화. null이면 구현체 기본 동작을 따른다. */
|
||||
default byte[] decrypt(byte[] src, int sindex, int slength, byte[] aad) throws Exception {
|
||||
return decrypt(src, sindex, slength);
|
||||
}
|
||||
|
||||
default byte[] decrypt(byte[] src) throws Exception {
|
||||
return decrypt(src, 0, src.length);
|
||||
}
|
||||
|
||||
default byte[] decrypt(byte[] src, byte[] aad) throws Exception {
|
||||
return decrypt(src, 0, src.length, aad);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
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 = vo.getIvHex() != null ? DatatypeConverter.parseHexBinary(vo.getIvHex()) : null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* 어댑터 필터 등에서 암복호화를 위임하는 진입점.
|
||||
*
|
||||
* STATIC 키 방식:
|
||||
* service.encrypt("MODULE_NAME", plainBytes);
|
||||
*
|
||||
* DYNAMIC 키 방식 (런타임 컨텍스트 전달):
|
||||
* Map<String,String> ctx = Map.of("X-Api-Enc-Key", request.getHeader("X-Api-Enc-Key"));
|
||||
* service.encrypt("MODULE_NAME", ctx, plainBytes);
|
||||
*
|
||||
* AAD 명시 방식 (GCM 인증 데이터 전달):
|
||||
* byte[] aad = request.getHeader("X-Api-Request-Id").getBytes(StandardCharsets.UTF_8);
|
||||
* service.encrypt("MODULE_NAME", ctx, aad, plainBytes);
|
||||
*/
|
||||
@Service
|
||||
public class CryptoModuleService {
|
||||
|
||||
public static CryptoModuleService getInstance() {
|
||||
return ApplicationContextProvider.getContext().getBean(CryptoModuleService.class);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// STATIC 키
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public byte[] encrypt(String cryptoName, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length);
|
||||
}
|
||||
|
||||
public byte[] encrypt(String cryptoName, byte[] aad, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length, aad);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, byte[] aad, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length, aad);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// DYNAMIC 키
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
public byte[] encrypt(String cryptoName, Map<String, String> runtimeContext, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length);
|
||||
}
|
||||
|
||||
public byte[] encrypt(String cryptoName, Map<String, String> runtimeContext, byte[] aad, byte[] plainBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.encrypt(plainBytes, 0, plainBytes.length, aad);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, Map<String, String> runtimeContext, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length);
|
||||
}
|
||||
|
||||
public byte[] decrypt(String cryptoName, Map<String, String> runtimeContext, byte[] aad, byte[] cipherBytes) throws Exception {
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance().createExtension(cryptoName, runtimeContext);
|
||||
return ext.decrypt(cipherBytes, 0, cipherBytes.length, aad);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
public class DerivedKey {
|
||||
|
||||
private final byte[] encKey;
|
||||
private final byte[] decKey;
|
||||
|
||||
public DerivedKey(byte[] encKey, byte[] decKey) {
|
||||
this.encKey = encKey;
|
||||
this.decKey = decKey != null ? decKey : encKey;
|
||||
}
|
||||
|
||||
public byte[] getEncKey() {
|
||||
return encKey;
|
||||
}
|
||||
|
||||
public byte[] getDecKey() {
|
||||
return decKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.eactive.eai.common.security.keyderiv;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface KeyDerivationStrategy {
|
||||
|
||||
/**
|
||||
* 키를 도출한다.
|
||||
*
|
||||
* @param params DB의 key_deriv_params (JSON 파싱된 Map)
|
||||
* @param runtimeContext 런타임 컨텍스트 (HTTP 헤더, 거래 정보 등 호출자가 채워서 전달)
|
||||
*/
|
||||
DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception;
|
||||
|
||||
/**
|
||||
* 캐시 키를 생성한다. 전략마다 어떤 runtimeContext 값이 키를 결정하는지 다르다.
|
||||
*
|
||||
* @param cryptoName 암호화모듈 이름
|
||||
* @param params DB의 key_deriv_params
|
||||
* @param runtimeContext 런타임 컨텍스트
|
||||
*/
|
||||
String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext);
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.eactive.eai.common.security.keyderiv.strategy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.keyderiv.DerivedKey;
|
||||
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* HSM 마스터키 + 런타임 컨텍스트값 XOR 조합으로 키를 도출하는 전략.
|
||||
*
|
||||
* key_deriv_params (JSON) 예시:
|
||||
* {
|
||||
* "hsmKeyAlias" : "MASTER_KEY_AES",
|
||||
* "contextKey" : "X-Api-Enc-Key", -- runtimeContext에서 꺼낼 키 이름
|
||||
* "offset" : "0", -- contextKey 값 중 사용할 시작 위치 (byte)
|
||||
* "length" : "16" -- contextKey 값 중 사용할 길이 (byte)
|
||||
* }
|
||||
*/
|
||||
public class HsmContextXorKeyDerivationStrategy implements KeyDerivationStrategy {
|
||||
|
||||
/** DB key_deriv_strategy 컬럼에 사용하는 FQCN 참조용 상수. */
|
||||
public static final String STRATEGY_CLASS = HsmContextXorKeyDerivationStrategy.class.getName();
|
||||
|
||||
private static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
|
||||
private static final String PARAM_CONTEXT_KEY = "contextKey";
|
||||
private static final String PARAM_OFFSET = "offset";
|
||||
private static final String PARAM_LENGTH = "length";
|
||||
|
||||
@Override
|
||||
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
|
||||
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
|
||||
String contextKey = required(params, PARAM_CONTEXT_KEY);
|
||||
int offset = Integer.parseInt(params.getOrDefault(PARAM_OFFSET, "0"));
|
||||
int length = Integer.parseInt(required(params, PARAM_LENGTH));
|
||||
|
||||
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
|
||||
byte[] masterBytes = masterKey.getEncoded();
|
||||
|
||||
String contextValue = runtimeContext.getOrDefault(contextKey, "");
|
||||
byte[] contextBytes = contextValue.getBytes("UTF-8");
|
||||
|
||||
byte[] derived = xor(masterBytes, contextBytes, offset, length);
|
||||
return new DerivedKey(derived, derived);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
|
||||
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
|
||||
String contextValue = runtimeContext.getOrDefault(contextKey, "");
|
||||
return cryptoName + ":" + contextValue;
|
||||
}
|
||||
|
||||
private byte[] xor(byte[] masterBytes, byte[] contextBytes, int offset, int length) {
|
||||
byte[] derived = Arrays.copyOf(masterBytes, masterBytes.length);
|
||||
for (int i = 0; i < length && i < derived.length; i++) {
|
||||
int contextIdx = offset + i;
|
||||
if (contextIdx < contextBytes.length) {
|
||||
derived[i] ^= contextBytes[contextIdx];
|
||||
}
|
||||
}
|
||||
return derived;
|
||||
}
|
||||
|
||||
private String required(Map<String, String> params, String key) {
|
||||
String value = params.get(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private HsmCryptoService hsmCryptoService() {
|
||||
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.eactive.eai.common.security.mapper;
|
||||
|
||||
import org.mapstruct.InheritInverseConfiguration;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
import com.eactive.eai.common.security.CryptoModuleConfigVO;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
import com.eactive.eai.data.mapper.GenericMapper;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface CryptoModuleConfigMapper extends GenericMapper<CryptoModuleConfigVO, CryptoModuleConfig> {
|
||||
|
||||
@Override
|
||||
CryptoModuleConfigVO toVo(CryptoModuleConfig entity);
|
||||
|
||||
@InheritInverseConfiguration
|
||||
CryptoModuleConfig toEntity(CryptoModuleConfigVO vo);
|
||||
}
|
||||
Reference in New Issue
Block a user