HSM, 암호화 관련 테스트 수행 중 수정

This commit is contained in:
curry772
2026-06-29 13:16:39 +09:00
parent ab9b86e7cf
commit a923ff3f9f
11 changed files with 548 additions and 1262 deletions
@@ -44,9 +44,25 @@ public class HsmCryptoService implements PropertyChangeListener {
private static final String PROP_GROUP = "HSM";
private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";
private static final long CACHE_TTL_MS = 10 * 60 * 1000; // 10분, 필요시 PropManager로 외부화
private static class CachedKey<T> {
final T key;
final long cachedAt;
CachedKey(T key) {
this.key = key;
this.cachedAt = System.currentTimeMillis();
}
boolean isExpired() {
return System.currentTimeMillis() - cachedAt > CACHE_TTL_MS;
}
}
// HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
private final ConcurrentHashMap<String, SecretKey> secretKeyCache = new ConcurrentHashMap<String, SecretKey>();
private final ConcurrentHashMap<String, PublicKey> publicKeyCache = new ConcurrentHashMap<String, PublicKey>();
private final ConcurrentHashMap<String, CachedKey<SecretKey>> secretKeyCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, CachedKey<PublicKey>> publicKeyCache = new ConcurrentHashMap<>();
@Autowired
private PropManager propManager;
@@ -81,9 +97,9 @@ public class HsmCryptoService implements PropertyChangeListener {
*/
public PublicKey getPublicKey(String keyAlias) throws HsmException {
checkReady();
PublicKey cached = publicKeyCache.get(keyAlias);
if (cached != null) {
return cached;
CachedKey<PublicKey> cached = publicKeyCache.get(keyAlias);
if (cached != null && !cached.isExpired()) {
return cached.key;
}
try {
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
@@ -91,7 +107,7 @@ public class HsmCryptoService implements PropertyChangeListener {
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
}
PublicKey key = cert.getPublicKey();
publicKeyCache.put(keyAlias, key);
publicKeyCache.put(keyAlias, new CachedKey<>(key));
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
return key;
} catch (HsmException e) {
@@ -107,20 +123,21 @@ public class HsmCryptoService implements PropertyChangeListener {
*/
public SecretKey getSecretKey(String keyAlias) throws HsmException {
checkReady();
SecretKey cached = secretKeyCache.get(keyAlias);
if (cached != null) {
return cached;
CachedKey<SecretKey> cached = secretKeyCache.get(keyAlias);
if (cached != null && !cached.isExpired()) {
return cached.key;
}
try {
KeyStore keyStore = HsmManager.getInstance().getKeyStore();
java.security.Key key = keyStore.getKey(keyAlias, null);
if (!(key instanceof SecretKey)) {
throw new HsmException("AES 키를 찾을 수 없습니다 (CKA_EXTRACTABLE=true 확인 필요). alias=" + keyAlias);
if (key == null) {
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
}
SecretKey secretKey = (SecretKey) key;
secretKeyCache.put(keyAlias, secretKey);
logger.warn("HsmCryptoService] 대칭키 캐시 등록: alias=" + keyAlias);
secretKeyCache.put(keyAlias, new CachedKey<>(secretKey));
logger.warn("HsmCryptoService] 대칭키 캐시 등록(갱신): alias=" + keyAlias);
return secretKey;
} catch (HsmException e) {
throw e;
} catch (Exception e) {
@@ -7,10 +7,12 @@ import java.lang.reflect.Method;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.Base64;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.crypto.SecretKey;
@@ -52,11 +54,18 @@ public class HsmManager implements Lifecycle {
private static final String PROP_PIN = "PIN";
private Provider pkcs11Provider;
private KeyStore keyStore;
private volatile KeyStore keyStore;
private boolean started;
private final LifecycleSupport lifecycle = new LifecycleSupport(this);
private volatile char[] pin;
// 재로드 주기 (분 단위). 필요시 PropManager로 외부화 가능.
private static final long RELOAD_INTERVAL_MINUTES = 1;
private ScheduledExecutorService scheduler;
private ScheduledFuture<?> reloadFuture;
private HsmManager() {
}
@@ -73,6 +82,7 @@ public class HsmManager implements Lifecycle {
try {
init();
startReloadScheduler();
} catch (Exception e) {
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
}
@@ -85,7 +95,7 @@ public class HsmManager implements Lifecycle {
EncryptionManager encManager = EncryptionManager.getInstance();
String configContent = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_CONFIG));
String pin = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));
String pinStr = encManager.decryptDBData(PropManager.getInstance().getProperty(GROUP_NAME, PROP_PIN));
if (configContent == null || configContent.trim().isEmpty()) {
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
@@ -102,8 +112,8 @@ public class HsmManager implements Lifecycle {
Security.addProvider(pkcs11Provider);
keyStore = KeyStore.getInstance("PKCS11", pkcs11Provider);
char[] pinChars = (pin != null) ? pin.toCharArray() : null;
keyStore.load(null, pinChars);
this.pin = (pinStr != null) ? pinStr.toCharArray() : null;
keyStore.load(null, pin);
logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
@@ -128,6 +138,66 @@ public class HsmManager implements Lifecycle {
}
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
}
/**
* 별도 스레드에서 주기적으로 KeyStore.load() 를 다시 호출하여
* 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,
RELOAD_INTERVAL_MINUTES,
RELOAD_INTERVAL_MINUTES,
TimeUnit.MINUTES
);
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + RELOAD_INTERVAL_MINUTES + "");
}
/**
* 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다.
* (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다)
*/
private void reloadKeyStoreSafely() {
try {
reloadKeyStoreIfNeeded();
} catch (Throwable t) {
logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage());
}
}
/**
* KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
* 즉시 재시도용으로 직접 호출할 수도 있다.
*/
public synchronized void reloadKeyStoreIfNeeded() throws Exception {
if (pkcs11Provider == null) {
return; // HSM 비활성화 상태
}
KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
ks.load(null, pin);
this.keyStore = ks; // volatile 필드 교체 - 다른 스레드에서 즉시 가시성 확보
logger.warn("HsmManager] KeyStore 재로드 완료.");
logAliases(ks);
}
private void logAliases(KeyStore ks) throws Exception {
java.util.Enumeration<String> aliases = ks.aliases();
StringBuilder aliasList = new StringBuilder();
while (aliases.hasMoreElements()) {
if (aliasList.length() > 0) aliasList.append(", ");
aliasList.append(aliases.nextElement());
}
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
}
/**
* JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
@@ -164,6 +234,13 @@ public class HsmManager implements Lifecycle {
}
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
if (reloadFuture != null) {
reloadFuture.cancel(false);
}
if (scheduler != null) {
scheduler.shutdown();
}
if (pkcs11Provider != null) {
Security.removeProvider(pkcs11Provider.getName());
}
@@ -177,7 +177,7 @@ public class CryptoModuleManager implements Lifecycle {
private KeyDerivationStrategy resolveStrategy(String fqcn) {
return strategyCache.computeIfAbsent(fqcn, key -> {
try {
Class<?> clazz = Class.forName(key);
Class<?> clazz = Class.forName(key.trim());
return (KeyDerivationStrategy) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new IllegalArgumentException("전략 클래스 로드 실패: " + key, e);
@@ -1,62 +0,0 @@
package com.eactive.eai.common.security.keyderiv.strategy;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
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 마스터키 + 런타임 컨텍스트값을 연결(concatenate)한 뒤 SHA-256 해싱으로 키를 도출하는 전략.
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
*
* key_deriv_params (JSON) 예시:
* {
* "hsmKeyAlias" : "MASTER_KEY_AES",
* "contextKey" : "X-Api-Group-Seq" -- runtimeContext에서 꺼낼 키 이름
* }
*/
public class HsmContextSha256KeyDerivationStrategy implements KeyDerivationStrategy {
@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);
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
byte[] masterKeyBytes = masterKey.getEncoded();
byte[] contextBytes = runtimeContext.getOrDefault(contextKey, "")
.getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[masterKeyBytes.length + contextBytes.length];
System.arraycopy(masterKeyBytes, 0, combined, 0, masterKeyBytes.length);
System.arraycopy(contextBytes, 0, combined, masterKeyBytes.length, contextBytes.length);
byte[] derived = MessageDigest.getInstance("SHA-256").digest(combined);
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 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);
}
}