Dynamic IV 도출 지원 — DerivedKey iv 필드 추가, HsmKeyAndIvDerivationStrategy 구현

This commit is contained in:
curry772
2026-05-27 13:48:23 +09:00
parent c95307901e
commit c74d1777df
3 changed files with 217 additions and 1 deletions
@@ -0,0 +1,41 @@
package com.eactive.eai.custom.common.security.keyderiv;
import java.util.Map;
import javax.xml.bind.DatatypeConverter;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.security.keyderiv.strategy.BaseHsmKeyDerivationStrategy;
/**
* HSM에서 키와 IV를 각각 가져오는 전략.
* IV는 HSM에서 가져온 바이트 배열을 hex 문자열로 변환한 뒤 substring(32, 64)를 적용하여 사용한다.
* (32바이트 HSM 키 → 64자 hex → substring(32,64) → 16바이트 IV)
*
* key_deriv_params (JSON) 예시:
* { "hsmKeyAlias" : "MASTER_KEY_AES", "hsmIvAlias" : "MASTER_IV_AES" }
*/
public class HsmKeyAndIvSliceDerivationStrategy extends BaseHsmKeyDerivationStrategy {
private static final int IV_HEX_OFFSET = 32;
private static final int IV_HEX_END = 64;
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
byte[] keyBytes = super.getHsmKey(params);
byte[] rawIv = super.getHsmIv(params);
String ivHex = DatatypeConverter.printHexBinary(rawIv);
if (ivHex.length() < IV_HEX_END) {
throw new IllegalArgumentException(
"hsmIvAlias hex 길이 부족: 최소 " + IV_HEX_END + "자 필요, 실제=" + ivHex.length()
+ " (" + rawIv.length + "바이트)");
}
byte[] iv = DatatypeConverter.parseHexBinary(ivHex.substring(IV_HEX_OFFSET, IV_HEX_END));
return new DerivedKey(keyBytes, keyBytes, iv);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
return cryptoName;
}
}
@@ -0,0 +1,175 @@
package com.eactive.eai.custom.common.security.keyderiv;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HsmKeyAndIvSliceDerivationStrategy 단위 테스트
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class HsmKeyAndIvSliceDerivationStrategyTest {
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
private static final String HSM_IV_ALIAS = "MASTER_IV_AES";
/** 32바이트 암호화 키 (AES-256) */
private static final byte[] MASTER_KEY = new byte[32];
/** 32바이트 IV 원본 → hex 64자 → substring(32,64) → 16바이트 IV */
private static final byte[] MASTER_IV = new byte[32];
static {
for (int i = 0; i < MASTER_KEY.length; i++) MASTER_KEY[i] = (byte) (i + 1);
for (int i = 0; i < MASTER_IV.length; i++) MASTER_IV[i] = (byte) (i + 0x10);
}
private static GenericApplicationContext ctx;
private static HsmCryptoService mockHsmCryptoService;
private static HsmKeyAndIvSliceDerivationStrategy strategy;
private Map<String, String> params;
private Map<String, String> runtimeContext;
@BeforeAll
static void setUpClass() throws Exception {
mockHsmCryptoService = mock(HsmCryptoService.class);
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS))
.thenReturn(new SecretKeySpec(MASTER_KEY, "AES"));
when(mockHsmCryptoService.getSecretKey(HSM_IV_ALIAS))
.thenReturn(new SecretKeySpec(MASTER_IV, "AES"));
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
strategy = new HsmKeyAndIvSliceDerivationStrategy();
}
@BeforeEach
void setUpParams() {
params = new HashMap<>();
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
params.put("hsmIvAlias", HSM_IV_ALIAS);
runtimeContext = new HashMap<>();
}
// =========================================================================
// 1. deriveKey
// =========================================================================
@Test
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, encKey/iv 모두 non-null")
void testDeriveKey_validParams_returnsKeyAndIv() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertNotNull(dk);
assertNotNull(dk.getEncKey());
assertNotNull(dk.getIv());
}
@Test
@DisplayName("1-2. encKey는 hsmKeyAlias에 해당하는 HSM 키")
void testDeriveKey_encKeyMatchesMasterKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(MASTER_KEY, dk.getEncKey());
}
@Test
@DisplayName("1-3. encKey == decKey (대칭 방식)")
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(dk.getEncKey(), dk.getDecKey());
}
@Test
@DisplayName("1-4. iv는 HSM IV hex 문자열의 substring(32,64)와 일치")
void testDeriveKey_ivIsHexSubstring32To64() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
String ivHex = DatatypeConverter.printHexBinary(MASTER_IV);
byte[] expected = DatatypeConverter.parseHexBinary(ivHex.substring(32, 64));
assertArrayEquals(expected, dk.getIv(), "iv는 HSM IV hex substring(32,64) 파싱 결과여야 한다");
}
@Test
@DisplayName("1-5. iv 길이 == 16바이트 (hex 32자 = 16바이트)")
void testDeriveKey_ivLength16() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertEquals(16, dk.getIv().length);
}
@Test
@DisplayName("1-6. hsmIvAlias hex 길이가 64자 미만 — IllegalArgumentException")
void testDeriveKey_ivTooShort_throwsException() throws Exception {
byte[] shortIv = new byte[15]; // hex 30자 → 64자 미만
when(mockHsmCryptoService.getSecretKey("SHORT_IV"))
.thenReturn(new SecretKeySpec(shortIv, "AES"));
params.put("hsmIvAlias", "SHORT_IV");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext),
"IV hex 길이 부족 시 IllegalArgumentException이 발생해야 한다");
}
@Test
@DisplayName("1-7. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
void testDeriveKey_missingHsmKeyAlias_throwsException() {
params.remove("hsmKeyAlias");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext));
}
@Test
@DisplayName("1-8. 필수 파라미터 누락(hsmIvAlias) — IllegalArgumentException")
void testDeriveKey_missingHsmIvAlias_throwsException() {
params.remove("hsmIvAlias");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext));
}
// =========================================================================
// 2. buildCacheKey
// =========================================================================
@Test
@DisplayName("2-1. buildCacheKey — cryptoName 단독 반환")
void testBuildCacheKey_returnsCryptoName() {
assertEquals("CRYPTO_A", strategy.buildCacheKey("CRYPTO_A", params, runtimeContext));
}
@Test
@DisplayName("2-2. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
void testBuildCacheKey_differentCryptoName_differentKey() {
assertNotEquals(
strategy.buildCacheKey("CRYPTO_A", params, runtimeContext),
strategy.buildCacheKey("CRYPTO_B", params, runtimeContext));
}
}