feat: HSM 키 캐시 및 PropManager 연동 초기화, DB 설정 파싱 오류 수정
- HsmCryptoService: getPublicKey/getSecretKey ConcurrentHashMap 캐시 추가 (HSM 통신 최소화) - HsmCryptoService: encryptAes/decryptAes Provider 파라미터 오버로드 추가 (non-extractable 키 지원) - HsmCryptoService: PropertyChangeListener 구현, HSM.CACHE_RELOAD_YN=Y 시 clearKeyCache() 자동 호출 - HsmCryptoService: @PostConstruct NPE 수정 - PropManager.getInstance() → @Autowired 직접 주입 - HsmManager: DB PKCS11_CONFIG 리터럴 \n → 실제 줄바꿈 치환 (파싱 오류 수정) - HsmManager: 기동 시 HSM 키 목록 로그 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,57 +1,99 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* SafeNet ProtectServer HSM 암복호화 서비스 (JDK 8)
|
||||
*
|
||||
* [키 조회]
|
||||
* getPublicKey - 공개키 반환 (외부 노출)
|
||||
* getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요, 외부 노출)
|
||||
* getPrivateKey - 내부 전용 (개인키는 HSM 외부로 반출하지 않는 것이 원칙)
|
||||
* getPublicKey - 공개키 반환 (외부 노출). 최초 1회만 HSM 통신, 이후 캐시 반환.
|
||||
* getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요). 최초 1회만 HSM 통신, 이후 캐시 반환.
|
||||
*
|
||||
* [암복호화]
|
||||
* encryptRsa - 공개키로 JVM 소프트웨어 암호화 (HSM 불필요)
|
||||
* decryptRsa - alias 기반, HSM 내부 복호화 (HsmManager 미초기화 시 JVM fallback)
|
||||
* decryptRsa - alias 기반, HSM 내부 복호화 (매 호출마다 HSM 통신 발생 - 불가피)
|
||||
* encryptAes - SecretKey 객체로 AES/CBC 암호화
|
||||
* decryptAes - SecretKey 객체로 AES/CBC 복호화
|
||||
*/
|
||||
@Service
|
||||
public class HsmCryptoService {
|
||||
public class HsmCryptoService implements PropertyChangeListener {
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
|
||||
private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||
|
||||
private static final String PROP_GROUP = "HSM";
|
||||
private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";
|
||||
|
||||
// HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
|
||||
private final ConcurrentHashMap<String, SecretKey> secretKeyCache = new ConcurrentHashMap<String, SecretKey>();
|
||||
private final ConcurrentHashMap<String, PublicKey> publicKeyCache = new ConcurrentHashMap<String, PublicKey>();
|
||||
|
||||
@Autowired
|
||||
private PropManager propManager;
|
||||
|
||||
@PostConstruct
|
||||
public void registerPropertyChangeListener() {
|
||||
propManager.addPropertyChangeListener(this);
|
||||
logger.warn("HsmCryptoService] PropManager PropertyChangeListener 등록 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리포털에서 PropManager.reload("HSM") 호출 시 이벤트를 수신한다.
|
||||
* HSM.CACHE_RELOAD_YN = Y 이면 키 캐시를 초기화한다.
|
||||
*/
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (!PROP_GROUP.equals(evt.getPropertyName())) {
|
||||
return;
|
||||
}
|
||||
String reloadYn = propManager.getProperty(PROP_GROUP, PROP_CACHE_RELOAD_YN, "N");
|
||||
if ("Y".equalsIgnoreCase(reloadYn)) {
|
||||
clearKeyCache();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 키 조회 (외부 노출)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* HSM KeyStore 에서 공개키를 반환합니다.
|
||||
* 공개키는 외부 노출에 제약이 없으며, 직접 암호화에 활용할 수 있습니다.
|
||||
* HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
|
||||
*/
|
||||
public PublicKey getPublicKey(String keyAlias) throws HsmException {
|
||||
checkReady();
|
||||
PublicKey cached = publicKeyCache.get(keyAlias);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
|
||||
if (cert == null) {
|
||||
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
return cert.getPublicKey();
|
||||
PublicKey key = cert.getPublicKey();
|
||||
publicKeyCache.put(keyAlias, key);
|
||||
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
|
||||
return key;
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -60,18 +102,25 @@ public class HsmCryptoService {
|
||||
}
|
||||
|
||||
/**
|
||||
* HSM KeyStore 에서 AES 대칭키를 반환합니다.
|
||||
* HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
|
||||
* HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다.
|
||||
*/
|
||||
public SecretKey getSecretKey(String keyAlias) throws HsmException {
|
||||
checkReady();
|
||||
SecretKey cached = secretKeyCache.get(keyAlias);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
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);
|
||||
}
|
||||
return (SecretKey) key;
|
||||
SecretKey secretKey = (SecretKey) key;
|
||||
secretKeyCache.put(keyAlias, secretKey);
|
||||
logger.warn("HsmCryptoService] 대칭키 캐시 등록: alias=" + keyAlias);
|
||||
return secretKey;
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -79,6 +128,13 @@ public class HsmCryptoService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */
|
||||
public void clearKeyCache() {
|
||||
secretKeyCache.clear();
|
||||
publicKeyCache.clear();
|
||||
logger.warn("HsmCryptoService] 키 캐시 초기화 완료");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// RSA
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -146,15 +202,19 @@ public class HsmCryptoService {
|
||||
|
||||
/**
|
||||
* AES/CBC/PKCS5Padding 암호화.
|
||||
* HSM 없이도 호출 가능합니다 (getSecretKey 로 키를 미리 가져온 경우).
|
||||
* extractable 키: provider=null 로 JVM 소프트웨어 암호화.
|
||||
* non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
|
||||
*
|
||||
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||
* @param iv 초기화 벡터 (16바이트)
|
||||
* @param plaintext 평문 바이트
|
||||
* @param provider null 이면 JVM 기본 Provider 사용
|
||||
*/
|
||||
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException {
|
||||
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext, Provider provider) throws HsmException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
||||
Cipher cipher = (provider != null)
|
||||
? Cipher.getInstance(AES_ALGORITHM, provider)
|
||||
: Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
@@ -162,17 +222,26 @@ public class HsmCryptoService {
|
||||
}
|
||||
}
|
||||
|
||||
/** AES 암호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
|
||||
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException {
|
||||
return encryptAes(secretKey, iv, plaintext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES/CBC/PKCS5Padding 복호화.
|
||||
* HSM 없이도 호출 가능합니다 (getSecretKey 로 키를 미리 가져온 경우).
|
||||
* extractable 키: provider=null 로 JVM 소프트웨어 복호화.
|
||||
* non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
|
||||
*
|
||||
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||
* @param iv 초기화 벡터 (16바이트)
|
||||
* @param ciphertext 암호문 바이트
|
||||
* @param provider null 이면 JVM 기본 Provider 사용
|
||||
*/
|
||||
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException {
|
||||
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext, Provider provider) throws HsmException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
||||
Cipher cipher = (provider != null)
|
||||
? Cipher.getInstance(AES_ALGORITHM, provider)
|
||||
: Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(ciphertext);
|
||||
} catch (Exception e) {
|
||||
@@ -180,6 +249,11 @@ public class HsmCryptoService {
|
||||
}
|
||||
}
|
||||
|
||||
/** AES 복호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
|
||||
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException {
|
||||
return decryptAes(secretKey, iv, ciphertext, null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -80,7 +80,7 @@ public class HsmManager implements Lifecycle {
|
||||
return;
|
||||
}
|
||||
|
||||
pkcs11Provider = createProvider(configContent.trim());
|
||||
pkcs11Provider = createProvider(configContent.trim().replace("\\n", "\n"));
|
||||
|
||||
// 이미 등록된 Provider 가 있으면 제거 후 재등록
|
||||
Provider existing = Security.getProvider(pkcs11Provider.getName());
|
||||
@@ -94,6 +94,14 @@ public class HsmManager implements Lifecycle {
|
||||
keyStore.load(null, pinChars);
|
||||
|
||||
logger.warn("HsmManager] 초기화 완료. Provider=" + pkcs11Provider.getName());
|
||||
|
||||
java.util.Enumeration<String> aliases = keyStore.aliases();
|
||||
StringBuilder aliasList = new StringBuilder();
|
||||
while (aliases.hasMoreElements()) {
|
||||
if (aliasList.length() > 0) aliasList.append(", ");
|
||||
aliasList.append(aliases.nextElement());
|
||||
}
|
||||
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user