feat: SafeNet HSM 연동 구현 (JDK 8 / SunPKCS11)
- HsmManager: SunPKCS11 Provider 초기화, Lifecycle 관리 - JDK 8 / JDK 9+ 분기를 리플렉션으로 처리하는 createProvider() 추가 - PropManager DB에서 PKCS11_CONFIG / PIN 읽어 초기화 - HsmCryptoService: RSA / AES 암복호화 서비스 - encryptRsa: 공개키 기반 JVM 소프트웨어 암호화 - decryptRsa: HSM 내부 복호화 (미초기화 시 HsmException 발생) - encryptAes / decryptAes: AES/CBC/PKCS5Padding - HsmException: HSM 전용 커스텀 예외 - pkcs11-dev.cfg: SoftHSM2용 (slotListIndex = 0) - pkcs11-prod.cfg: SafeNet ProtectServer 운영용 (slot = 0) - HsmSoftHsm2IntegrationTest: 저수준 PKCS11 직접 테스트 (8개) - HsmManagerServiceTest: HsmManager/HsmCryptoService 경유 테스트 (8개) Mockito + GenericApplicationContext로 DB 없이 실행 가능 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* SafeNet ProtectServer HSM 암복호화 서비스 (JDK 8)
|
||||
*
|
||||
* [키 조회]
|
||||
* getPublicKey - 공개키 반환 (외부 노출)
|
||||
* getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요, 외부 노출)
|
||||
* getPrivateKey - 내부 전용 (개인키는 HSM 외부로 반출하지 않는 것이 원칙)
|
||||
*
|
||||
* [암복호화]
|
||||
* encryptRsa - 공개키로 JVM 소프트웨어 암호화 (HSM 불필요)
|
||||
* decryptRsa - alias 기반, HSM 내부 복호화 (HsmManager 미초기화 시 JVM fallback)
|
||||
* encryptAes - SecretKey 객체로 AES/CBC 암호화
|
||||
* decryptAes - SecretKey 객체로 AES/CBC 복호화
|
||||
*/
|
||||
@Service
|
||||
public class HsmCryptoService {
|
||||
|
||||
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";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 키 조회 (외부 노출)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* HSM KeyStore 에서 공개키를 반환합니다.
|
||||
* 공개키는 외부 노출에 제약이 없으며, 직접 암호화에 활용할 수 있습니다.
|
||||
*/
|
||||
public PublicKey getPublicKey(String keyAlias) throws HsmException {
|
||||
checkReady();
|
||||
try {
|
||||
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
|
||||
if (cert == null) {
|
||||
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
return cert.getPublicKey();
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("공개키 조회 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HSM KeyStore 에서 AES 대칭키를 반환합니다.
|
||||
* HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다.
|
||||
*/
|
||||
public SecretKey getSecretKey(String keyAlias) throws HsmException {
|
||||
checkReady();
|
||||
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;
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("AES 키 조회 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// RSA
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* RSA 암호화 - HSM 인증서의 공개키로 JVM 소프트웨어 암호화.
|
||||
* 공개키는 HSM 에서 추출하므로 HsmManager 가 초기화되어 있어야 합니다.
|
||||
*/
|
||||
public byte[] encryptRsa(String keyAlias, byte[] plaintext) throws HsmException {
|
||||
return encryptRsa(getPublicKey(keyAlias), plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 암호화 - 전달받은 공개키로 JVM 소프트웨어 암호화.
|
||||
* HSM 없이도 호출 가능합니다.
|
||||
*/
|
||||
public byte[] encryptRsa(PublicKey publicKey, byte[] plaintext) throws HsmException {
|
||||
try {
|
||||
// Provider 미명시 → JVM 소프트웨어 Provider(SunRsaSign) 자동 선택
|
||||
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("RSA 암호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 복호화 - 개인키 핸들을 HSM 에서 가져와 HSM 내부에서 복호화.
|
||||
* HsmManager 가 초기화되지 않은 경우 JVM 소프트웨어로 fallback 합니다.
|
||||
*
|
||||
* @param keyAlias HSM KeyStore 의 개인키 별칭
|
||||
* @param pin HSM 슬롯 PIN (null 이면 HsmManager 초기화 시 사용한 PIN 재사용)
|
||||
* @param ciphertext 암호문 바이트
|
||||
*/
|
||||
public byte[] decryptRsa(String keyAlias, char[] pin, byte[] ciphertext) throws HsmException {
|
||||
try {
|
||||
HsmManager hsmManager = HsmManager.getInstance();
|
||||
|
||||
if (hsmManager.isReady()) {
|
||||
// HSM 내부 복호화: pkcs11Provider 명시 필수
|
||||
Provider pkcs11Provider = hsmManager.getPkcs11Provider();
|
||||
java.security.Key key = hsmManager.getKeyStore().getKey(keyAlias, pin);
|
||||
if (!(key instanceof PrivateKey)) {
|
||||
throw new HsmException("개인키를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM, pkcs11Provider);
|
||||
cipher.init(Cipher.DECRYPT_MODE, (PrivateKey) key);
|
||||
return cipher.doFinal(ciphertext);
|
||||
|
||||
} else {
|
||||
throw new HsmException("RSA 복호화 불가: HsmManager 가 초기화되지 않았습니다. alias=" + keyAlias);
|
||||
}
|
||||
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("RSA 복호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AES
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* AES/CBC/PKCS5Padding 암호화.
|
||||
* HSM 없이도 호출 가능합니다 (getSecretKey 로 키를 미리 가져온 경우).
|
||||
*
|
||||
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||
* @param iv 초기화 벡터 (16바이트)
|
||||
* @param plaintext 평문 바이트
|
||||
*/
|
||||
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("AES 암호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES/CBC/PKCS5Padding 복호화.
|
||||
* HSM 없이도 호출 가능합니다 (getSecretKey 로 키를 미리 가져온 경우).
|
||||
*
|
||||
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||
* @param iv 초기화 벡터 (16바이트)
|
||||
* @param ciphertext 암호문 바이트
|
||||
*/
|
||||
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(ciphertext);
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("AES 복호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void checkReady() throws HsmException {
|
||||
if (!HsmManager.getInstance().isReady()) {
|
||||
throw new HsmException("HsmManager 가 초기화되지 않았습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user